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