#define PROG_NAME "pnmpairtopng" #define PROG_DESC "convert two PNM files into a partially transparent PNG." #define PROG_VERS "1.0" /* Last edited on 2024-12-25 09:46:28 by stolfi */ /* Copyright © 2003 by the State University of Campinas (UNICAMP). ** See the copyright, authorship, and warranty notice at end of file. */ #define PROG_HELP \ " " PROG_NAME " \\\n" \ " " argparser_help_info_HELP " \\\n" \ " {BLACKPNMFILE} {WHITEPNMFILE} \\\n" \ " > {OUTPNG}" #define PROG_INFO \ "NAME\n" \ " " PROG_NAME " - " PROG_DESC "\n" \ "\n" \ "SYNOPSIS\n" \ PROG_HELP "\n" \ "\n" \ "DESCRIPTION\n" \ " The program reads two PBM, PGM, or PBM image files," \ " {BLACKPNMFILE} and {WHITEPNMFILE}, and combines" \ " them into a PNG file with alpha (opacity) channel." \ " The file name \"-\" denotes the standard input or output.\n" \ "\n" \ " The two images must have the same pixel type(PPM, PGM, or PBM), the" \ " same dimensions, and the same {maxval}. They must show" \ " the same object, in exactly the" \ " same pose, imaged against a black background and against" \ " a white background, respectively. The transparency of" \ " each pixel is deduced from its colors in the two images.\n" \ "\n" \ " The technique works well with synthetic images such as" \ " those generated by POV-Ray. It does the right thing on" \ " anti-aliased edges, projected shadows and penumbras," \ " translucent materials, etc. Transparent materials with" \ " will also work if they are thin and neutral-tinted. Thick" \ " refractive and mirrored surfaces may work only if they" \ " are neutral-tinted and are used against untextured" \ " backgrounds. The technique does not work for colored" \ " transparent objects and more complicated" \ " object-background light interactions.\n" \ "\n" \ " The output file will be in color (3 chennels + alpha) if" \ " the inputs are PPM files,and grayscale (1 channel + alpha) otherwise. The" \ " output will use 8 bits per channel if the input maxval is 255 or" \ " less, and 16 bits per channel otherwise. In any case, the output will" \ " be true-color (i.e. not palette-based), and the alpha channel will be" \ " stored as an extra channel (i.e. not through a \"tRNS\" transparency chunk).\n" \ "\n" \ " The input images are assumed to use gamma = 1 (linear intensity" \ "\n" \ " encoding), which is saved in the PNG file as the file gamma. The output samples" \ "\n" \ " will be compressed with some internally chosen filter and compression level.\n" \ "\n" \ " This is an intelligent program, because it does not try to be smart. All" \ " input information is preserved to the extent allowed by the output" \ " format. Any additional manipulations of the output file, such" \ " as the insertion of ancillary chunks, should be performed by" \ " separate PNG tools.\n" \ "\n" \ "OPTIONS\n" \ " None.\n" \ "\n" \ "DOCUMENTATION OPTIONS\n" \ argparser_help_info_HELP_INFO "\n" \ "\n" \ "SEE ALSO\n" \ " pnmtopng(1).\n" \ "\n" \ "AUTHOR\n" \ " Created 2003 by Jorge Stolfi, IC-UNICAMP.\n" \ "\n" \ " Inspired by pnmtopng.c by A. Lehmann, W. van Schaik, and G.\n" \ " Roelofs, as well as on other NetPBM programs by M. Wijkstra and J.\n" \ " Poskanzer.\n" \ "\n" \ "MODIFICATION HISTORY\n" \ " 2006-11-xx: Rewritten to use lean PBM libs, argparser, etc. J. Stolfi.\n" \ " 2024-12-25: Merged old manpage into the {PROG_INFO} string. J. Stolfi.\n" \ "\n" \ "WARRANTY\n" \ argparser_help_info_NO_WARRANTY "\n" \ "\n" \ "RIGHTS\n" \ " Copyright © 2003 by the State University of Campinas (UNICAMP).\n" \ "\n" \ argparser_help_info_STANDARD_RIGHTS #include #include #include #include #include #include #include #include #include #include #include #define MAXCHANNELS 3 typedef struct options_t { char *bname; /* Black-background image filename. */ char *wname; /* White-background image filename. */ } options_t; /* PROTOTYPES */ int main(int argc, char* argv[]); void process_ppm_images(FILE *bifp, FILE *wifp, FILE *ofp); /* Reads the black-bg and white-bg PNM images from streams {bifp,wifp}, and writes the PNG image to stream {ofp}. */ void start_png_output ( FILE *ofp, int cols, int rows, int chns, uint16_t imaxval, png_struct **prP, png_info **piP, int *omaxvalP ); /* Creates the header and internal structures for writing a PNG image to file {ofp}. The image will have dimensions {cols,rows} and {chns} channels (1 or 3) plus alpha. The depth of each channel will be 8 bits/sample if {imaxval <= 255}, 16 bits/sample otherwise. Returns in {*prP} and {*piP} the status and info records of the PNG library, and in {*omaxvalP} the maximum PNG sample value (2^depth - 1). */ options_t *parse_options(int argc, char **argv); /* Parses the command line options. */ void print_version(void); /* Prints the program version and other comilation information. */ int main(int argc, char* argv[]) { options_t *o = parse_options(argc, argv); if ((strcmp(o->bname, "-") == 0) && (strcmp(o->wname, "-") == 0)) { pnm_error("cannot use \"-\" for both input images"); } FILE* bifp = open_read(o->bname, TRUE); FILE* wifp = open_read(o->wname, TRUE); process_ppm_images(bifp, wifp, stdout); if (bifp != stdin) { fclose(bifp); } if (wifp != stdin) { fclose(wifp); } return 0; } void process_ppm_images(FILE *bifp, FILE *wifp, FILE *ofp) { uint16_t bformat; /* Include raw/nonraw distinction. */ bool_t braw, bbits; uint16_t wformat; /* Include raw/nonraw distinction. */ bool_t wraw, wbits; int rows, cols, chns; uint16_t imaxval; /* Input {maxval}. */ /* Read PNM image headers: */ { int brows, bcols, bchns; uint16_t bmaxval; pnm_read_header(bifp, &bcols, &brows, &bchns, &bmaxval, &braw, &bbits, &bformat); int wrows, wcols, wchns; uint16_t wmaxval; pnm_read_header(wifp, &wcols, &wrows, &wchns, &wmaxval, &wraw, &wbits, &wformat); /* Compatibility checks: */ if (wcols != bcols || wrows != brows) { pnm_error("the two images must have the same width and height"); } cols = bcols; rows = brows; chns = bchns; if (wchns != bchns) { pnm_error("the two images must have the same number of channels"); } if (wmaxval != bmaxval) { pnm_error("the two images must have the same maxval"); } imaxval = bmaxval; } png_struct *pr; png_info *pi; int omaxval; start_png_output(ofp, cols, rows, chns, imaxval, &pr, &pi, &omaxval); /* Allocate input scanline buffers: */ uint16_t *bxelrow = uint16_image_alloc_pixel_row(cols, chns); uint16_t *wxelrow = uint16_image_alloc_pixel_row(cols, chns); /* Allocate output scanline buffer: */ int spp = chns + 1; /* Samples per PNG pixel. */ png_byte *oxelrow = (png_byte *)malloc(cols*spp*sizeof(png_byte)); assert(oxelrow != NULL); /* Compute the input-to-output scaling factor {scale}. If {omaxval == imaxval}, {scale} is exactly 1, and the mapping is one-to-one for fully opaque pixels. In general, the input-to-output mapping is injective as long as {omaxval >= imaxval}. Note that in the common case, when {imaxval} and {omaxval} are of the form {2^k-1}, the conversion is NOT a simple binary shift. */ double iscale = imaxval; double oscale = omaxval; double bv[MAXCHANNELS]; /* Scaled black pixel. */ double wv[MAXCHANNELS]; /* Scaled white pixel. */ double cv[MAXCHANNELS]; /* Color of pixel. */ /* Process rows: */ /* Assumes non-interlaced PNG: */ int row; for (row = 0; row < rows; ++row) { pnm_read_pixels(bifp, bxelrow, cols, chns, imaxval, braw, bbits); pnm_read_pixels(wifp, wxelrow, cols, chns, imaxval, wraw, wbits); int col; uint16_t *bp = &(bxelrow[0]); uint16_t *wp = &(wxelrow[0]); png_byte *op = &(oxelrow[0]); for(col = 0; col < cols; ++col) { bool_t debug = (col == row); /* Get input pixels, scaled to [0_1]: */ int c; for (c = 0; c < chns; c++) { bv[c] = (*bp)/iscale; bp++; wv[c] = (*wp)/iscale; wp++; } if (debug) { fprintf(stderr, "bv = "); for (c = 0; c < chns; c++) { fprintf(stderr, " %5.3f", bv[c]); } fprintf(stderr, " wv = "); for (c = 0; c < chns; c++) { fprintf(stderr, " %5.3f", wv[c]); } } /* Reduce input pixels to grayscale {by,wy}: */ double by, wy; if (chns == 1) { by = bv[0]; wy = wv[0]; } else if (chns == 3) { by = (6969 * bv[0] + 23434 * bv[1] + 2365 * bv[2])/32768; wy = (6969 * wv[0] + 23434 * wv[1] + 2365 * wv[2])/32768; } else { assert(FALSE); } /* The difference in brightness should be {1-alpha}: */ double alpha = 1.0 - fabs(wy - by); /* Make sure that alpha lies in [0_omaxval]: */ if (alpha < 0.0) { alpha = 0.0; } if (alpha > 1.0) { alpha = 1.0; } for (c = 0; c < chns; c++) { /* Compute the color for 0.5 gray background: */ cv[c] = 0.5*(bv[c] + wv[c]); /* Increase {alpha} to make {cv[c]} consistent: */ if (alpha < 1.0 - 2*cv[c]) { alpha = 1.0 - 2*cv[c]; } } if (debug) { fprintf(stderr, " cv = "); for (c = 0; c < chns; c++) { fprintf(stderr, " %5.3f", cv[c]); } } /* Quantize the {alpha} value: */ int qca = (int)(oscale*alpha + 0.5); alpha = qca/oscale; if (debug) { fprintf(stderr, " alpha = %5.3f", alpha); } /* Compute the foreground color {cv[]}: */ if (qca == 0) { /* Fully transparent pixel, set foreground to black: */ for (c = 0; c < chns; c++) { cv[c] = 0.0; } } else if (qca == omaxval) { /* Fully opaque pixel, use {cv[]} itself as foreground: */ } else { /* Compute self-color {cv'} from {cv = (1-alpha)*0.5 + alpha*cv'}: */ for (c = 0; c < chns; c++) { cv[c] = (cv[c] - 0.5*(1.0 - alpha))/alpha; if (cv[c] < 0.0) { cv[c] = 0.0; } if (cv[c] > 1.0) { cv[c] = 1.0; } } } if (debug) { fprintf(stderr, " cv = "); for (c = 0; c < chns; c++) { fprintf(stderr, " %5.3f", cv[c]); } } /* Quantize {cv} and store in output buffer: */ for (c = 0; c < chns; c++) { (*op) = (int)(oscale*cv[c] + 0.5); op++; } /* Store quantized alpha in output buffer: */ (*op) = qca; op++; /* (*op) = omaxval; op++; */ if (debug) { fprintf(stderr, "\n"); } } png_write_row(pr, oxelrow); } png_write_end(pr, pi); fflush(ofp); png_destroy_write_struct(&pr, &pi); free(bxelrow); free(wxelrow); } void start_png_output ( FILE *ofp, int cols, int rows, int chns, uint16_t imaxval, png_struct **prP, png_info **piP, int *omaxvalP ) { /* Create {libpng} work records {**prP, **piP}. */ /* Avoid {setjmp/longjmp} complexity for now, use default error handling. */ png_struct *pr; pr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (pr == NULL) { pnm_error("cannot allocate main libpng structure (pr)"); } png_info *pi; pi = png_create_info_struct(pr); if (pi == NULL) { pnm_error("cannot allocate libpng info structure (pi)"); } pi->width = cols; pi->height = rows; /* Assign {ofp} as the output file for writes to {pr}: */ png_init_io(pr, ofp); /* Select output image type: */ pi->color_type = (chns == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_GRAY) | PNG_COLOR_MASK_ALPHA; pi->interlace_type = FALSE; /* Select bit depth. */ /* Note that alpha channel is allowed only on 8-bit and 16-bit images. */ int depth_ot; unsigned int imv = imaxval; /* To supress a bogus compiler warning. */ if (imv <= 255) { depth_ot = 8; } else if (imv <= 65535) { depth_ot = 16; } else { pnm_message("excessive input maxval, will lose precision"); depth_ot = 16; } pi->bit_depth = depth_ot; /* Set filter type: */ png_set_filter(pr, 0, PNG_FILTER_PAETH); /* Set the {zlib} compression level: */ png_set_compression_level(pr, 0); /* Let libpng take care of, e.g., bit-depth conversions: */ png_set_packing(pr); /* Provide a "gAMA" chunk. Note that inputs have {gamma == 1}. */ pi->valid |= PNG_INFO_gAMA; png_set_gAMA(pr, pi, 1.0); /* Write the PNG header: */ png_write_info (pr, pi); (*omaxvalP) = (1 << depth_ot) - 1; (*prP) = pr; (*piP) = pi; } options_t *parse_options(int argc, char **argv) { argparser_t *pp = argparser_new(stderr, argc, argv); argparser_set_help(pp, PROG_NAME " version " PROG_VERS ", usage:\n" PROG_HELP); argparser_set_info(pp, PROG_INFO); argparser_process_help_info_options(pp); options_t *o = (options_t *)notnull(malloc(sizeof(options_t)), "no mem"); /* Get mandatory black-background and white-background image names: */ argparser_skip_parsed(pp); o->bname = argparser_get_next(pp); o->wname = argparser_get_next(pp); /* Check for extraneous args after image names: */ argparser_finish(pp); return o; } void print_version(void) { fprintf(stderr,"%s version %s.\n", PROG_NAME, PROG_VERS); fprintf(stderr, " Compiled with libpng %s; using libpng %s.\n", PNG_LIBPNG_VER_STRING, png_libpng_ver); fprintf(stderr, " Compiled with zlib %s; using zlib %s.\n", ZLIB_VERSION, zlib_version); fprintf(stderr, "\n"); }