/* | |
* 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.PDFICCStream; | |
import org.apache.fop.pdf.PDFReference; | |
public class ImageRawPNGAdapter extends AbstractImageAdapter { | |
/** logging instance */ | |
private static Log log = LogFactory.getLog(ImageRawPNGAdapter.class); | |
private PDFICCStream pdfICCStream; | |
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 PDFICCStream getICCStream() { | |
return pdfICCStream; | |
} | |
/** {@inheritDoc} */ | |
public String getFilterHint() { | |
return PDFFilterList.PRECOMPRESSED_FILTER; | |
} | |
public void populateXObjectDictionary(PDFDictionary dict) { | |
ColorModel cm = ((ImageRawPNG) image).getColorModel(); | |
if (cm instanceof IndexColorModel) { | |
IndexColorModel icm = (IndexColorModel) cm; | |
super.populateXObjectDictionaryForIndexColorModel(dict, icm); | |
} | |
} | |
} |