//======================================================================== // // ImageOutputDev.cc // // Copyright 1998-2003 Glyph & Cog, LLC // //======================================================================== //======================================================================== // // Modified under the Poppler project - http://poppler.freedesktop.org // // All changes made under the Poppler project to this file are licensed // under GPL version 2 or later // // Copyright (C) 2005, 2007, 2011, 2018, 2019, 2021, 2022 Albert Astals Cid // Copyright (C) 2006 Rainer Keller // Copyright (C) 2008 Timothy Lee // Copyright (C) 2008 Vasile Gaburici // Copyright (C) 2009 Carlos Garcia Campos // Copyright (C) 2009 William Bader // Copyright (C) 2010 Jakob Voss // Copyright (C) 2012, 2013, 2017, 2018 Adrian Johnson // Copyright (C) 2013 Thomas Fischer // Copyright (C) 2013 Hib Eris // Copyright (C) 2017 Caolán McNamara // Copyright (C) 2018 Andreas Gruenbacher // Copyright (C) 2020 mrbax <12640-mrbax@users.noreply.gitlab.freedesktop.org> // Copyright (C) 2024 Fernando Herrera // Copyright (C) 2024 Sebastian J. Bronner // // To see a description of the changes please see the Changelog file that // came with your tarball or type make ChangeLog if you are building from git // //======================================================================== #include "config.h" #include #include #include #include #include #include #include "goo/gmem.h" #include "goo/NetPBMWriter.h" #include "goo/PNGWriter.h" #include "goo/TiffWriter.h" #include "Error.h" #include "GfxState.h" #include "Object.h" #include "Stream.h" #include "JBIG2Stream.h" #include "ImageOutputDev.h" ImageOutputDev::ImageOutputDev(char *fileRootA, bool pageNamesA, bool listImagesA) { listImages = listImagesA; if (!listImages) { fileRoot = copyString(fileRootA); fileName = (char *)gmalloc(strlen(fileRoot) + 45); } outputPNG = false; outputTiff = false; dumpJPEG = false; dumpJP2 = false; dumpJBIG2 = false; dumpCCITT = false; pageNames = pageNamesA; printFilenames = false; imgNum = 0; pageNum = 0; errorCode = 0; if (listImages) { printf("page num type width height color comp bpc enc interp object ID x-ppi y-ppi size ratio\n"); printf("--------------------------------------------------------------------------------------------\n"); } } ImageOutputDev::~ImageOutputDev() { if (!listImages) { gfree(fileName); gfree(fileRoot); } } void ImageOutputDev::setFilename(const char *fileExt) { if (pageNames) { sprintf(fileName, "%s-%03d-%03d.%s", fileRoot, pageNum, imgNum, fileExt); } else { sprintf(fileName, "%s-%03d.%s", fileRoot, imgNum, fileExt); } } // Print a floating point number between 0 - 9999 using 4 characters // eg '1.23', '12.3', ' 123', '1234' // // We need to be careful to handle the cases where rounding adds an // extra digit before the decimal. eg printf("%4.2f", 9.99999) // outputs "10.00" instead of "9.99". static void printNumber(double d) { char buf[10]; if (d < 10.0) { sprintf(buf, "%4.2f", d); buf[4] = 0; printf("%s", buf); } else if (d < 100.0) { sprintf(buf, "%4.1f", d); if (!isdigit(buf[3])) { buf[3] = 0; printf(" %s", buf); } else { printf("%s", buf); } } else { printf("%4.0f", d); } } void ImageOutputDev::listImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, bool inlineImg, ImageType imageType) { const char *type; const char *colorspace; const char *enc; int components, bpc; printf("%4d %5d ", pageNum, imgNum); type = ""; switch (imageType) { case imgImage: type = "image"; break; case imgStencil: type = "stencil"; break; case imgMask: type = "mask"; break; case imgSmask: type = "smask"; break; } printf("%-7s %5d %5d ", type, width, height); colorspace = "-"; /* masks and stencils default to ncomps = 1 and bpc = 1 */ components = 1; bpc = 1; if (colorMap && colorMap->isOk()) { switch (colorMap->getColorSpace()->getMode()) { case csDeviceGray: case csCalGray: colorspace = "gray"; break; case csDeviceRGB: case csCalRGB: colorspace = "rgb"; break; case csDeviceCMYK: colorspace = "cmyk"; break; case csLab: colorspace = "lab"; break; case csICCBased: colorspace = "icc"; break; case csIndexed: colorspace = "index"; break; case csSeparation: colorspace = "sep"; break; case csDeviceN: colorspace = "devn"; break; case csPattern: default: colorspace = "-"; break; } components = colorMap->getNumPixelComps(); bpc = colorMap->getBits(); } printf("%-5s %2d %2d ", colorspace, components, bpc); switch (str->getKind()) { case strCCITTFax: enc = "ccitt"; break; case strDCT: enc = "jpeg"; break; case strJPX: enc = "jpx"; break; case strJBIG2: enc = "jbig2"; break; case strFile: case strFlate: case strCachedFile: case strASCIIHex: case strASCII85: case strLZW: case strRunLength: case strWeird: default: enc = "image"; break; } printf("%-5s ", enc); printf("%-3s ", interpolate ? "yes" : "no"); if (inlineImg) { printf("[inline] "); } else if (ref->isRef()) { const Ref imageRef = ref->getRef(); if (imageRef.gen >= 100000) { printf("[none] "); } else { printf(" %6d %2d ", imageRef.num, imageRef.gen); } } else { printf("[none] "); } const double *mat = state->getCTM(); double width2 = sqrt(mat[0] * mat[0] + mat[1] * mat[1]); double height2 = sqrt(mat[2] * mat[2] + mat[3] * mat[3]); double xppi = fabs(width * 72.0 / width2); double yppi = fabs(height * 72.0 / height2); if (xppi < 1.0) { printf("%5.3f ", xppi); } else { printf("%5.0f ", xppi); } if (yppi < 1.0) { printf("%5.3f ", yppi); } else { printf("%5.0f ", yppi); } Goffset embedSize = -1; if (inlineImg) { embedSize = getInlineImageLength(str, width, height, colorMap); } else { embedSize = str->getBaseStream()->getLength(); } long long imageSize = 0; if (colorMap && colorMap->isOk()) { imageSize = ((long long)width * height * colorMap->getNumPixelComps() * colorMap->getBits()) / 8; } else { imageSize = (long long)width * height / 8; // mask } double ratio = -1.0; if (imageSize > 0) { ratio = 100.0 * embedSize / imageSize; } if (embedSize < 0) { printf(" - "); } else if (embedSize <= 9999) { printf("%4lldB", embedSize); } else { double d = embedSize / 1024.0; if (d <= 9999.0) { printNumber(d); putchar('K'); } else { d /= 1024.0; if (d <= 9999.0) { printNumber(d); putchar('M'); } else { d /= 1024.0; printNumber(d); putchar('G'); } } } if (ratio > 9.9) { printf(" %3.0f%%\n", ratio); } else if (ratio >= 0.0) { printf(" %3.1f%%\n", ratio); } else { printf(" - \n"); } ++imgNum; } long ImageOutputDev::getInlineImageLength(Stream *str, int width, int height, GfxImageColorMap *colorMap) { long len; if (colorMap) { ImageStream *imgStr = new ImageStream(str, width, colorMap->getNumPixelComps(), colorMap->getBits()); imgStr->reset(); for (int y = 0; y < height; y++) { imgStr->getLine(); } imgStr->close(); delete imgStr; } else { str->reset(); for (int y = 0; y < height; y++) { int size = (width + 7) / 8; for (int x = 0; x < size; x++) { str->getChar(); } } } EmbedStream *embedStr = (EmbedStream *)(str->getBaseStream()); embedStr->rewind(); len = 0; while (embedStr->getChar() != EOF) { len++; } embedStr->restore(); return len; } void ImageOutputDev::writeRawImage(Stream *str, const char *ext) { FILE *f; int c; // open the image file setFilename(ext); ++imgNum; if (!(f = fopen(fileName, "wb"))) { error(errIO, -1, "Couldn't open image file '{0:s}'", fileName); errorCode = 2; return; } // initialize stream str = str->getNextStream(); str->reset(); // copy the stream while ((c = str->getChar()) != EOF) { fputc(c, f); } str->close(); fclose(f); } void ImageOutputDev::writeImageFile(ImgWriter *writer, ImageFormat format, const char *ext, Stream *str, int width, int height, GfxImageColorMap *colorMap) { FILE *f = nullptr; /* squelch bogus compiler warning */ ImageStream *imgStr = nullptr; unsigned char *row; unsigned char *rowp; unsigned char *p; GfxRGB rgb; GfxCMYK cmyk; GfxGray gray; unsigned char zero[gfxColorMaxComps]; int invert_bits; if (writer) { setFilename(ext); ++imgNum; if (!(f = fopen(fileName, "wb"))) { error(errIO, -1, "Couldn't open image file '{0:s}'", fileName); errorCode = 2; return; } if (!writer->init(f, width, height, 72, 72)) { error(errIO, -1, "Error writing '{0:s}'", fileName); errorCode = 2; return; } } int pixelSize = sizeof(unsigned int); if (format == imgRGB48) { pixelSize = 2 * sizeof(unsigned int); } row = (unsigned char *)gmallocn_checkoverflow(width, pixelSize); if (!row) { error(errIO, -1, "Image data for '{0:s}' is too big. {1:d} width with {2:d} bytes per pixel", fileName, width, pixelSize); errorCode = 99; return; } if (format != imgMonochrome) { // initialize stream imgStr = new ImageStream(str, width, colorMap->getNumPixelComps(), colorMap->getBits()); imgStr->reset(); } else { // initialize stream str->reset(); } // PDF masks use 0 = draw current color, 1 = leave unchanged. // We invert this to provide the standard interpretation of alpha // (0 = transparent, 1 = opaque). If the colorMap already inverts // the mask we leave the data unchanged. invert_bits = 0xff; if (colorMap) { memset(zero, 0, sizeof(zero)); colorMap->getGray(zero, &gray); if (colToByte(gray) == 0) { invert_bits = 0x00; } } // for each line... for (int y = 0; y < height; y++) { switch (format) { case imgRGB: p = imgStr->getLine(); rowp = row; for (int x = 0; x < width; ++x) { if (p) { colorMap->getRGB(p, &rgb); *rowp++ = colToByte(rgb.r); *rowp++ = colToByte(rgb.g); *rowp++ = colToByte(rgb.b); p += colorMap->getNumPixelComps(); } else { *rowp++ = 0; *rowp++ = 0; *rowp++ = 0; } } if (writer) { writer->writeRow(&row); } break; case imgRGB48: { p = imgStr->getLine(); unsigned short *rowp16 = reinterpret_cast(row); for (int x = 0; x < width; ++x) { if (p) { colorMap->getRGB(p, &rgb); *rowp16++ = colToShort(rgb.r); *rowp16++ = colToShort(rgb.g); *rowp16++ = colToShort(rgb.b); p += colorMap->getNumPixelComps(); } else { *rowp16++ = 0; *rowp16++ = 0; *rowp16++ = 0; } } if (writer) { writer->writeRow(&row); } break; } case imgCMYK: p = imgStr->getLine(); rowp = row; for (int x = 0; x < width; ++x) { if (p) { colorMap->getCMYK(p, &cmyk); *rowp++ = colToByte(cmyk.c); *rowp++ = colToByte(cmyk.m); *rowp++ = colToByte(cmyk.y); *rowp++ = colToByte(cmyk.k); p += colorMap->getNumPixelComps(); } else { *rowp++ = 0; *rowp++ = 0; *rowp++ = 0; *rowp++ = 0; } } if (writer) { writer->writeRow(&row); } break; case imgGray: p = imgStr->getLine(); rowp = row; for (int x = 0; x < width; ++x) { if (p) { colorMap->getGray(p, &gray); *rowp++ = colToByte(gray); p += colorMap->getNumPixelComps(); } else { *rowp++ = 0; } } if (writer) { writer->writeRow(&row); } break; case imgMonochrome: int size = (width + 7) / 8; for (int x = 0; x < size; x++) { row[x] = str->getChar() ^ invert_bits; } if (writer) { writer->writeRow(&row); } break; } } gfree(row); if (format != imgMonochrome) { imgStr->close(); delete imgStr; } str->close(); if (writer) { writer->close(); fclose(f); } } void ImageOutputDev::writeImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool inlineImg) { ImageFormat format; EmbedStream *embedStr; if (inlineImg) { embedStr = (EmbedStream *)(str->getBaseStream()); // Record the stream. This determines the size. getInlineImageLength(str, width, height, colorMap); // Reading the stream again will return EOF at end of recording. embedStr->rewind(); } if (dumpJPEG && str->getKind() == strDCT) { // dump JPEG file writeRawImage(str, "jpg"); } else if (dumpJP2 && str->getKind() == strJPX && !inlineImg) { // dump JPEG2000 file writeRawImage(str, "jp2"); } else if (dumpJBIG2 && str->getKind() == strJBIG2 && !inlineImg) { // dump JBIG2 globals stream if available JBIG2Stream *jb2Str = static_cast(str); Object *globals = jb2Str->getGlobalsStream(); if (globals->isStream()) { FILE *f; int c; Stream *globalsStr = globals->getStream(); setFilename("jb2g"); if (!(f = fopen(fileName, "wb"))) { error(errIO, -1, "Couldn't open image file '{0:s}'", fileName); errorCode = 2; return; } globalsStr->reset(); while ((c = globalsStr->getChar()) != EOF) { fputc(c, f); } globalsStr->close(); fclose(f); } // dump JBIG2 embedded file writeRawImage(str, "jb2e"); } else if (dumpCCITT && str->getKind() == strCCITTFax) { // write CCITT parameters CCITTFaxStream *ccittStr = static_cast(str); FILE *f; setFilename("params"); if (!(f = fopen(fileName, "wb"))) { error(errIO, -1, "Couldn't open image file '{0:s}'", fileName); errorCode = 2; return; } if (ccittStr->getEncoding() < 0) { fprintf(f, "-4 "); } else if (ccittStr->getEncoding() == 0) { fprintf(f, "-1 "); } else { fprintf(f, "-2 "); } if (ccittStr->getEndOfLine()) { fprintf(f, "-A "); } else { fprintf(f, "-P "); } fprintf(f, "-X %d ", ccittStr->getColumns()); if (ccittStr->getBlackIs1()) { fprintf(f, "-W "); } else { fprintf(f, "-B "); } fprintf(f, "-M\n"); // PDF uses MSB first fclose(f); // dump CCITT file writeRawImage(str, "ccitt"); } else if (outputPNG && !(outputTiff && colorMap && (colorMap->getColorSpace()->getMode() == csDeviceCMYK || (colorMap->getColorSpace()->getMode() == csICCBased && colorMap->getNumPixelComps() == 4)))) { // output in PNG format #ifdef ENABLE_LIBPNG ImgWriter *writer; if (!colorMap || (colorMap->getNumPixelComps() == 1 && colorMap->getBits() == 1)) { writer = new PNGWriter(PNGWriter::MONOCHROME); format = imgMonochrome; } else if (colorMap->getColorSpace()->getMode() == csDeviceGray || colorMap->getColorSpace()->getMode() == csCalGray) { writer = new PNGWriter(PNGWriter::GRAY); format = imgGray; } else if ((colorMap->getColorSpace()->getMode() == csDeviceRGB || colorMap->getColorSpace()->getMode() == csCalRGB || (colorMap->getColorSpace()->getMode() == csICCBased && colorMap->getNumPixelComps() == 3)) && colorMap->getBits() > 8) { writer = new PNGWriter(PNGWriter::RGB48); format = imgRGB48; } else { writer = new PNGWriter(PNGWriter::RGB); format = imgRGB; } writeImageFile(writer, format, "png", str, width, height, colorMap); delete writer; #endif } else if (outputTiff) { // output in TIFF format #ifdef ENABLE_LIBTIFF ImgWriter *writer; if (!colorMap || (colorMap->getNumPixelComps() == 1 && colorMap->getBits() == 1)) { writer = new TiffWriter(TiffWriter::MONOCHROME); format = imgMonochrome; } else if (colorMap->getColorSpace()->getMode() == csDeviceGray || colorMap->getColorSpace()->getMode() == csCalGray) { writer = new TiffWriter(TiffWriter::GRAY); format = imgGray; } else if (colorMap->getColorSpace()->getMode() == csDeviceCMYK || (colorMap->getColorSpace()->getMode() == csICCBased && colorMap->getNumPixelComps() == 4)) { writer = new TiffWriter(TiffWriter::CMYK); format = imgCMYK; } else if ((colorMap->getColorSpace()->getMode() == csDeviceRGB || colorMap->getColorSpace()->getMode() == csCalRGB || (colorMap->getColorSpace()->getMode() == csICCBased && colorMap->getNumPixelComps() == 3)) && colorMap->getBits() > 8) { writer = new TiffWriter(TiffWriter::RGB48); format = imgRGB48; } else { writer = new TiffWriter(TiffWriter::RGB); format = imgRGB; } writeImageFile(writer, format, "tif", str, width, height, colorMap); delete writer; #endif } else { // output in PPM/PBM format ImgWriter *writer; if (!colorMap || (colorMap->getNumPixelComps() == 1 && colorMap->getBits() == 1)) { writer = new NetPBMWriter(NetPBMWriter::MONOCHROME); format = imgMonochrome; } else { writer = new NetPBMWriter(NetPBMWriter::RGB); format = imgRGB; } writeImageFile(writer, format, format == imgRGB ? "ppm" : "pbm", str, width, height, colorMap); delete writer; } if (inlineImg) { embedStr->restore(); } if (printFilenames) { printf("%s\n", fileName); } } bool ImageOutputDev::tilingPatternFill(GfxState *state, Gfx *gfx, Catalog *cat, GfxTilingPattern *tPat, const double *mat, int x0, int y0, int x1, int y1, double xStep, double yStep) { return true; // do nothing -- this avoids the potentially slow loop in Gfx.cc } void ImageOutputDev::drawImageMask(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool interpolate, bool inlineImg) { if (listImages) { listImage(state, ref, str, width, height, nullptr, interpolate, inlineImg, imgStencil); } else { writeImage(state, ref, str, width, height, nullptr, inlineImg); } } void ImageOutputDev::drawImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, const int *maskColors, bool inlineImg) { if (listImages) { listImage(state, ref, str, width, height, colorMap, interpolate, inlineImg, imgImage); } else { writeImage(state, ref, str, width, height, colorMap, inlineImg); } } void ImageOutputDev::drawMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, bool maskInvert, bool maskInterpolate) { if (listImages) { listImage(state, ref, str, width, height, colorMap, interpolate, false, imgImage); listImage(state, ref, maskStr, maskWidth, maskHeight, nullptr, maskInterpolate, false, imgMask); } else { writeImage(state, ref, str, width, height, colorMap, false); writeImage(state, ref, maskStr, maskWidth, maskHeight, nullptr, false); } } void ImageOutputDev::drawSoftMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, GfxImageColorMap *maskColorMap, bool maskInterpolate) { if (listImages) { listImage(state, ref, str, width, height, colorMap, interpolate, false, imgImage); listImage(state, ref, maskStr, maskWidth, maskHeight, maskColorMap, maskInterpolate, false, imgSmask); } else { writeImage(state, ref, str, width, height, colorMap, false); writeImage(state, ref, maskStr, maskWidth, maskHeight, maskColorMap, false); } }