Java tutorial
/* * 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: PSImageUtils.java 884159 2009-11-25 15:53:41Z jeremias $ */ package org.apache.xmlgraphics.ps; import java.awt.Dimension; import java.awt.color.ColorSpace; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.io.IOUtils; import org.apache.xmlgraphics.util.io.ASCII85OutputStream; import org.apache.xmlgraphics.util.io.Finalizable; import org.apache.xmlgraphics.util.io.FlateEncodeOutputStream; import org.apache.xmlgraphics.util.io.RunLengthEncodeOutputStream; /** * Utility code for rendering images in PostScript. */ public class PSImageUtils { /** * Writes a bitmap image to the PostScript stream. * @param img the bitmap image as a byte array * @param imgDim the dimensions of the image * @param imgDescription the name of the image * @param targetRect the target rectangle to place the image in * @param isJPEG true if "img" contains a DCT-encoded images, false if "img" contains the * decoded bitmap * @param colorSpace the color space of the image * @param gen the PostScript generator * @throws IOException In case of an I/O exception * @deprecated Please use the variant with the more versatile ImageEncoder as parameter */ public static void writeImage(final byte[] img, Dimension imgDim, String imgDescription, Rectangle2D targetRect, final boolean isJPEG, ColorSpace colorSpace, PSGenerator gen) throws IOException { ImageEncoder encoder = new ImageEncoder() { public void writeTo(OutputStream out) throws IOException { out.write(img); } public String getImplicitFilter() { if (isJPEG) { return "<< >> /DCTDecode"; } else { return null; } } }; writeImage(encoder, imgDim, imgDescription, targetRect, colorSpace, 8, false, gen); } /** * Writes a bitmap image to the PostScript stream. * @param encoder the image encoder * @param imgDim the dimensions of the image * @param imgDescription the name of the image * @param targetRect the target rectangle to place the image in * @param colorSpace the color space of the image * @param bitsPerComponent the number of bits per component * @param invertImage true if the image shall be inverted * @param gen the PostScript generator * @throws IOException In case of an I/O exception */ public static void writeImage(ImageEncoder encoder, Dimension imgDim, String imgDescription, Rectangle2D targetRect, ColorSpace colorSpace, int bitsPerComponent, boolean invertImage, PSGenerator gen) throws IOException { gen.saveGraphicsState(); translateAndScale(gen, null, targetRect); gen.commentln("%AXGBeginBitmap: " + imgDescription); gen.writeln("{{"); // Template: (RawData is used for the EOF signal only) // gen.write("/RawData currentfile <first filter> filter def"); // gen.write("/Data RawData <second filter> <third filter> [...] def"); String implicitFilter = encoder.getImplicitFilter(); if (implicitFilter != null) { gen.writeln("/RawData currentfile /ASCII85Decode filter def"); gen.writeln("/Data RawData " + implicitFilter + " filter def"); } else { if (gen.getPSLevel() >= 3) { gen.writeln("/RawData currentfile /ASCII85Decode filter def"); gen.writeln("/Data RawData /FlateDecode filter def"); } else { gen.writeln("/RawData currentfile /ASCII85Decode filter def"); gen.writeln("/Data RawData /RunLengthDecode filter def"); } } PSDictionary imageDict = new PSDictionary(); imageDict.put("/DataSource", "Data"); imageDict.put("/BitsPerComponent", Integer.toString(bitsPerComponent)); writeImageCommand(imageDict, imgDim, colorSpace, invertImage, gen); /* the following two lines could be enabled if something still goes wrong * gen.write("Data closefile"); * gen.write("RawData flushfile"); */ gen.writeln("} stopped {handleerror} if"); gen.writeln(" RawData flushfile"); gen.writeln("} exec"); compressAndWriteBitmap(encoder, gen); gen.newLine(); gen.commentln("%AXGEndBitmap"); gen.restoreGraphicsState(); } /** * Writes a bitmap image to the PostScript stream. * @param img the bitmap image as a byte array * @param targetRect the target rectangle to place the image in * @param gen the PostScript generator * @throws IOException In case of an I/O exception */ private static void writeImage(RenderedImage img, Rectangle2D targetRect, PSGenerator gen) throws IOException { ImageEncoder encoder = ImageEncodingHelper.createRenderedImageEncoder(img); String imgDescription = img.getClass().getName(); gen.saveGraphicsState(); translateAndScale(gen, null, targetRect); gen.commentln("%AXGBeginBitmap: " + imgDescription); gen.writeln("{{"); // Template: (RawData is used for the EOF signal only) // gen.write("/RawData currentfile <first filter> filter def"); // gen.write("/Data RawData <second filter> <third filter> [...] def"); String implicitFilter = encoder.getImplicitFilter(); if (implicitFilter != null) { gen.writeln("/RawData currentfile /ASCII85Decode filter def"); gen.writeln("/Data RawData " + implicitFilter + " filter def"); } else { if (gen.getPSLevel() >= 3) { gen.writeln("/RawData currentfile /ASCII85Decode filter def"); gen.writeln("/Data RawData /FlateDecode filter def"); } else { gen.writeln("/RawData currentfile /ASCII85Decode filter def"); gen.writeln("/Data RawData /RunLengthDecode filter def"); } } PSDictionary imageDict = new PSDictionary(); imageDict.put("/DataSource", "Data"); writeImageCommand(img, imageDict, gen); /* the following two lines could be enabled if something still goes wrong * gen.write("Data closefile"); * gen.write("RawData flushfile"); */ gen.writeln("} stopped {handleerror} if"); gen.writeln(" RawData flushfile"); gen.writeln("} exec"); compressAndWriteBitmap(encoder, gen); gen.writeln(""); gen.commentln("%AXGEndBitmap"); gen.restoreGraphicsState(); } private static ColorModel populateImageDictionary(ImageEncodingHelper helper, PSDictionary imageDict) { RenderedImage img = helper.getImage(); String w = Integer.toString(img.getWidth()); String h = Integer.toString(img.getHeight()); imageDict.put("/ImageType", "1"); imageDict.put("/Width", w); imageDict.put("/Height", h); ColorModel cm = helper.getEncodedColorModel(); boolean invertColors = false; String decodeArray = getDecodeArray(cm.getNumComponents(), invertColors); int bitsPerComp = cm.getComponentSize(0); // Setup scanning for left-to-right and top-to-bottom imageDict.put("/ImageMatrix", "[" + w + " 0 0 " + h + " 0 0]"); if ((cm instanceof IndexColorModel)) { IndexColorModel im = (IndexColorModel) cm; int c = im.getMapSize(); int hival = c - 1; if (hival > 4095) { throw new UnsupportedOperationException("hival must not go beyond 4095"); } bitsPerComp = im.getPixelSize(); int ceiling = ((int) Math.pow(2, bitsPerComp)) - 1; decodeArray = "[0 " + ceiling + "]"; } imageDict.put("/BitsPerComponent", Integer.toString(bitsPerComp)); imageDict.put("/Decode", decodeArray); return cm; } private static String getDecodeArray(int numComponents, boolean invertColors) { String decodeArray; StringBuffer sb = new StringBuffer("["); for (int i = 0; i < numComponents; i++) { if (i > 0) { sb.append(" "); } if (invertColors) { sb.append("1 0"); } else { sb.append("0 1"); } } sb.append("]"); decodeArray = sb.toString(); return decodeArray; } private static void prepareColorspace(PSGenerator gen, ColorSpace colorSpace) throws IOException { gen.writeln(getColorSpaceName(colorSpace) + " setcolorspace"); } private static void prepareColorSpace(PSGenerator gen, ColorModel cm) throws IOException { //Prepare color space if ((cm instanceof IndexColorModel)) { ColorSpace cs = cm.getColorSpace(); IndexColorModel im = (IndexColorModel) cm; gen.write("[/Indexed " + getColorSpaceName(cs)); int c = im.getMapSize(); int hival = c - 1; if (hival > 4095) { throw new UnsupportedOperationException("hival must not go beyond 4095"); } gen.writeln(" " + Integer.toString(hival)); gen.write(" <"); int[] palette = new int[c]; im.getRGBs(palette); for (int i = 0; i < c; i++) { if (i > 0) { if ((i % 8) == 0) { gen.newLine(); gen.write(" "); } else { gen.write(" "); } } gen.write(rgb2Hex(palette[i])); } gen.writeln(">"); gen.writeln("] setcolorspace"); } else { gen.writeln(getColorSpaceName(cm.getColorSpace()) + " setcolorspace"); } } static void writeImageCommand(RenderedImage img, PSDictionary imageDict, PSGenerator gen) throws IOException { ImageEncodingHelper helper = new ImageEncodingHelper(img, true); ColorModel cm = helper.getEncodedColorModel(); populateImageDictionary(helper, imageDict); writeImageCommand(imageDict, cm, gen); } static void writeImageCommand(PSDictionary imageDict, ColorModel cm, PSGenerator gen) throws IOException { prepareColorSpace(gen, cm); gen.write(imageDict.toString()); gen.writeln(" image"); } static void writeImageCommand(PSDictionary imageDict, Dimension imgDim, ColorSpace colorSpace, boolean invertImage, PSGenerator gen) throws IOException { imageDict.put("/ImageType", "1"); imageDict.put("/Width", Integer.toString(imgDim.width)); imageDict.put("/Height", Integer.toString(imgDim.height)); String decodeArray = getDecodeArray(colorSpace.getNumComponents(), invertImage); imageDict.put("/Decode", decodeArray); // Setup scanning for left-to-right and top-to-bottom imageDict.put("/ImageMatrix", "[" + imgDim.width + " 0 0 " + imgDim.height + " 0 0]"); prepareColorspace(gen, colorSpace); gen.write(imageDict.toString()); gen.writeln(" image"); } private static final char[] HEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private static String rgb2Hex(int rgb) { StringBuffer sb = new StringBuffer(); for (int i = 5; i >= 0; i--) { int shift = i * 4; int n = (rgb & (15 << shift)) >> shift; sb.append(HEX[n % 16]); } return sb.toString(); } /** * Renders a bitmap image to PostScript. * @param img image to render * @param x x position * @param y y position * @param w width * @param h height * @param gen PS generator * @throws IOException In case of an I/O problem while rendering the image */ public static void renderBitmapImage(RenderedImage img, float x, float y, float w, float h, PSGenerator gen) throws IOException { Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h); writeImage(img, targetRect, gen); } /** * Writes a bitmap image as a PostScript form enclosed by DSC resource wrappers to the * PostScript file. * @param img the raw bitmap data * @param imgDim the dimensions of the image * @param formName the name of the PostScript form to use * @param imageDescription a description of the image added as a DSC Title comment * @param isJPEG true if "img" contains a DCT-encoded images, false if "img" contains the * decoded bitmap * @param colorSpace the color space of the image * @param gen the PostScript generator * @return a PSResource representing the form for resource tracking * @throws IOException In case of an I/O exception * @deprecated Please use {@link FormGenerator} */ public static PSResource writeReusableImage(final byte[] img, Dimension imgDim, String formName, String imageDescription, final boolean isJPEG, ColorSpace colorSpace, PSGenerator gen) throws IOException { ImageEncoder encoder = new ImageEncoder() { public void writeTo(OutputStream out) throws IOException { out.write(img); } public String getImplicitFilter() { if (isJPEG) { return "<< >> /DCTDecode"; } else { return null; } } }; return writeReusableImage(encoder, imgDim, formName, imageDescription, colorSpace, false, gen); } /** * Writes a bitmap image as a PostScript form enclosed by DSC resource wrappers to the * PostScript file. * @param encoder the ImageEncoder that will provide the raw bitmap data * @param imgDim the dimensions of the image * @param formName the name of the PostScript form to use * @param imageDescription a description of the image added as a DSC Title comment * @param colorSpace the color space of the image * @param invertImage true if the image shall be inverted * @param gen the PostScript generator * @return a PSResource representing the form for resource tracking * @throws IOException In case of an I/O exception * @deprecated Please use {@link FormGenerator} */ protected static PSResource writeReusableImage(ImageEncoder encoder, Dimension imgDim, String formName, String imageDescription, ColorSpace colorSpace, boolean invertImage, PSGenerator gen) throws IOException { if (gen.getPSLevel() < 2) { throw new UnsupportedOperationException("Reusable images requires at least Level 2 PostScript"); } String dataName = formName + ":Data"; gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, formName); if (imageDescription != null) { gen.writeDSCComment(DSCConstants.TITLE, imageDescription); } String additionalFilters; String implicitFilter = encoder.getImplicitFilter(); if (implicitFilter != null) { additionalFilters = "/ASCII85Decode filter " + implicitFilter + " filter"; } else { if (gen.getPSLevel() >= 3) { additionalFilters = "/ASCII85Decode filter /FlateDecode filter"; } else { additionalFilters = "/ASCII85Decode filter /RunLengthDecode filter"; } } gen.writeln("/" + formName); gen.writeln("<< /FormType 1"); gen.writeln(" /BBox [0 0 " + imgDim.width + " " + imgDim.height + "]"); gen.writeln(" /Matrix [1 0 0 1 0 0]"); gen.writeln(" /PaintProc {"); gen.writeln(" pop"); gen.writeln(" gsave"); if (gen.getPSLevel() == 2) { gen.writeln(" userdict /i 0 put"); //rewind image data } else { gen.writeln(" " + dataName + " 0 setfileposition"); //rewind image data } String dataSource; if (gen.getPSLevel() == 2) { dataSource = "{ " + dataName + " i get /i i 1 add store } bind"; } else { dataSource = dataName; } PSDictionary imageDict = new PSDictionary(); imageDict.put("/DataSource", dataSource); imageDict.put("/BitsPerComponent", Integer.toString(8)); writeImageCommand(imageDict, imgDim, colorSpace, invertImage, gen); gen.writeln(" grestore"); gen.writeln(" } bind"); gen.writeln(">> def"); gen.writeln("/" + dataName + " currentfile"); gen.writeln(additionalFilters); if (gen.getPSLevel() == 2) { //Creates a data array from the inline file gen.writeln("{ /temp exch def [" + " { temp 16384 string readstring not {exit } if } loop ] } exec"); } else { gen.writeln("/ReusableStreamDecode filter"); } compressAndWriteBitmap(encoder, gen); gen.writeln("def"); gen.writeDSCComment(DSCConstants.END_RESOURCE); PSResource res = new PSResource(PSResource.TYPE_FORM, formName); gen.getResourceTracker().registerSuppliedResource(res); return res; } /** * Paints a reusable image (previously added as a PostScript form). * @param formName the name of the PostScript form implementing the image * @param targetRect the target rectangle to place the image in * @param gen the PostScript generator * @throws IOException In case of an I/O exception * @deprecated Please use {@link #paintForm(PSResource, Dimension2D, Rectangle2D, PSGenerator)} * instead. */ public static void paintReusableImage(String formName, Rectangle2D targetRect, PSGenerator gen) throws IOException { PSResource form = new PSResource(PSResource.TYPE_FORM, formName); paintForm(form, null, targetRect, gen); } /** * Paints a reusable image (previously added as a PostScript form). * @param form the PostScript form resource implementing the image * @param targetRect the target rectangle to place the image in * @param gen the PostScript generator * @throws IOException In case of an I/O exception * @deprecated Please use {@link #paintForm(PSResource, Dimension2D, Rectangle2D, PSGenerator)} * instead. */ public static void paintForm(PSResource form, Rectangle2D targetRect, PSGenerator gen) throws IOException { paintForm(form, null, targetRect, gen); } /** * Paints a reusable image (previously added as a PostScript form). * @param form the PostScript form resource implementing the image * @param formDimensions the original dimensions of the form * @param targetRect the target rectangle to place the image in * @param gen the PostScript generator * @throws IOException In case of an I/O exception */ public static void paintForm(PSResource form, Dimension2D formDimensions, Rectangle2D targetRect, PSGenerator gen) throws IOException { gen.saveGraphicsState(); translateAndScale(gen, formDimensions, targetRect); gen.writeln(form.getName() + " execform"); gen.getResourceTracker().notifyResourceUsageOnPage(form); gen.restoreGraphicsState(); } private static String getColorSpaceName(ColorSpace colorSpace) { if (colorSpace.getType() == ColorSpace.TYPE_CMYK) { return ("/DeviceCMYK"); } else if (colorSpace.getType() == ColorSpace.TYPE_GRAY) { return ("/DeviceGray"); } else { return ("/DeviceRGB"); } } static void compressAndWriteBitmap(ImageEncoder encoder, PSGenerator gen) throws IOException { OutputStream out = gen.getOutputStream(); out = new ASCII85OutputStream(out); String implicitFilter = encoder.getImplicitFilter(); if (implicitFilter != null) { //nop } else { if (gen.getPSLevel() >= 3) { out = new FlateEncodeOutputStream(out); } else { out = new RunLengthEncodeOutputStream(out); } } encoder.writeTo(out); if (out instanceof Finalizable) { ((Finalizable) out).finalizeStream(); } else { out.flush(); } gen.newLine(); //Just to be sure } /** * Generates commands to modify the current transformation matrix so an image fits * into a given rectangle. * @param gen the PostScript generator * @param imageDimensions the image's dimensions * @param targetRect the target rectangle * @throws IOException if an I/O error occurs */ public static void translateAndScale(PSGenerator gen, Dimension2D imageDimensions, Rectangle2D targetRect) throws IOException { gen.writeln(gen.formatDouble(targetRect.getX()) + " " + gen.formatDouble(targetRect.getY()) + " translate"); if (imageDimensions == null) { imageDimensions = new Dimension(1, 1); } double sx = targetRect.getWidth() / imageDimensions.getWidth(); double sy = targetRect.getHeight() / imageDimensions.getHeight(); if (sx != 1 || sy != 1) { gen.writeln(gen.formatDouble(sx) + " " + gen.formatDouble(sy) + " scale"); } } /** * Extracts a packed RGB integer array of a RenderedImage. * @param img the image * @param startX the starting X coordinate * @param startY the starting Y coordinate * @param w the width of the cropped image * @param h the height of the cropped image * @param rgbArray the prepared integer array to write to * @param offset offset in the target array * @param scansize width of a row in the target array * @return the populated integer array previously passed in as rgbArray parameter */ public static int[] getRGB(RenderedImage img, int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { Raster raster = img.getData(); int yoff = offset; int off; Object data; int nbands = raster.getNumBands(); int dataType = raster.getDataBuffer().getDataType(); switch (dataType) { case DataBuffer.TYPE_BYTE: data = new byte[nbands]; break; case DataBuffer.TYPE_USHORT: data = new short[nbands]; break; case DataBuffer.TYPE_INT: data = new int[nbands]; break; case DataBuffer.TYPE_FLOAT: data = new float[nbands]; break; case DataBuffer.TYPE_DOUBLE: data = new double[nbands]; break; default: throw new IllegalArgumentException("Unknown data buffer type: " + dataType); } if (rgbArray == null) { rgbArray = new int[offset + h * scansize]; } ColorModel colorModel = img.getColorModel(); for (int y = startY; y < startY + h; y++, yoff += scansize) { off = yoff; for (int x = startX; x < startX + w; x++) { rgbArray[off++] = colorModel.getRGB(raster.getDataElements(x, y, data)); } } return rgbArray; } /** * Places an EPS file in the PostScript stream. * @param rawEPS byte array containing the raw EPS data * @param name name for the EPS document * @param x x-coordinate of viewport in points * @param y y-coordinate of viewport in points * @param w width of viewport in points * @param h height of viewport in points * @param bboxx x-coordinate of EPS bounding box in points * @param bboxy y-coordinate of EPS bounding box in points * @param bboxw width of EPS bounding box in points * @param bboxh height of EPS bounding box in points * @param gen the PS generator * @throws IOException in case an I/O error happens during output * @deprecated Please use the variant with the InputStream as parameter */ public static void renderEPS(byte[] rawEPS, String name, float x, float y, float w, float h, float bboxx, float bboxy, float bboxw, float bboxh, PSGenerator gen) throws IOException { renderEPS(new java.io.ByteArrayInputStream(rawEPS), name, new Rectangle2D.Float(x, y, w, h), new Rectangle2D.Float(bboxx, bboxy, bboxw, bboxh), gen); } /** * Places an EPS file in the PostScript stream. * @param in the InputStream that contains the EPS stream * @param name name for the EPS document * @param viewport the viewport in points in which to place the EPS * @param bbox the EPS bounding box in points * @param gen the PS generator * @throws IOException in case an I/O error happens during output */ public static void renderEPS(InputStream in, String name, Rectangle2D viewport, Rectangle2D bbox, PSGenerator gen) throws IOException { gen.getResourceTracker().notifyResourceUsageOnPage(PSProcSets.EPS_PROCSET); gen.writeln("%AXGBeginEPS: " + name); gen.writeln("BeginEPSF"); gen.writeln(gen.formatDouble(viewport.getX()) + " " + gen.formatDouble(viewport.getY()) + " translate"); gen.writeln("0 " + gen.formatDouble(viewport.getHeight()) + " translate"); gen.writeln("1 -1 scale"); double sx = viewport.getWidth() / bbox.getWidth(); double sy = viewport.getHeight() / bbox.getHeight(); if (sx != 1 || sy != 1) { gen.writeln(gen.formatDouble(sx) + " " + gen.formatDouble(sy) + " scale"); } if (bbox.getX() != 0 || bbox.getY() != 0) { gen.writeln(gen.formatDouble(-bbox.getX()) + " " + gen.formatDouble(-bbox.getY()) + " translate"); } gen.writeln(gen.formatDouble(bbox.getX()) + " " + gen.formatDouble(bbox.getY()) + " " + gen.formatDouble(bbox.getWidth()) + " " + gen.formatDouble(bbox.getHeight()) + " re clip"); gen.writeln("newpath"); PSResource res = new PSResource(PSResource.TYPE_FILE, name); gen.getResourceTracker().registerSuppliedResource(res); gen.getResourceTracker().notifyResourceUsageOnPage(res); gen.writeDSCComment(DSCConstants.BEGIN_DOCUMENT, res.getName()); IOUtils.copy(in, gen.getOutputStream()); gen.newLine(); gen.writeDSCComment(DSCConstants.END_DOCUMENT); gen.writeln("EndEPSF"); gen.writeln("%AXGEndEPS"); } }