You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ki...@apache.org on 2022/03/31 19:53:08 UTC

[commons-imaging] 01/03: [IMAGING-330] Add PNG predictor to reduce output size

This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit 535f51b25e4eba5457e571a0c2248432148ce8e8
Author: gwlucastrig <co...@gmail.com>
AuthorDate: Mon Mar 28 23:34:17 2022 -0400

    [IMAGING-330] Add PNG predictor to reduce output size
    
    Imaging 330: Add PNG predictor to reduce output size
---
 .../imaging/formats/png/PngImagingParameters.java  |  25 ++++
 .../commons/imaging/formats/png/PngWriter.java     |  47 +++++++-
 .../imaging/formats/png/PngWritePredictorTest.java | 134 +++++++++++++++++++++
 3 files changed, 205 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngImagingParameters.java b/src/main/java/org/apache/commons/imaging/formats/png/PngImagingParameters.java
index 7c8ca1b..2127e8d 100644
--- a/src/main/java/org/apache/commons/imaging/formats/png/PngImagingParameters.java
+++ b/src/main/java/org/apache/commons/imaging/formats/png/PngImagingParameters.java
@@ -37,6 +37,8 @@ public class PngImagingParameters extends XmpImagingParameters {
 
     private boolean forceTrueColor = false;
 
+    private boolean predictorEnabled = false;
+
     /**
      * Used in write operations to indicate the Physical Scale - sCAL.
      *
@@ -92,4 +94,27 @@ public class PngImagingParameters extends XmpImagingParameters {
     public void setTextChunks(List<? extends PngText> textChunks) {
         this.textChunks = Collections.unmodifiableList(textChunks);
     }
+
+    /**
+     * Indicates that the PNG write operation should enable
+     * the predictor.
+     * @return true if the predictor is enabled; otherwise, false.
+     */
+    public boolean isPredictorEnabled(){
+        return predictorEnabled;
+    }
+
+    /**
+     * Sets the enabled status of the predictor. When performing
+     * data compression on an image, a PNG predictor often results in a
+     * reduced file size. Predictors are particularly effective on
+     * photographic images, but may also work on graphics.
+     * The specification of a predictor may result in an increased
+     * processing time when writing an image, but will not affect the
+     * time required to read an image.
+     * @param predictorEnabled true if a predictor is enabled; otherwise, false.
+     */
+    public void setPredictorEnabled(boolean predictorEnabled){
+        this.predictorEnabled = predictorEnabled;
+    }
 }
diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java b/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java
index f51387c..93b4795 100644
--- a/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java
+++ b/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java
@@ -463,8 +463,15 @@ class PngWriter {
 
             // IDAT Yes Multiple IDAT chunks shall be consecutive
 
+            // 28 March 2022.  At this time, we only apply the predictor
+            // for non-grayscale, true-color images.  This choice is made
+            // out of caution and is not necessarily required by the PNG
+            // spec.  We may broaden the use of predictors in future versions.
+            boolean usePredictor = params.isPredictorEnabled() &&
+                !isGrayscale && palette==null;
+
             byte[] uncompressed;
-            {
+            if(!usePredictor) {
                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
 
                 final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA
@@ -515,8 +522,46 @@ class PngWriter {
                     }
                 }
                 uncompressed = baos.toByteArray();
+            } else {
+                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+                final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA
+                        || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
+
+                final int[] row = new int[width];
+                for (int y = 0; y < height; y++) {
+                    // Debug.debug("y", y + "/" + height);
+                    src.getRGB(0, y, width, 1, row, 0, width);
+
+                    int priorA = 0;
+                    int priorR = 0;
+                    int priorG = 0;
+                    int priorB = 0;
+                    baos.write(FilterType.SUB.ordinal());
+                    for (int x = 0; x < width; x++) {
+                      final int argb  = row[x];
+                      final int alpha = 0xff & (argb >> 24);
+                      final int red   = 0xff & (argb >> 16);
+                      final int green = 0xff & (argb >> 8);
+                      final int blue  = 0xff & argb;
+
+                      baos.write(red   - priorR);
+                      baos.write(green - priorG);
+                      baos.write(blue  - priorB);
+                      priorR = red;
+                      priorG = green;
+                      priorB = blue;
+
+                      if (useAlpha) {
+                          baos.write(alpha - priorA);
+                          priorA = alpha;
+                      }
+                    }
+                }
+                uncompressed = baos.toByteArray();
             }
 
+
             // Debug.debug("uncompressed", uncompressed.length);
 
             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/src/test/java/org/apache/commons/imaging/formats/png/PngWritePredictorTest.java b/src/test/java/org/apache/commons/imaging/formats/png/PngWritePredictorTest.java
new file mode 100644
index 0000000..26bafd5
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/png/PngWritePredictorTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+
+package org.apache.commons.imaging.formats.png;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+import org.apache.commons.imaging.ImageWriteException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Provides a test for the PngWriter using predictors
+ */
+public class PngWritePredictorTest {
+
+  public PngWritePredictorTest() {
+  }
+
+  @BeforeAll
+  public static void setUpClass() {
+  }
+
+  @BeforeEach
+  public void setUp() {
+  }
+
+  /**
+   * Populate an integer pixel array for a 256-by-256 image
+   * with varied colors across the image area and a white and
+   * black line down the main diagonal.
+   * @return a valid array of integers.
+   */
+  private int[] populateARGB() {
+    //populate array with a blend of color components
+    int[] argb = new int[256 * 256];
+    for (int i = 0; i < 256; i++) {
+      for (int j = 0; j < 256; j++) {
+        int red = i;
+        int green = (255 - i);
+        int blue = j;
+        argb[i * 256 + j] = ((((0xff00 | red) << 8) | green) << 8) | blue;
+      }
+    }
+
+    // also draw a black and white strip down main diagonal
+    for (int i = 0; i < 256; i++) {
+      argb[i * 256 + i] = 0xff000000;
+      if (i < 255) {
+        argb[i * 256 + i + 1] = 0xffffffff;
+      }
+    }
+    return argb;
+  }
+
+  @Test
+  void testWriteWithPredictor() {
+    int[] argb = populateARGB();
+
+    // Test the RGB (no alpha) case ---------------------
+    BufferedImage bImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
+    bImage.setRGB(0, 0, 256, 256, argb, 0, 256);
+
+    File tempFile = null;
+
+    try {
+      tempFile = File.createTempFile("PngWritePredictorRGB", ".png");
+    } catch (IOException ioex) {
+      fail("Failed to create temporary file, " + ioex.getMessage());
+    }
+    PngImagingParameters params = new PngImagingParameters();
+    params.setPredictorEnabled(true);
+    PngImageParser parser = new PngImageParser();
+    try ( FileOutputStream fos = new FileOutputStream(tempFile);  BufferedOutputStream bos = new BufferedOutputStream(fos)) {
+      parser.writeImage(bImage, bos, params);
+      bos.flush();
+    } catch (IOException | ImageWriteException ex) {
+      fail("Failed writing RGB with exception " + ex.getMessage());
+    }
+
+    try {
+      int[] brgb = new int[256 * 256];
+      bImage = ImageIO.read(tempFile);
+      bImage.getRGB(0, 0, 256, 256, brgb, 0, 256);
+      assertArrayEquals(argb, brgb, "Round trip for RGB failed");
+    } catch (IOException ex) {
+      fail("Failed reading RGB with exception " + ex.getMessage());
+    }
+
+     // Test the ARGB (some semi-transparent alpha) case ---------------------
+    for (int i = 0; i < 256; i++) {
+      argb[i * 256 + i] &= 0x88ffffff;
+    }
+    bImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
+    bImage.setRGB(0, 0, 256, 256, argb, 0, 256);
+    try ( FileOutputStream fos = new FileOutputStream(tempFile);  BufferedOutputStream bos = new BufferedOutputStream(fos)) {
+      parser.writeImage(bImage, bos, params);
+      bos.flush();
+    } catch (IOException | ImageWriteException ex) {
+      fail("Failed writing ARGB with exception " + ex.getMessage());
+    }
+    try {
+      int[] brgb = new int[256 * 256];
+      bImage = ImageIO.read(tempFile);
+      bImage.getRGB(0, 0, 256, 256, brgb, 0, 256);
+      assertArrayEquals(argb, brgb, "Round trip for ARGB failed");
+    } catch (IOException ex) {
+      fail("Failed reading ARGB with exception " + ex.getMessage());
+    }
+
+  }
+}