blob: 3af77555ddec5434354fab95d7298475a47dba09 [file] [log] [blame] [edit]
//========================================================================
//
// 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 <aacid@kde.org>
// Copyright (C) 2006 Rainer Keller <class321@gmx.de>
// Copyright (C) 2008 Timothy Lee <timothy.lee@siriushk.com>
// Copyright (C) 2008 Vasile Gaburici <gaburici@cs.umd.edu>
// Copyright (C) 2009 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2009 William Bader <williambader@hotmail.com>
// Copyright (C) 2010 Jakob Voss <jakob.voss@gbv.de>
// Copyright (C) 2012, 2013, 2017, 2018 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2013 Thomas Fischer <fischer@unix-ag.uni-kl.de>
// Copyright (C) 2013 Hib Eris <hib@hiberis.nl>
// Copyright (C) 2017 Caolán McNamara <caolanm@redhat.com>
// Copyright (C) 2018 Andreas Gruenbacher <agruenba@redhat.com>
// Copyright (C) 2020 mrbax <12640-mrbax@users.noreply.gitlab.freedesktop.org>
// Copyright (C) 2024 Fernando Herrera <fherrera@onirica.com>
// Copyright (C) 2024 Sebastian J. Bronner <waschtl@sbronner.com>
//
// 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 <poppler-config.h>
#include <cstdio>
#include <cstdlib>
#include <cstddef>
#include <cctype>
#include <cmath>
#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<unsigned short *>(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<JBIG2Stream *>(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<CCITTFaxStream *>(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);
}
}