/*
 * Copyright (C)2011-2012, 2014-2015, 2017-2018, 2022-2024 D. R. Commander.
 *                                                         All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the libjpeg-turbo Project nor the names of its
 *   contributors may be used to endorse or promote products derived from this
 *   software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * This program demonstrates how to use the TurboJPEG C API to approximate the
 * functionality of the IJG's jpegtran program.  jpegtran features that are not
 * covered:
 *
 * - Scan scripts
 * - Expanding the input image when cropping
 * - Wiping a region of the input image
 * - Dropping another JPEG image into the input image
 * - Progress reporting
 * - Treating warnings as non-fatal [limitation of the TurboJPEG Java API]
 * - Debug output
 */

import java.io.*;
import java.util.*;
import org.libjpegturbo.turbojpeg.*;


@SuppressWarnings("checkstyle:JavadocType")
final class TJTran {

  private TJTran() {}

  static final String CLASS_NAME =
    new TJTran().getClass().getName();


  private static boolean isCropped(java.awt.Rectangle cr) {
    return (cr.x != 0 || cr.y != 0 || cr.width != 0 || cr.height != 0);
  }


  static void usage() {
    int i;
    TJScalingFactor[] scalingFactors = TJ.getScalingFactors();
    int numScalingFactors = scalingFactors.length;

    System.out.println("\nUSAGE: java [Java options] " + CLASS_NAME +
                       " [options] <JPEG input image> <JPEG output image>\n");

    System.out.println("This program reads the DCT coefficients from the lossy JPEG input image,");
    System.out.println("optionally transforms them, and writes them to a lossy JPEG output image.\n");

    System.out.println("OPTIONS (CAN BE ABBREVBIATED)");
    System.out.println("-----------------------------");
    System.out.println("-arithmetic");
    System.out.println("    Use arithmetic entropy coding in the output image instead of Huffman");
    System.out.println("    entropy coding (can be combined with -progressive)");
    System.out.println("-copy all");
    System.out.println("    Copy all extra markers (including comments, JFIF thumbnails, Exif data, and");
    System.out.println("    ICC profile data) from the input image to the output image");
    System.out.println("-copy comments");
    System.out.println("    Do not copy any extra markers, except comment markers, from the input");
    System.out.println("    image to the output image [default]");
    System.out.println("-copy icc");
    System.out.println("    Do not copy any extra markers, except ICC profile data, from the input");
    System.out.println("    image to the output image");
    System.out.println("-copy none");
    System.out.println("    Do not copy any extra markers from the input image to the output image");
    System.out.println("-crop WxH+X+Y");
    System.out.println("    Include only the specified region of the input image.  (W, H, X, and Y are");
    System.out.println("    the width, height, left boundary, and upper boundary of the region, all");
    System.out.println("    specified relative to the transformed image dimensions.)  If necessary, X");
    System.out.println("    and Y will be shifted up and left to the nearest iMCU boundary, and W and H");
    System.out.println("    will be increased accordingly.");
    System.out.println("-flip {horizontal|vertical}, -rotate {90|180|270}, -transpose, -transverse");
    System.out.println("    Perform the specified lossless transform operation (these options are");
    System.out.println("    mutually exclusive)");
    System.out.println("-grayscale");
    System.out.println("    Create a grayscale output image from a full-color input image");
    System.out.println("-icc FILE");
    System.out.println("    Embed the ICC (International Color Consortium) color management profile");
    System.out.println("    from the specified file into the output image");
    System.out.println("-maxmemory N");
    System.out.println("    Memory limit (in megabytes) for intermediate buffers used with progressive");
    System.out.println("    JPEG compression, Huffman table optimization, and lossless transformation");
    System.out.println("    [default = no limit]");
    System.out.println("-maxscans N");
    System.out.println("    Refuse to transform progressive JPEG images that have more than N scans");
    System.out.println("-optimize");
    System.out.println("    Use Huffman table optimization in the output image");
    System.out.println("-perfect");
    System.out.println("    Abort if the requested transform operation is imperfect (non-reversible.)");
    System.out.println("    '-flip horizontal', '-rotate 180', '-rotate 270', and '-transverse' are");
    System.out.println("    imperfect if the image width is not evenly divisible by the iMCU width.");
    System.out.println("    '-flip vertical', '-rotate 90', '-rotate 180', and '-transverse' are");
    System.out.println("    imperfect if the image height is not evenly divisible by the iMCU height.");
    System.out.println("-progressive");
    System.out.println("    Create a progressive output image instead of a single-scan output image");
    System.out.println("    (can be combined with -arithmetic; implies -optimize unless -arithmetic is");
    System.out.println("    also specified)");
    System.out.println("-restart N");
    System.out.println("    Add a restart marker every N MCU rows [default = 0 (no restart markers)].");
    System.out.println("    Append 'B' to specify the restart marker interval in MCUs.");
    System.out.println("-trim");
    System.out.println("    If necessary, trim the partial iMCUs at the right or bottom edge of the");
    System.out.println("    image to make the requested transform perfect\n");

    System.exit(1);
  }


  static boolean matchArg(String arg, String string, int minChars) {
    if (arg.length() > string.length() || arg.length() < minChars)
      return false;

    int cmpChars = Math.max(arg.length(), minChars);
    string = string.substring(0, cmpChars);

    return arg.equalsIgnoreCase(string);
  }


  public static void main(String[] argv) {
    int exitStatus = 0;
    TJTransformer tjt = null;
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {

      int i;
      int arithmetic = 0, maxMemory = -1, maxScans = -1, optimize = -1,
        progressive = 0, restartIntervalBlocks = -1, restartIntervalRows = -1,
        saveMarkers = 1, subsamp;
      TJTransform[] xform = new TJTransform[1];
      xform[0] = new TJTransform();
      String iccFilename = null;
      byte[] srcBuf, iccBuf;
      int width, height, iccSize = 0;
      byte[][] dstBuf;

      for (i = 0; i < argv.length; i++) {
        if (matchArg(argv[i], "-arithmetic", 2))
          arithmetic = 1;
        else if (matchArg(argv[i], "-crop", 3) && i < argv.length - 1) {
          int tempWidth = -1, tempHeight = -1, tempX = -1, tempY = -1;
          Scanner scanner = new Scanner(argv[++i]).useDelimiter("x|X|\\+");

          try {
            tempWidth = scanner.nextInt();
            tempHeight = scanner.nextInt();
            tempX = scanner.nextInt();
            tempY = scanner.nextInt();
          } catch (Exception e) {}

          if (tempWidth < 1 || tempHeight < 1 || tempX < 0 || tempY < 0)
            usage();
          xform[0].options |= TJTransform.OPT_CROP;
          xform[0].width = tempWidth;
          xform[0].height = tempHeight;
          xform[0].x = tempX;
          xform[0].y = tempY;
        } else if (matchArg(argv[i], "-copy", 2) && i < argv.length - 1) {
          i++;
          if (matchArg(argv[i], "all", 1))
            saveMarkers = 2;
          else if (matchArg(argv[i], "icc", 1))
            saveMarkers = 4;
          else if (matchArg(argv[i], "none", 1))
            saveMarkers = 0;
          else if (!matchArg(argv[i], "comments", 1))
            usage();
        } else if (matchArg(argv[i], "-flip", 2) && i < argv.length - 1) {
          i++;
          if (matchArg(argv[i], "horizontal", 1))
            xform[0].op = TJTransform.OP_HFLIP;
          else if (matchArg(argv[i], "vertical", 1))
            xform[0].op = TJTransform.OP_VFLIP;
          else
            usage();
        } else if (matchArg(argv[i], "-grayscale", 2) ||
                   matchArg(argv[i], "-greyscale", 2))
          xform[0].options |= TJTransform.OPT_GRAY;
        else if (matchArg(argv[i], "-icc", 2) && i < argv.length - 1)
          iccFilename = argv[++i];
        else if (matchArg(argv[i], "-maxscans", 5) && i < argv.length - 1) {
          int temp = -1;

          try {
            temp = Integer.parseInt(argv[++i]);
          } catch (NumberFormatException e) {}
          if (temp < 0)
            usage();
          maxScans = temp;
        } else if (matchArg(argv[i], "-maxmemory", 2) && i < argv.length - 1) {
          int temp = -1;

          try {
            temp = Integer.parseInt(argv[++i]);
          } catch (NumberFormatException e) {}
          if (temp < 0)
            usage();
          maxMemory = temp;
        } else if (matchArg(argv[i], "-optimize", 2) ||
                   matchArg(argv[i], "-optimise", 2))
          optimize = 1;
        else if (matchArg(argv[i], "-perfect", 3))
          xform[0].options |= TJTransform.OPT_PERFECT;
        else if (matchArg(argv[i], "-progressive", 2))
          progressive = 1;
        else if (matchArg(argv[i], "-rotate", 3) && i < argv.length - 1) {
          i++;
          if (matchArg(argv[i], "90", 2))
            xform[0].op = TJTransform.OP_ROT90;
          else if (matchArg(argv[i], "180", 3))
            xform[0].op = TJTransform.OP_ROT180;
          else if (matchArg(argv[i], "270", 3))
            xform[0].op = TJTransform.OP_ROT270;
          else
            usage();
        } else if (matchArg(argv[i], "-restart", 2) && i < argv.length - 1) {
          int temp = -1;
          String arg = argv[++i];
          Scanner scanner = new Scanner(arg).useDelimiter("b|B");

          try {
            temp = scanner.nextInt();
          } catch (Exception e) {}

          if (temp < 0 || temp > 65535 || scanner.hasNext())
            usage();
          if (arg.endsWith("B") || arg.endsWith("b"))
            restartIntervalBlocks = temp;
          else
            restartIntervalRows = temp;
        } else if (matchArg(argv[i], "-transverse", 7))
          xform[0].op = TJTransform.OP_TRANSVERSE;
        else if (matchArg(argv[i], "-trim", 4))
          xform[0].options |= TJTransform.OPT_TRIM;
        else if (matchArg(argv[i], "-transpose", 2))
          xform[0].op = TJTransform.OP_TRANSPOSE;
        else break;
      }

      if (i != argv.length - 2)
        usage();

      if (iccFilename != null) {
        if (saveMarkers == 2) saveMarkers = 3;
        else if (saveMarkers == 4) saveMarkers = 0;
      }

      tjt = new TJTransformer();

      if (optimize >= 0)
        tjt.set(TJ.PARAM_OPTIMIZE, optimize);
      if (maxScans >= 0)
        tjt.set(TJ.PARAM_SCANLIMIT, maxScans);
      if (restartIntervalBlocks >= 0)
        tjt.set(TJ.PARAM_RESTARTBLOCKS, restartIntervalBlocks);
      if (restartIntervalRows >= 0)
        tjt.set(TJ.PARAM_RESTARTROWS, restartIntervalRows);
      if (maxMemory >= 0)
        tjt.set(TJ.PARAM_MAXMEMORY, maxMemory);
      tjt.set(TJ.PARAM_SAVEMARKERS, saveMarkers);

      File inFile = new File(argv[i++]);
      fis = new FileInputStream(inFile);
      int srcSize = fis.available();
      if (srcSize < 1)
        throw new Exception("Input file contains no data");
      srcBuf = new byte[srcSize];
      fis.read(srcBuf);
      fis.close();  fis = null;

      tjt.setSourceImage(srcBuf, srcSize);
      subsamp = tjt.get(TJ.PARAM_SUBSAMP);
      if ((xform[0].options & TJTransform.OPT_GRAY) != 0)
        subsamp = TJ.SAMP_GRAY;
      if (xform[0].op == TJTransform.OP_TRANSPOSE ||
          xform[0].op == TJTransform.OP_TRANSVERSE ||
          xform[0].op == TJTransform.OP_ROT90 ||
          xform[0].op == TJTransform.OP_ROT270) {
        width = tjt.get(TJ.PARAM_JPEGHEIGHT);
        height = tjt.get(TJ.PARAM_JPEGWIDTH);
        if (subsamp == TJ.SAMP_422)
          subsamp = TJ.SAMP_440;
        else if (subsamp == TJ.SAMP_440)
          subsamp = TJ.SAMP_422;
        else if (subsamp == TJ.SAMP_411)
          subsamp = TJ.SAMP_441;
        else if (subsamp == TJ.SAMP_441)
          subsamp = TJ.SAMP_411;
      } else {
        width = tjt.get(TJ.PARAM_JPEGWIDTH);
        height = tjt.get(TJ.PARAM_JPEGHEIGHT);
      }

      if (progressive >= 0)
        tjt.set(TJ.PARAM_PROGRESSIVE, progressive);
      if (arithmetic >= 0)
        tjt.set(TJ.PARAM_ARITHMETIC, arithmetic);

      if (isCropped(xform[0])) {
        int xAdjust, yAdjust;

        if (subsamp == TJ.SAMP_UNKNOWN)
          throw new Exception("Could not determine subsampling level of input image");
        xAdjust = xform[0].x % TJ.getMCUWidth(subsamp);
        yAdjust = xform[0].y % TJ.getMCUHeight(subsamp);
        xform[0].x -= xAdjust;
        xform[0].width += xAdjust;
        xform[0].y -= yAdjust;
        xform[0].height += yAdjust;
      }

      if (iccFilename != null) {
        File iccFile = new File(iccFilename);
        fis = new FileInputStream(iccFile);
        iccSize = fis.available();
        if (iccSize < 1)
          throw new Exception("ICC profile contains no data");
        iccBuf = new byte[iccSize];
        fis.read(iccBuf);
        fis.close();  fis = null;
        tjt.setICCProfile(iccBuf);
      }

      TJDecompressor[] tjd = tjt.transform(xform);

      File outFile = new File(argv[i]);
      fos = new FileOutputStream(outFile);
      fos.write(tjd[0].getJPEGBuf(), 0, tjd[0].getJPEGSize());
    } catch (Exception e) {
      e.printStackTrace();
      exitStatus = -1;
    }

    try {
      if (fis != null) fis.close();
      if (tjt != null) tjt.close();
      if (fos != null) fos.close();
    } catch (Exception e) {}
    System.exit(exitStatus);
  }
};
