//========================================================================
//
// 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 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>
//
// 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;
  imgNum = 0;
  pageNum = 0;
  ok = true;
  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 = mat[0] + mat[2];
  double height2 = mat[1] + mat[3];
  double xppi = fabs(width*72.0/width2) + 0.5;
  double yppi = fabs(height*72.0/height2) + 0.5;
  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);
    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);
      return;
    }

    if (!writer->init(f, width, height, 72, 72)) {
      error(errIO, -1, "Error writing '{0:s}'", fileName);
      return;
    }
  }

  if (format != imgMonochrome) {
    // initialize stream
    imgStr = new ImageStream(str, width, colorMap->getNumPixelComps(),
                             colorMap->getBits());
    imgStr->reset();
  } else {
    // initialize stream
    str->reset();
  }

  int pixelSize = sizeof(unsigned int);
  if (format == imgRGB48)
    pixelSize = 2*sizeof(unsigned int);

  row = (unsigned char *) gmallocn(width, pixelSize);

  // 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);
        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);
      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();
}

bool ImageOutputDev::tilingPatternFill(GfxState *state, Gfx *gfx, Catalog *cat, Object *str,
				  const double *pmat, int paintType, int tilingType, Dict *resDict,
				  const double *mat, const double *bbox,
				  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, str, 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);
  }
}
