/* * 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$ */ // Original author: Matthias Reichenbacher package org.apache.fop.render.pdf; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; import org.apache.xmlgraphics.image.loader.impl.ImageRawStream; import org.apache.fop.pdf.BitmapImage; import org.apache.fop.pdf.FlateFilter; import org.apache.fop.pdf.PDFColor; import org.apache.fop.pdf.PDFDeviceColorSpace; import org.apache.fop.pdf.PDFDictionary; import org.apache.fop.pdf.PDFDocument; import org.apache.fop.pdf.PDFFilter; import org.apache.fop.pdf.PDFFilterException; import org.apache.fop.pdf.PDFFilterList; import org.apache.fop.pdf.PDFName; import org.apache.fop.pdf.PDFReference; public class ImageRawPNGAdapter extends AbstractImageAdapter { /** logging instance */ private static Log log = LogFactory.getLog(ImageRawPNGAdapter.class); private static final PDFName RI_PERCEPTUAL = new PDFName("Perceptual"); private static final PDFName RI_RELATIVE_COLORIMETRIC = new PDFName("RelativeColorimetric"); private static final PDFName RI_SATURATION = new PDFName("Saturation"); private static final PDFName RI_ABSOLUTE_COLORIMETRIC = new PDFName("AbsoluteColorimetric"); private PDFFilter pdfFilter; private String maskRef; private PDFReference softMask; private int numberOfInterleavedComponents; /** * Creates a new PDFImage from an Image instance. * @param image the image * @param key XObject key */ public ImageRawPNGAdapter(ImageRawPNG image, String key) { super(image, key); } /** {@inheritDoc} */ public void setup(PDFDocument doc) { super.setup(doc); ColorModel cm = ((ImageRawPNG) this.image).getColorModel(); if (cm instanceof IndexColorModel) { numberOfInterleavedComponents = 1; } else { // this can be 1 (gray), 2 (gray + alpha), 3 (rgb) or 4 (rgb + alpha) // numberOfInterleavedComponents = (cm.hasAlpha() ? 1 : 0) + cm.getNumColorComponents(); numberOfInterleavedComponents = cm.getNumComponents(); } // set up image compression for non-alpha channel FlateFilter flate; try { flate = new FlateFilter(); flate.setApplied(true); flate.setPredictor(FlateFilter.PREDICTION_PNG_OPT); if (numberOfInterleavedComponents < 3) { // means palette (1) or gray (1) or gray + alpha (2) flate.setColors(1); } else { // means rgb (3) or rgb + alpha (4) flate.setColors(3); } flate.setColumns(image.getSize().getWidthPx()); flate.setBitsPerComponent(this.getBitsPerComponent()); } catch (PDFFilterException e) { throw new RuntimeException("FlateFilter configuration error", e); } this.pdfFilter = flate; this.disallowMultipleFilters(); // Handle transparency channel if applicable; note that for palette images the transparency is // not TRANSLUCENT if (cm.hasAlpha() && cm.getTransparency() == ColorModel.TRANSLUCENT) { doc.getProfile().verifyTransparencyAllowed(image.getInfo().getOriginalURI()); // TODO: Implement code to combine image with background color if transparency is not allowed // here we need to inflate the PNG pixel data, which includes alpha, separate the alpha channel // and then deflate it back again ByteArrayOutputStream baos = new ByteArrayOutputStream(); DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater()); InputStream in = ((ImageRawStream) image).createInputStream(); try { InflaterInputStream infStream = new InflaterInputStream(in, new Inflater()); DataInputStream dataStream = new DataInputStream(infStream); // offset is the byte offset of the alpha component int offset = numberOfInterleavedComponents - 1; // 1 for GA, 3 for RGBA int numColumns = image.getSize().getWidthPx(); int bytesPerRow = numberOfInterleavedComponents * numColumns; int filter; // read line by line; the first byte holds the filter while ((filter = dataStream.read()) != -1) { byte[] bytes = new byte[bytesPerRow]; dataStream.readFully(bytes, 0, bytesPerRow); dos.write((byte) filter); for (int j = 0; j < numColumns; j++) { dos.write(bytes, offset, 1); offset += numberOfInterleavedComponents; } offset = numberOfInterleavedComponents - 1; } dos.close(); } catch (IOException e) { throw new RuntimeException("Error processing transparency channel:", e); } finally { IOUtils.closeQuietly(in); } // set up alpha channel compression FlateFilter transFlate; try { transFlate = new FlateFilter(); transFlate.setApplied(true); transFlate.setPredictor(FlateFilter.PREDICTION_PNG_OPT); transFlate.setColors(1); transFlate.setColumns(image.getSize().getWidthPx()); transFlate.setBitsPerComponent(this.getBitsPerComponent()); } catch (PDFFilterException e) { throw new RuntimeException("FlateFilter configuration error", e); } BitmapImage alphaMask = new BitmapImage("Mask:" + this.getKey(), image.getSize().getWidthPx(), image.getSize().getHeightPx(), baos.toByteArray(), null); alphaMask.setPDFFilter(transFlate); alphaMask.disallowMultipleFilters(); alphaMask.setColorSpace(new PDFDeviceColorSpace(PDFDeviceColorSpace.DEVICE_GRAY)); softMask = doc.addImage(null, alphaMask).makeReference(); } } /** {@inheritDoc} */ public PDFDeviceColorSpace getColorSpace() { // DeviceGray, DeviceRGB, or DeviceCMYK return toPDFColorSpace(image.getColorSpace()); } /** {@inheritDoc} */ public int getBitsPerComponent() { return ((ImageRawPNG) this.image).getBitDepth(); } /** {@inheritDoc} */ public boolean isTransparent() { return ((ImageRawPNG) this.image).isTransparent(); } /** {@inheritDoc} */ public PDFColor getTransparentColor() { return new PDFColor(((ImageRawPNG) this.image).getTransparentColor()); } /** {@inheritDoc} */ public String getMask() { return maskRef; } /** {@inheritDoc} */ public String getSoftMask() { return softMask.toString(); } /** {@inheritDoc} */ public PDFReference getSoftMaskReference() { return softMask; } /** {@inheritDoc} */ public PDFFilter getPDFFilter() { return pdfFilter; } /** {@inheritDoc} */ public void outputContents(OutputStream out) throws IOException { InputStream in = ((ImageRawStream) image).createInputStream(); try { if (numberOfInterleavedComponents == 1 || numberOfInterleavedComponents == 3) { // means we have Gray, RGB, or Palette IOUtils.copy(in, out); } else { // means we have Gray + alpha or RGB + alpha // TODO: since we have alpha here do this when the alpha channel is extracted int numBytes = numberOfInterleavedComponents - 1; // 1 for Gray, 3 for RGB int numColumns = image.getSize().getWidthPx(); InflaterInputStream infStream = new InflaterInputStream(in, new Inflater()); DataInputStream dataStream = new DataInputStream(infStream); int offset = 0; int bytesPerRow = numberOfInterleavedComponents * numColumns; int filter; // here we need to inflate the PNG pixel data, which includes alpha, separate the alpha // channel and then deflate the RGB channels back again DeflaterOutputStream dos = new DeflaterOutputStream(out, new Deflater()); while ((filter = dataStream.read()) != -1) { byte[] bytes = new byte[bytesPerRow]; dataStream.readFully(bytes, 0, bytesPerRow); dos.write((byte) filter); for (int j = 0; j < numColumns; j++) { dos.write(bytes, offset, numBytes); offset += numberOfInterleavedComponents; } offset = 0; } dos.close(); } } finally { IOUtils.closeQuietly(in); } } /** {@inheritDoc} */ public String getFilterHint() { return PDFFilterList.PRECOMPRESSED_FILTER; } public void populateXObjectDictionary(PDFDictionary dict) { int renderingIntent = ((ImageRawPNG) image).getRenderingIntent(); if (renderingIntent != -1) { switch (renderingIntent) { case 0: dict.put("Intent", RI_PERCEPTUAL); break; case 1: dict.put("Intent", RI_RELATIVE_COLORIMETRIC); break; case 2: dict.put("Intent", RI_SATURATION); break; case 3: dict.put("Intent", RI_ABSOLUTE_COLORIMETRIC); break; default: // ignore } } ColorModel cm = ((ImageRawPNG) image).getColorModel(); if (cm instanceof IndexColorModel) { IndexColorModel icm = (IndexColorModel) cm; super.populateXObjectDictionaryForIndexColorModel(dict, icm); } } protected boolean issRGB() { if (((ImageRawPNG) image).getRenderingIntent() != -1) { return true; } return false; } }