diff options
author | Simon Steiner <ssteiner@apache.org> | 2019-03-27 16:06:24 +0000 |
---|---|---|
committer | Simon Steiner <ssteiner@apache.org> | 2019-03-27 16:06:24 +0000 |
commit | eff7c6c6d80501dcce34df54936f289e5c11703d (patch) | |
tree | 001662a5d2e890a85d719d50fa91d2fa61c0d6c7 | |
parent | 5a40c4dfd11230414a729347f136c1e670b4e9e9 (diff) | |
download | xmlgraphics-fop-eff7c6c6d80501dcce34df54936f289e5c11703d.tar.gz xmlgraphics-fop-eff7c6c6d80501dcce34df54936f289e5c11703d.zip |
FOP-2847: Support palette-based transparency PNG in PDF
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1856423 13f79535-47bb-0310-9956-ffa450edef68
6 files changed, 359 insertions, 27 deletions
diff --git a/fop-core/src/main/java/org/apache/fop/pdf/AlphaRasterImage.java b/fop-core/src/main/java/org/apache/fop/pdf/AlphaRasterImage.java index 250b5daa2..3c17de148 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/AlphaRasterImage.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/AlphaRasterImage.java @@ -19,6 +19,8 @@ package org.apache.fop.pdf; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; @@ -68,7 +70,40 @@ public class AlphaRasterImage implements PDFImage { * @param image the image (must have an alpha channel) */ public AlphaRasterImage(String k, RenderedImage image) { - this(k, GraphicsUtil.getAlphaRaster(image)); + this(k, getAlphaRaster(image)); + } + + /** + * Extracts the Alpha Raster for the given image. + * Also works for {@link java.awt.image.IndexColorModel}. + */ + private static Raster getAlphaRaster(RenderedImage image) { + Raster alphaRaster = GraphicsUtil.getAlphaRaster(image); + + /* + * {@link GraphicsUtil#getAlphaRaster} calls internally + * {@link java.awt.image.BufferedImage#getAlphRaster} which + * will return <code>null</code> according to Java API + * documentation for {@link java.awt.image.IndexColorModel}. + * + * In that case we create the raster drawing a hidden + * image. That code might be better placed in + * {@link java.awt.image.BufferedImage#getAlphRaster} + * but since this is a different project and a change + * to the interface semantics, it might break consumers. + */ + if (alphaRaster == null) { + BufferedImage bufferedImage = (BufferedImage) image; + int w = bufferedImage.getWidth(); + int h = bufferedImage.getHeight(); + int type = BufferedImage.TYPE_INT_ARGB; + BufferedImage bia = new BufferedImage(w, h, type); + Graphics2D g = bia.createGraphics(); + g.drawImage(bufferedImage, 0, 0, null); + g.dispose(); + alphaRaster = GraphicsUtil.getAlphaRaster(bia); + } + return alphaRaster; } /** {@inheritDoc} */ diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFImage.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFImage.java index 38da60e7b..328931ca6 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/PDFImage.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFImage.java @@ -85,7 +85,14 @@ public interface PDFImage { /** * Check if this image has a transparent color transparency. * - * @return true if it has transparency + * Classes such as {@link PDFImageXObject} rely on this simple + * binary model of transparency (e.g. compare to + * {@link java.awt.Transparency}) in order to render + * color key masking (see PDF Spec 1.7 Chapter 8.9.6.4). + * Therefore only return true if image has fully transparent + * colors. + * + * @return true if it has at least one fully transparent color */ boolean isTransparent(); diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/AbstractImageAdapter.java b/fop-core/src/main/java/org/apache/fop/render/pdf/AbstractImageAdapter.java index c7e516a63..fb362ac75 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/AbstractImageAdapter.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/AbstractImageAdapter.java @@ -304,13 +304,7 @@ public abstract class AbstractImageAdapter implements PDFImage { private static Integer getIndexOfFirstTransparentColorInPalette(IndexColorModel icm) { byte[] alphas = new byte[icm.getMapSize()]; - byte[] reds = new byte[icm.getMapSize()]; - byte[] greens = new byte[icm.getMapSize()]; - byte[] blues = new byte[icm.getMapSize()]; icm.getAlphas(alphas); - icm.getReds(reds); - icm.getGreens(greens); - icm.getBlues(blues); for (int i = 0; i < icm.getMapSize(); i++) { if ((alphas[i] & 0xFF) == 0) { return i; diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java b/fop-core/src/main/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java index c1eaa07c9..c47f4e507 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java @@ -18,12 +18,12 @@ /* $Id$ */ package org.apache.fop.render.pdf; +import java.awt.Color; import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; -import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.io.IOException; import java.io.OutputStream; @@ -31,7 +31,6 @@ import java.io.OutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.xmlgraphics.image.GraphicsUtil; import org.apache.xmlgraphics.image.loader.impl.ImageRendered; import org.apache.xmlgraphics.ps.ImageEncodingHelper; @@ -126,11 +125,8 @@ public class ImageRenderedAdapter extends AbstractImageAdapter { //TODO Implement code to combine image with background color if transparency is not //allowed (need BufferedImage support for that) - Raster raster = GraphicsUtil.getAlphaRaster(ri); - if (raster != null) { - AlphaRasterImage alphaImage = new AlphaRasterImage("Mask:" + getKey(), raster); - this.softMask = doc.addImage(null, alphaImage).makeReference(); - } + AlphaRasterImage alphaImage = new AlphaRasterImage("Mask:" + getKey(), ri); + this.softMask = doc.addImage(null, alphaImage).makeReference(); } } @@ -154,12 +150,6 @@ public class ImageRenderedAdapter extends AbstractImageAdapter { /** {@inheritDoc} */ @Override public boolean isTransparent() { - ColorModel cm = getEffectiveColorModel(); - if (cm instanceof IndexColorModel) { - if (cm.getTransparency() == IndexColorModel.TRANSLUCENT) { - return true; - } - } return (getImage().getTransparentColor() != null); } @@ -169,15 +159,23 @@ public class ImageRenderedAdapter extends AbstractImageAdapter { ColorModel cm = getEffectiveColorModel(); if (cm instanceof IndexColorModel) { IndexColorModel icm = (IndexColorModel)cm; - if (cm.getTransparency() == IndexColorModel.TRANSLUCENT) { + if (cm.getTransparency() == IndexColorModel.TRANSLUCENT + || cm.getTransparency() == IndexColorModel.BITMASK) { int transPixel = icm.getTransparentPixel(); - return new PDFColor( - icm.getRed(transPixel), - icm.getGreen(transPixel), - icm.getBlue(transPixel)); + if (transPixel != -1) { + return new PDFColor( + icm.getRed(transPixel), + icm.getGreen(transPixel), + icm.getBlue(transPixel)); + } } } - return new PDFColor(getImage().getTransparentColor()); + Color transColor = getImage().getTransparentColor(); + if (transColor != null) { + return new PDFColor(transColor); + } else { + return null; + } } /** {@inheritDoc} */ diff --git a/fop-core/src/test/java/org/apache/fop/pdf/PDFImageXObjectTestCase.java b/fop-core/src/test/java/org/apache/fop/pdf/PDFImageXObjectTestCase.java new file mode 100644 index 000000000..0f21b0439 --- /dev/null +++ b/fop-core/src/test/java/org/apache/fop/pdf/PDFImageXObjectTestCase.java @@ -0,0 +1,105 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.impl.ImageRendered; + +import org.apache.fop.render.pdf.ImageRenderedAdapter; +import org.apache.fop.render.pdf.ImageRenderedAdapterTestCase; + +public class PDFImageXObjectTestCase { + + /** + * FOP-2847: tests whether images with index color model returns a valid color key mask</p> + */ + @Test + public void testPDFImageXObjectHasCorrectMaskForSemiTransparentIndexColorModel() { + + RenderedImage ri = ImageRenderedAdapterTestCase.createRenderedImageWithIndexColorModel(false); + + ImageRendered ir = mock(ImageRendered.class); + when(ir.getRenderedImage()).thenReturn(ri); + ImageInfo ii = mock(ImageInfo.class); + when(ir.getInfo()).thenReturn(ii); + ImageRenderedAdapter ira = new ImageRenderedAdapter(ir, "mock"); + + PDFDocument doc = ImageRenderedAdapterTestCase.createPDFDocumentFromRenderedImage(); + ira.setup(doc); + + AbstractPDFStream pdfImageXObject = new PDFImageXObject(0, ira); + pdfImageXObject.populateStreamDict(null); + + /* + * Currently FOP may generate a color key mask (/Mask) as well + * as the more flexible (/SMask) both for a single transparent image. + * That seems and actually is redundant, but it may help limited + * PDF viewers to show at least the fully transparent parts (/Mask), + * while omitting the translucent ones (/SMask). + * + * If it contains a /Mask, then make sure it has only length 2. + * Length 2 actually means it holds the two bounds (min/max) as + * single 8-bit values see section 8.9.6.4 color key masking + * of PDF Spec 1.7. + */ + assertTrue(ri.getColorModel() instanceof IndexColorModel); + Object obj = pdfImageXObject.getDictionary().get("Mask"); + if (obj != null && obj instanceof PDFArray) { + assertEquals(2, ((PDFArray) obj).length()); + } + } + + /** + * FOP-2847: tests whether images with index color model return a valid color key mask</p> + */ + @Test + public void testPDFImageXObjectHasCorrectMaskForFullyTransparentIndexColorModel() { + + RenderedImage ri = ImageRenderedAdapterTestCase.createRenderedImageWithIndexColorModel(true); + + ImageRendered ir = mock(ImageRendered.class); + when(ir.getRenderedImage()).thenReturn(ri); + ImageInfo ii = mock(ImageInfo.class); + when(ir.getInfo()).thenReturn(ii); + ImageRenderedAdapter ira = new ImageRenderedAdapter(ir, "mock"); + + PDFDocument doc = ImageRenderedAdapterTestCase.createPDFDocumentFromRenderedImage(); + ira.setup(doc); + + AbstractPDFStream pdfImageXObject = new PDFImageXObject(0, ira); + pdfImageXObject.populateStreamDict(null); + + assertTrue(ri.getColorModel() instanceof IndexColorModel); + Object obj = pdfImageXObject.getDictionary().get("Mask"); + if (obj != null && obj instanceof PDFArray) { + assertEquals(2, ((PDFArray) obj).length()); + } + } +} diff --git a/fop-core/src/test/java/org/apache/fop/render/pdf/ImageRenderedAdapterTestCase.java b/fop-core/src/test/java/org/apache/fop/render/pdf/ImageRenderedAdapterTestCase.java new file mode 100644 index 000000000..8cb52789d --- /dev/null +++ b/fop-core/src/test/java/org/apache/fop/render/pdf/ImageRenderedAdapterTestCase.java @@ -0,0 +1,193 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.render.pdf; + +import java.awt.Color; +import java.awt.color.ICC_Profile; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.impl.ImageRendered; + +import org.apache.fop.pdf.PDFAMode; +import org.apache.fop.pdf.PDFDictionary; +import org.apache.fop.pdf.PDFDocument; +import org.apache.fop.pdf.PDFFactory; +import org.apache.fop.pdf.PDFICCBasedColorSpace; +import org.apache.fop.pdf.PDFICCStream; +import org.apache.fop.pdf.PDFImage; +import org.apache.fop.pdf.PDFImageXObject; +import org.apache.fop.pdf.PDFProfile; +import org.apache.fop.pdf.PDFResourceContext; +import org.apache.fop.pdf.PDFResources; + +public class ImageRenderedAdapterTestCase { + + /** + * tests whether ARGB images return a soft mask + */ + @Test + public void testSetupWithARGBReturnsSoftMask() { + RenderedImage ri = createRenderedImageWithRGBA(); + + ImageRendered ir = mock(ImageRendered.class); + when(ir.getRenderedImage()).thenReturn(ri); + ImageInfo ii = mock(ImageInfo.class); + when(ir.getInfo()).thenReturn(ii); + ImageRenderedAdapter ira = new ImageRenderedAdapter(ir, "mock"); + + PDFDocument doc = createPDFDocumentFromRenderedImage(); + PDFDictionary dict = new PDFDictionary(); + ira.setup(doc); + ira.populateXObjectDictionary(dict); + assertNotNull(ira.getSoftMaskReference()); + } + + /** + * FOP-2847: tests whether images with index color model return a soft mask</p> + */ + @Test + public void testSetupWithIndexColorModelSemiTransparentReturnsSoftMask() { + RenderedImage ri = createRenderedImageWithIndexColorModel(false); + + ImageRendered ir = mock(ImageRendered.class); + when(ir.getRenderedImage()).thenReturn(ri); + ImageInfo ii = mock(ImageInfo.class); + when(ir.getInfo()).thenReturn(ii); + ImageRenderedAdapter ira = new ImageRenderedAdapter(ir, "mock"); + + PDFDocument doc = createPDFDocumentFromRenderedImage(); + PDFDictionary dict = new PDFDictionary(); + ira.setup(doc); + ira.populateXObjectDictionary(dict); + assertNotNull(ira.getSoftMaskReference()); + } + + /** + * FOP-2847: tests whether images with index color model return a soft mask</p> + */ + @Test + public void testSetupWithIndexColorModelFullyTransparentReturnsSoftMask() { + RenderedImage ri = createRenderedImageWithIndexColorModel(true); + + ImageRendered ir = mock(ImageRendered.class); + when(ir.getRenderedImage()).thenReturn(ri); + ImageInfo ii = mock(ImageInfo.class); + when(ir.getInfo()).thenReturn(ii); + ImageRenderedAdapter ira = new ImageRenderedAdapter(ir, "mock"); + + PDFDocument doc = createPDFDocumentFromRenderedImage(); + PDFDictionary dict = new PDFDictionary(); + ira.setup(doc); + ira.populateXObjectDictionary(dict); + assertNotNull(ira.getSoftMaskReference()); + } + + /** + * Creates a semi transparent 4x4 image in index color space. + * + * @param fullyTransparent true if image is supposed to have a fully + * transparent color + * @return RenderedImage + */ + public static RenderedImage createRenderedImageWithIndexColorModel(boolean fullyTransparent) { + /* + * Define an index color model with just four colors. For reasons of + * simplicity colors will be gray. + */ + IndexColorModel cm; + if (fullyTransparent) { + byte[] i = {(byte)0x00, (byte)0x80, (byte)0xB0, (byte)0xF0}; + cm = new IndexColorModel(8, 4, i, i, i, i); + } else { + byte[] i = {(byte)0x10, (byte)0x80, (byte)0xB0, (byte)0xF0}; + cm = new IndexColorModel(8, 4, i, i, i, i); + } + + // create a 4x4 image with just one uniform color + BufferedImage ri = new BufferedImage(4, 4, BufferedImage.TYPE_BYTE_INDEXED, cm); + for (int x = 0; x < 2; x++) { + for (int y = 0; y < 2; y++) { + Color c = new Color(128, 128, 128, 128); + ri.setRGB(x, y, c.getRGB()); + } + } + return ri; + } + + /** + * creates a semi transparent 4x4 image in ABGR color space + * + * @return RenderedImage + */ + static RenderedImage createRenderedImageWithRGBA() { + // create a 4x4 image + BufferedImage ri = new BufferedImage(4, 4, BufferedImage.TYPE_4BYTE_ABGR); + for (int x = 0; x < 2; x++) { + for (int y = 0; y < 2; y++) { + Color c = new Color(128, 128, 128, 128); + ri.setRGB(x, y, c.getRGB()); + } + } + return ri; + } + + /** + * Create a mocked PDF document from RenderedImage. + * + * @return + */ + public static PDFDocument createPDFDocumentFromRenderedImage() { + // mock PDFDocument + PDFDocument doc = mock(PDFDocument.class); + PDFResources resources = mock(PDFResources.class); + when(doc.getResources()).thenReturn(resources); + + PDFProfile profile = mock(PDFProfile.class); + when(profile.getPDFAMode()).thenReturn(PDFAMode.PDFA_2A); + + PDFImageXObject pio = new PDFImageXObject(0, null); + pio.setObjectNumber(0); + when(doc.getProfile()).thenReturn(profile); + when(doc.addImage(any(PDFResourceContext.class), any(PDFImage.class))).thenReturn(pio); + + // ICC Color info + PDFFactory factory = mock(PDFFactory.class); + PDFICCStream iccStream = mock(PDFICCStream.class); + ICC_Profile iccProfile = mock(ICC_Profile.class); + when(iccProfile.getNumComponents()).thenReturn(4); + when(iccStream.getICCProfile()).thenReturn(iccProfile); + when(factory.makePDFICCStream()).thenReturn(iccStream); + PDFICCBasedColorSpace iccbcs = new PDFICCBasedColorSpace(null, iccStream); + when(factory.makeICCBasedColorSpace(null, null, iccStream)).thenReturn(iccbcs); + when(doc.getFactory()).thenReturn(factory); + + return doc; + } +} |