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 <[email protected]> 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()); + } + + } +}
