/* * 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.pcl; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.awt.image.DirectColorModel; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SinglePixelPackedSampleModel; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.io.output.CountingOutputStream; import org.apache.xmlgraphics.util.UnitConv; import org.apache.fop.fonts.Typeface; import org.apache.fop.render.pcl.fonts.PCLFontReader; import org.apache.fop.render.pcl.fonts.PCLSoftFontManager; import org.apache.fop.util.bitmap.BitmapImageUtil; import org.apache.fop.util.bitmap.DitherUtil; /** * This class provides methods for generating PCL print files. */ public class PCLGenerator { private static final String US_ASCII = "US-ASCII"; private static final String ISO_8859_1 = "ISO-8859-1"; /** The ESC (escape) character */ public static final char ESC = '\033'; /** A list of all supported resolutions in PCL (values in dpi) */ public static final int[] PCL_RESOLUTIONS = new int[] {75, 100, 150, 200, 300, 600}; private final DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US); private final DecimalFormat df2 = new DecimalFormat("0.##", symbols); private final DecimalFormat df4 = new DecimalFormat("0.####", symbols); private final CountingOutputStream out; protected Map<Typeface, PCLFontReader> fontReaderMap = new HashMap<Typeface, PCLFontReader>(); protected Map<PCLSoftFontManager, Map<Typeface, Long>> fontManagerMap = new LinkedHashMap<PCLSoftFontManager, Map<Typeface, Long>>(); private boolean currentSourceTransparency = true; private boolean currentPatternTransparency = true; private int maxBitmapResolution = PCL_RESOLUTIONS[PCL_RESOLUTIONS.length - 1]; private float ditheringQuality = 0.5f; /** * true: Standard PCL shades are used (poor quality). false: user-defined pattern are used * to create custom dither patterns for better grayscale quality. */ private static final boolean USE_PCL_SHADES = false; /** * Main constructor. * @param out the OutputStream to write the PCL stream to */ public PCLGenerator(OutputStream out) { this.out = new CountingOutputStream(out); } /** * Main constructor. * @param out the OutputStream to write the PCL stream to * @param maxResolution the maximum resolution to encode bitmap images at */ public PCLGenerator(OutputStream out, int maxResolution) { this(out); boolean found = false; for (int pclResolutions : PCL_RESOLUTIONS) { if (pclResolutions == maxResolution) { found = true; break; } } if (!found) { throw new IllegalArgumentException("Illegal value for maximum resolution!"); } this.maxBitmapResolution = maxResolution; } public void addFont(PCLSoftFontManager sfManager, Typeface font) { if (!fontManagerMap.containsKey(sfManager)) { fontManagerMap.put(sfManager, new LinkedHashMap<Typeface, Long>()); } Map<Typeface, Long> fonts = fontManagerMap.get(sfManager); if (!fonts.containsKey(font)) { fonts.put(font, out.getByteCount()); } } /** @return the OutputStream that this generator writes to */ public OutputStream getOutputStream() { return this.out; } /** * Returns the currently active text encoding. * @return the text encoding */ public String getTextEncoding() { return ISO_8859_1; } /** @return the maximum resolution to encode bitmap images at */ public int getMaximumBitmapResolution() { return this.maxBitmapResolution; } /** * Writes a PCL escape command to the output stream. * @param cmd the command (without the ESCAPE character) * @throws IOException In case of an I/O error */ public void writeCommand(String cmd) throws IOException { out.write(27); //ESC out.write(cmd.getBytes(US_ASCII)); } /** * Writes raw text (in ISO-8859-1 encoding) to the output stream. * @param s the text * @throws IOException In case of an I/O error */ public void writeText(String s) throws IOException { out.write(s.getBytes(ISO_8859_1)); } /** * Writes raw bytes to the output stream * @param bytes The bytes * @throws IOException In case of an I/O error */ public void writeBytes(byte[] bytes) throws IOException { out.write(bytes); } /** * Formats a double value with two decimal positions for PCL output. * * @param value value to format * @return the formatted value */ public final String formatDouble2(double value) { return df2.format(value); } /** * Formats a double value with four decimal positions for PCL output. * * @param value value to format * @return the formatted value */ public final String formatDouble4(double value) { return df4.format(value); } /** * Sends the universal end of language command (UEL). * @throws IOException In case of an I/O error */ public void universalEndOfLanguage() throws IOException { writeCommand("%-12345X"); } /** * Resets the printer and restores the user default environment. * @throws IOException In case of an I/O error */ public void resetPrinter() throws IOException { writeCommand("E"); } /** * Sends the job separation command. * @throws IOException In case of an I/O error */ public void separateJobs() throws IOException { writeCommand("&l1T"); } /** * Sends the form feed character. * @throws IOException In case of an I/O error */ public void formFeed() throws IOException { out.write(12); //=OC ("FF", Form feed) } /** * Sets the unit of measure. * @param value the resolution value (units per inch) * @throws IOException In case of an I/O error */ public void setUnitOfMeasure(int value) throws IOException { writeCommand("&u" + value + "D"); } /** * Sets the raster graphics resolution * @param value the resolution value (units per inch) * @throws IOException In case of an I/O error */ public void setRasterGraphicsResolution(int value) throws IOException { writeCommand("*t" + value + "R"); } /** * Selects the page size. * @param selector the integer representing the page size * @throws IOException In case of an I/O error */ public void selectPageSize(int selector) throws IOException { writeCommand("&l" + selector + "A"); } /** * Selects the paper source. The parameter is usually printer-specific. Usually, "1" is the * default tray, "2" is the manual paper feed, "3" is the manual envelope feed, "4" is the * "lower" tray and "7" is "auto-select". Consult the technical reference for your printer * for all available values. * @param selector the integer representing the paper source/tray * @throws IOException In case of an I/O error */ public void selectPaperSource(int selector) throws IOException { writeCommand("&l" + selector + "H"); } /** * Selects the output bin. The parameter is usually printer-specific. Usually, "1" is the * default output bin (upper bin) and "2" is the lower (rear) output bin. Some printers * may support additional output bins. Consult the technical reference for your printer * for all available values. * @param selector the integer representing the output bin * @throws IOException In case of an I/O error */ public void selectOutputBin(int selector) throws IOException { writeCommand("&l" + selector + "G"); } /** * Selects the duplexing mode for the page. * The parameter is usually printer-specific. * "0" means Simplex, * "1" means Duplex, Long-Edge Binding, * "2" means Duplex, Short-Edge Binding. * @param selector the integer representing the duplexing mode of the page * @throws IOException In case of an I/O error */ public void selectDuplexMode(int selector) throws IOException { writeCommand("&l" + selector + "S"); } /** * Clears the horizontal margins. * @throws IOException In case of an I/O error */ public void clearHorizontalMargins() throws IOException { writeCommand("9"); } /** * The Top Margin command designates the number of lines between * the top of the logical page and the top of the text area. * @param numberOfLines the number of lines (See PCL specification for details) * @throws IOException In case of an I/O error */ public void setTopMargin(int numberOfLines) throws IOException { writeCommand("&l" + numberOfLines + "E"); } /** * The Text Length command can be used to define the bottom border. See the PCL specification * for details. * @param numberOfLines the number of lines * @throws IOException In case of an I/O error */ public void setTextLength(int numberOfLines) throws IOException { writeCommand("&l" + numberOfLines + "F"); } /** * Sets the Vertical Motion Index (VMI). * @param value the VMI value * @throws IOException In case of an I/O error */ public void setVMI(double value) throws IOException { writeCommand("&l" + formatDouble4(value) + "C"); } /** * Sets the cursor to a new absolute coordinate. * @param x the X coordinate (in millipoints) * @param y the Y coordinate (in millipoints) * @throws IOException In case of an I/O error */ public void setCursorPos(double x, double y) throws IOException { if (x < 0) { //A negative x value will result in a relative movement so go to "0" first. //But this will most probably have no effect anyway since you can't paint to the left //of the logical page writeCommand("&a0h" + formatDouble2(x / 100) + "h" + formatDouble2(y / 100) + "V"); } else { writeCommand("&a" + formatDouble2(x / 100) + "h" + formatDouble2(y / 100) + "V"); } } /** * Pushes the current cursor position on a stack (stack size: max 20 entries) * @throws IOException In case of an I/O error */ public void pushCursorPos() throws IOException { writeCommand("&f0S"); } /** * Pops the current cursor position from the stack. * @throws IOException In case of an I/O error */ public void popCursorPos() throws IOException { writeCommand("&f1S"); } /** * Changes the current print direction while maintaining the current cursor position. * @param rotate the rotation angle (counterclockwise), one of 0, 90, 180 and 270. * @throws IOException In case of an I/O error */ public void changePrintDirection(int rotate) throws IOException { writeCommand("&a" + rotate + "P"); } /** * Enters the HP GL/2 mode. * @param restorePreviousHPGL2Cursor true if the previous HP GL/2 pen position should be * restored, false if the current position is maintained * @throws IOException In case of an I/O error */ public void enterHPGL2Mode(boolean restorePreviousHPGL2Cursor) throws IOException { if (restorePreviousHPGL2Cursor) { writeCommand("%0B"); } else { writeCommand("%1B"); } } /** * Enters the PCL mode. * @param restorePreviousPCLCursor true if the previous PCL cursor position should be restored, * false if the current position is maintained * @throws IOException In case of an I/O error */ public void enterPCLMode(boolean restorePreviousPCLCursor) throws IOException { if (restorePreviousPCLCursor) { writeCommand("%0A"); } else { writeCommand("%1A"); } } /** * Generate a filled rectangle at the current cursor position. * * @param w the width in millipoints * @param h the height in millipoints * @param col the fill color * @throws IOException In case of an I/O error */ protected void fillRect(int w, int h, Color col, boolean colorEnabled) throws IOException { if ((w == 0) || (h == 0)) { return; } if (h < 0) { h *= -1; } else { //y += h; } setPatternTransparencyMode(false); if (USE_PCL_SHADES || Color.black.equals(col) || Color.white.equals(col)) { writeCommand("*c" + formatDouble4(w / 100.0) + "h" + formatDouble4(h / 100.0) + "V"); int lineshade = convertToPCLShade(col); writeCommand("*c" + lineshade + "G"); writeCommand("*c2P"); //Shaded fill } else { if (colorEnabled) { selectColor(col); writeCommand("*c" + formatDouble4(w / 100.0) + "h" + formatDouble4(h / 100.0) + "V"); writeCommand("*c0P"); //Solid fill } else { defineGrayscalePattern(col, 32, DitherUtil.DITHER_MATRIX_4X4); writeCommand("*c" + formatDouble4(w / 100.0) + "h" + formatDouble4(h / 100.0) + "V"); writeCommand("*c32G"); writeCommand("*c4P"); //User-defined pattern } } // Reset pattern transparency mode. setPatternTransparencyMode(true); } /** * Generates a user-defined pattern for a dithering pattern matching the grayscale value * of the color given. * @param col the color to create the pattern for * @param patternID the pattern ID to use * @param ditherMatrixSize the size of the Bayer dither matrix to use (4 or 8 supported) * @throws IOException In case of an I/O error */ public void defineGrayscalePattern(Color col, int patternID, int ditherMatrixSize) throws IOException { ByteArrayOutputStream baout = new ByteArrayOutputStream(); DataOutputStream data = new DataOutputStream(baout); data.writeByte(0); //Format data.writeByte(0); //Continuation data.writeByte(1); //Pixel Encoding data.writeByte(0); //Reserved data.writeShort(8); //Width in Pixels data.writeShort(8); //Height in Pixels //data.writeShort(600); //X Resolution (didn't manage to get that to work) //data.writeShort(600); //Y Resolution int gray255 = convertToGray(col.getRed(), col.getGreen(), col.getBlue()); byte[] pattern; if (ditherMatrixSize == 8) { pattern = DitherUtil.getBayerDither(DitherUtil.DITHER_MATRIX_8X8, gray255, false); } else { //Since a 4x4 pattern did not work, the 4x4 pattern is applied 4 times to an //8x8 pattern. Maybe this could be changed to use an 8x8 bayer dither pattern //instead of the 4x4 one. pattern = DitherUtil.getBayerDither(DitherUtil.DITHER_MATRIX_4X4, gray255, true); } data.write(pattern); if ((baout.size() % 2) > 0) { baout.write(0); } writeCommand("*c" + patternID + "G"); writeCommand("*c" + baout.size() + "W"); baout.writeTo(this.out); IOUtils.closeQuietly(data); IOUtils.closeQuietly(baout); writeCommand("*c4Q"); //temporary pattern } /** * Sets the source transparency mode. * @param transparent true if transparent, false for opaque * @throws IOException In case of an I/O error */ public void setSourceTransparencyMode(boolean transparent) throws IOException { setTransparencyMode(transparent, currentPatternTransparency); } /** * Sets the pattern transparency mode. * @param transparent true if transparent, false for opaque * @throws IOException In case of an I/O error */ public void setPatternTransparencyMode(boolean transparent) throws IOException { setTransparencyMode(currentSourceTransparency, transparent); } /** * Sets the transparency modes. * @param source source transparency: true if transparent, false for opaque * @param pattern pattern transparency: true if transparent, false for opaque * @throws IOException In case of an I/O error */ public void setTransparencyMode(boolean source, boolean pattern) throws IOException { if (source != currentSourceTransparency && pattern != currentPatternTransparency) { writeCommand("*v" + (source ? '0' : '1') + "n" + (pattern ? '0' : '1') + "O"); } else if (source != currentSourceTransparency) { writeCommand("*v" + (source ? '0' : '1') + "N"); } else if (pattern != currentPatternTransparency) { writeCommand("*v" + (pattern ? '0' : '1') + "O"); } this.currentSourceTransparency = source; this.currentPatternTransparency = pattern; } /** * Convert an RGB color value to a grayscale from 0 to 100. * @param r the red component * @param g the green component * @param b the blue component * @return the gray value */ public final int convertToGray(int r, int g, int b) { return BitmapImageUtil.convertToGray(r, g, b); } /** * Convert a Color value to a PCL shade value (0-100). * @param col the color * @return the PCL shade value (100=black) */ public final int convertToPCLShade(Color col) { float gray = convertToGray(col.getRed(), col.getGreen(), col.getBlue()) / 255f; return (int)(100 - (gray * 100f)); } /** * Selects the current grayscale color (the given color is converted to grayscales). * @param col the color * @throws IOException In case of an I/O error */ public void selectGrayscale(Color col) throws IOException { if (Color.black.equals(col)) { selectCurrentPattern(0, 0); //black } else if (Color.white.equals(col)) { selectCurrentPattern(0, 1); //white } else { if (USE_PCL_SHADES) { selectCurrentPattern(convertToPCLShade(col), 2); } else { defineGrayscalePattern(col, 32, DitherUtil.DITHER_MATRIX_4X4); selectCurrentPattern(32, 4); } } } public void selectColor(Color col) throws IOException { writeCommand("*v6W"); writeBytes(new byte[]{0, 1, 1, 8, 8, 8}); writeCommand(String.format("*v%da%db%dc0I", col.getRed(), col.getGreen(), col.getBlue())); writeCommand("*v0S"); } /** * Select the current pattern * @param patternID the pattern ID (<ESC>*c#G command) * @param pattern the pattern type (<ESC>*v#T command) * @throws IOException In case of an I/O error */ public void selectCurrentPattern(int patternID, int pattern) throws IOException { if (pattern > 1) { writeCommand("*c" + patternID + "G"); } writeCommand("*v" + pattern + "T"); } /** * Sets the dithering quality used when encoding gray or color images. If not explicitely * set a medium setting (0.5f) is used. * @param quality a quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest) */ public void setDitheringQuality(float quality) { quality = Math.min(Math.max(0f, quality), 1.0f); this.ditheringQuality = quality; } /** * Returns the dithering quality used when encoding gray or color images. * @return the quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest) */ public float getDitheringQuality() { return this.ditheringQuality; } /** * Indicates whether an image is a monochrome (b/w) image. * @param img the image * @return true if it's a monochrome image */ public static boolean isMonochromeImage(RenderedImage img) { return BitmapImageUtil.isMonochromeImage(img); } /** * Indicates whether an image is a grayscale image. * @param img the image * @return true if it's a grayscale image */ public static boolean isGrayscaleImage(RenderedImage img) { return BitmapImageUtil.isGrayscaleImage(img); } private static int jaiAvailable = -1; //no synchronization necessary, not critical /** * Indicates whether JAI is available. JAI has shown to be reliable when dithering a * grayscale or color image to monochrome bitmaps (1-bit). * @return true if JAI is available */ public static boolean isJAIAvailable() { if (jaiAvailable < 0) { try { String clName = "javax.media.jai.JAI"; Class.forName(clName); jaiAvailable = 1; } catch (ClassNotFoundException cnfe) { jaiAvailable = 0; } } return (jaiAvailable > 0); } private int calculatePCLResolution(int resolution) { return calculatePCLResolution(resolution, false); } /** * Calculates the ideal PCL resolution for a given resolution. * @param resolution the input resolution * @param increased true if you want to go to a higher resolution, for example if you * convert grayscale or color images to monochrome images so dithering has * a chance to generate better quality. * @return the resulting PCL resolution (one of 75, 100, 150, 200, 300, 600) */ private int calculatePCLResolution(int resolution, boolean increased) { int choice = -1; for (int i = PCL_RESOLUTIONS.length - 2; i >= 0; i--) { if (resolution > PCL_RESOLUTIONS[i]) { int idx = i + 1; if (idx < PCL_RESOLUTIONS.length - 2) { idx += increased ? 2 : 0; } else if (idx < PCL_RESOLUTIONS.length - 1) { idx += increased ? 1 : 0; } choice = idx; break; //return PCL_RESOLUTIONS[idx]; } } if (choice < 0) { choice = (increased ? 2 : 0); } while (choice > 0 && PCL_RESOLUTIONS[choice] > getMaximumBitmapResolution()) { choice--; } return PCL_RESOLUTIONS[choice]; } private boolean isValidPCLResolution(int resolution) { return resolution == calculatePCLResolution(resolution); } //Threshold table to convert an alpha channel (8-bit) into a clip mask (1-bit) private static final byte[] THRESHOLD_TABLE = new byte[256]; static { // Initialize the arrays for (int i = 0; i < 256; i++) { THRESHOLD_TABLE[i] = (byte) ((i < 240) ? 255 : 0); } } /* not used private RenderedImage getMask(RenderedImage img, Dimension targetDim) { ColorModel cm = img.getColorModel(); if (cm.hasAlpha()) { BufferedImage alpha = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY); Raster raster = img.getData(); GraphicsUtil.copyBand(raster, cm.getNumColorComponents(), alpha.getRaster(), 0); BufferedImageOp op1 = new LookupOp(new ByteLookupTable(0, THRESHOLD_TABLE), null); BufferedImage alphat = op1.filter(alpha, null); BufferedImage mask; if (true) { mask = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_BYTE_BINARY); } else { byte[] arr = {(byte)0, (byte)0xff}; ColorModel colorModel = new IndexColorModel(1, 2, arr, arr, arr); WritableRaster wraster = Raster.createPackedRaster(DataBuffer.TYPE_BYTE, targetDim.width, targetDim.height, 1, 1, null); mask = new BufferedImage(colorModel, wraster, false, null); } Graphics2D g2d = mask.createGraphics(); try { AffineTransform at = new AffineTransform(); double sx = targetDim.getWidth() / img.getWidth(); double sy = targetDim.getHeight() / img.getHeight(); at.scale(sx, sy); g2d.drawRenderedImage(alphat, at); } finally { g2d.dispose(); } return mask; } else { return null; } } */ /** * Paint a bitmap at the current cursor position. The bitmap is converted to a monochrome * (1-bit) bitmap image. * @param img the bitmap image * @param targetDim the target Dimention (in mpt) * @param sourceTransparency true if the background should not be erased * @throws IOException In case of an I/O error */ public void paintBitmap(RenderedImage img, Dimension targetDim, boolean sourceTransparency, PCLRenderingUtil pclUtil) throws IOException { final boolean printerSupportsColor = pclUtil.isColorEnabled(); boolean monochrome = isMonochromeImage(img); double targetHResolution = img.getWidth() / UnitConv.mpt2in(targetDim.width); double targetVResolution = img.getHeight() / UnitConv.mpt2in(targetDim.height); double targetResolution = Math.max(targetHResolution, targetVResolution); int resolution = (int)Math.round(targetResolution); int effResolution = calculatePCLResolution(resolution, !(printerSupportsColor && !monochrome)); Dimension orgDim = new Dimension(img.getWidth(), img.getHeight()); Dimension effDim; if (targetResolution == effResolution) { effDim = orgDim; //avoid scaling side-effects } else { effDim = new Dimension( (int)Math.ceil(UnitConv.mpt2px(targetDim.width, effResolution)), (int)Math.ceil(UnitConv.mpt2px(targetDim.height, effResolution))); } boolean scaled = !orgDim.equals(effDim); if (!monochrome) { if (printerSupportsColor) { selectCurrentPattern(0, 0); //Solid black renderImageAsColor(img, effResolution); } else { //Transparency mask disabled. Doesn't work reliably /* final boolean transparencyDisabled = true; RenderedImage mask = (transparencyDisabled ? null : getMask(img, effDim)); if (mask != null) { pushCursorPos(); selectCurrentPattern(0, 1); //Solid white setTransparencyMode(true, true); paintMonochromeBitmap(mask, effResolution); popCursorPos(); } */ RenderedImage red = BitmapImageUtil.convertToMonochrome( img, effDim, this.ditheringQuality); selectCurrentPattern(0, 0); //Solid black setTransparencyMode(sourceTransparency /*|| mask != null*/, true); paintMonochromeBitmap(red, effResolution); } } else { RenderedImage effImg = img; if (scaled) { effImg = BitmapImageUtil.convertToMonochrome(img, effDim); } setSourceTransparencyMode(sourceTransparency); selectCurrentPattern(0, 0); //Solid black paintMonochromeBitmap(effImg, effResolution); } } private int toGray(int rgb) { // see http://www.jguru.com/faq/view.jsp?EID=221919 double greyVal = 0.072169d * (rgb & 0xff); rgb >>= 8; greyVal += 0.715160d * (rgb & 0xff); rgb >>= 8; greyVal += 0.212671d * (rgb & 0xff); return (int)greyVal; } private void renderImageAsColor(RenderedImage imgOrg, int dpi) throws IOException { BufferedImage img = new BufferedImage(imgOrg.getWidth(), imgOrg.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g = img.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, imgOrg.getWidth(), imgOrg.getHeight()); g.drawImage((Image) imgOrg, 0, 0, null); if (!isValidPCLResolution(dpi)) { throw new IllegalArgumentException("Invalid PCL resolution: " + dpi); } int w = img.getWidth(); ColorModel cm = img.getColorModel(); if (cm instanceof DirectColorModel) { writeCommand("*v6W"); // ImagingMode out.write(new byte[]{0, 3, 0, 8, 8, 8}); } else { IndexColorModel icm = (IndexColorModel)cm; writeCommand("*v6W"); // ImagingMode out.write(new byte[]{0, 1, (byte)icm.getMapSize(), 8, 8, 8}); byte[] reds = new byte[256]; byte[] greens = new byte[256]; byte[] blues = new byte[256]; icm.getReds(reds); icm.getGreens(greens); icm.getBlues(blues); for (int i = 0; i < icm.getMapSize(); i++) { writeCommand("*v" + (reds[i] & 0xFF) + "A"); //ColorComponentOne writeCommand("*v" + (greens[i] & 0xFF) + "B"); //ColorComponentTwo writeCommand("*v" + (blues[i] & 0xFF) + "C"); //ColorComponentThree writeCommand("*v" + i + "I"); //AssignColorIndex } } setRasterGraphicsResolution(dpi); writeCommand("*r0f" + img.getHeight() + "t" + (w) + "S"); writeCommand("*r1A"); Raster raster = img.getData(); ColorEncoder encoder = new ColorEncoder(img); // Transfer graphics data if (cm.getTransferType() == DataBuffer.TYPE_BYTE) { DataBufferByte dataBuffer = (DataBufferByte)raster.getDataBuffer(); if (img.getSampleModel() instanceof MultiPixelPackedSampleModel && dataBuffer.getNumBanks() == 1) { byte[] buf = dataBuffer.getData(); MultiPixelPackedSampleModel sampleModel = (MultiPixelPackedSampleModel)img.getSampleModel(); int scanlineStride = sampleModel.getScanlineStride(); int idx = 0; for (int y = 0, maxy = img.getHeight(); y < maxy; y++) { for (int x = 0; x < scanlineStride; x++) { encoder.add8Bits(buf[idx]); idx++; } encoder.endLine(); } } else { throw new IOException("Unsupported image"); } } else if (cm.getTransferType() == DataBuffer.TYPE_INT) { DataBufferInt dataBuffer = (DataBufferInt)raster.getDataBuffer(); if (img.getSampleModel() instanceof SinglePixelPackedSampleModel && dataBuffer.getNumBanks() == 1) { int[] buf = dataBuffer.getData(); SinglePixelPackedSampleModel sampleModel = (SinglePixelPackedSampleModel)img.getSampleModel(); int scanlineStride = sampleModel.getScanlineStride(); int idx = 0; for (int y = 0, maxy = img.getHeight(); y < maxy; y++) { for (int x = 0; x < scanlineStride; x++) { encoder.add8Bits((byte)(buf[idx] >> 16)); encoder.add8Bits((byte)(buf[idx] >> 8)); encoder.add8Bits((byte)(buf[idx] >> 0)); idx++; } encoder.endLine(); } } else { throw new IOException("Unsupported image"); } } else { throw new IOException("Unsupported image"); } // End raster graphics writeCommand("*rB"); } /** * Paint a bitmap at the current cursor position. The bitmap must be a monochrome * (1-bit) bitmap image. * @param img the bitmap image (must be 1-bit b/w) * @param resolution the resolution of the image (must be a PCL resolution) * @throws IOException In case of an I/O error */ public void paintMonochromeBitmap(RenderedImage img, int resolution) throws IOException { if (!isValidPCLResolution(resolution)) { throw new IllegalArgumentException("Invalid PCL resolution: " + resolution); } boolean monochrome = isMonochromeImage(img); if (!monochrome) { throw new IllegalArgumentException("img must be a monochrome image"); } setRasterGraphicsResolution(resolution); writeCommand("*r0f" + img.getHeight() + "t" + img.getWidth() + "s1A"); Raster raster = img.getData(); Encoder encoder = new Encoder(img); // Transfer graphics data int imgw = img.getWidth(); IndexColorModel cm = (IndexColorModel)img.getColorModel(); if (cm.getTransferType() == DataBuffer.TYPE_BYTE) { DataBufferByte dataBuffer = (DataBufferByte)raster.getDataBuffer(); MultiPixelPackedSampleModel packedSampleModel = new MultiPixelPackedSampleModel( DataBuffer.TYPE_BYTE, img.getWidth(), img.getHeight(), 1); if (img.getSampleModel().equals(packedSampleModel) && dataBuffer.getNumBanks() == 1) { //Optimized packed encoding byte[] buf = dataBuffer.getData(); int scanlineStride = packedSampleModel.getScanlineStride(); int idx = 0; int c0 = toGray(cm.getRGB(0)); int c1 = toGray(cm.getRGB(1)); boolean zeroIsWhite = c0 > c1; for (int y = 0, maxy = img.getHeight(); y < maxy; y++) { for (int x = 0, maxx = scanlineStride; x < maxx; x++) { if (zeroIsWhite) { encoder.add8Bits(buf[idx]); } else { encoder.add8Bits((byte)~buf[idx]); } idx++; } encoder.endLine(); } } else { //Optimized non-packed encoding for (int y = 0, maxy = img.getHeight(); y < maxy; y++) { byte[] line = (byte[])raster.getDataElements(0, y, imgw, 1, null); for (int x = 0, maxx = imgw; x < maxx; x++) { encoder.addBit(line[x] == 0); } encoder.endLine(); } } } else { //Safe but slow fallback for (int y = 0, maxy = img.getHeight(); y < maxy; y++) { for (int x = 0, maxx = imgw; x < maxx; x++) { int sample = raster.getSample(x, y, 0); encoder.addBit(sample == 0); } encoder.endLine(); } } // End raster graphics writeCommand("*rB"); } private class Encoder { private int imgw; private int bytewidth; private byte[] rle; //compressed (RLE) private byte[] uncompressed; //uncompressed private int lastcount = -1; private byte lastbyte; private int rlewidth; private byte ib; //current image bits private int x; private boolean zeroRow = true; public Encoder(RenderedImage img) { imgw = img.getWidth(); bytewidth = (imgw / 8); if ((imgw % 8) != 0) { bytewidth++; } rle = new byte[bytewidth * 2]; uncompressed = new byte[bytewidth]; } public void addBit(boolean bit) { //Set image bit for black if (bit) { ib |= 1; } //RLE encoding if ((x % 8) == 7 || ((x + 1) == imgw)) { finishedByte(); } else { ib <<= 1; } x++; } public void add8Bits(byte b) { ib = b; finishedByte(); x += 8; } private void finishedByte() { if (rlewidth < bytewidth) { if (lastcount >= 0) { if (ib == lastbyte) { lastcount++; } else { rle[rlewidth++] = (byte)(lastcount & 0xFF); rle[rlewidth++] = lastbyte; lastbyte = ib; lastcount = 0; } } else { lastbyte = ib; lastcount = 0; } if (lastcount == 255 || ((x + 1) == imgw)) { rle[rlewidth++] = (byte)(lastcount & 0xFF); rle[rlewidth++] = lastbyte; lastbyte = 0; lastcount = -1; } } uncompressed[x / 8] = ib; if (ib != 0) { zeroRow = false; } ib = 0; } public void endLine() throws IOException { if (zeroRow && PCLGenerator.this.currentSourceTransparency) { writeCommand("*b1Y"); } else if (rlewidth < bytewidth) { writeCommand("*b1m" + rlewidth + "W"); out.write(rle, 0, rlewidth); } else { writeCommand("*b0m" + bytewidth + "W"); out.write(uncompressed); } lastcount = -1; rlewidth = 0; ib = 0; x = 0; zeroRow = true; } } private class ColorEncoder { private int imgw; private int bytewidth; private byte ib; //current image bits private int currentIndex; private int len; private int shiftBit = 0x80; private int whiteLines; final byte[] zeros; final byte[] buff1; final byte[] buff2; final byte[] encodedRun; final byte[] encodedTagged; final byte[] encodedDelta; byte[] seed; byte[] current; int compression; int seedLen; public ColorEncoder(RenderedImage img) { imgw = img.getWidth(); bytewidth = imgw * 3 + 1; zeros = new byte[bytewidth]; buff1 = new byte[bytewidth]; buff2 = new byte[bytewidth]; encodedRun = new byte[bytewidth]; encodedTagged = new byte[bytewidth]; encodedDelta = new byte[bytewidth]; seed = buff1; current = buff2; seedLen = 0; compression = (-1); System.arraycopy(zeros, 0, seed, 0, zeros.length); } private int runCompression(byte[] buff, int len) { int bytes = 0; try { for (int i = 0; i < len;) { int sameCount; byte seed = current[i++]; for (sameCount = 1; i < len && current[i] == seed; i++) { sameCount++; } for (; sameCount > 256; sameCount -= 256) { buff[bytes++] = (byte)255; buff[bytes++] = seed; } if (sameCount > 0) { buff[bytes++] = (byte)(sameCount - 1); buff[bytes++] = seed; } } } catch (ArrayIndexOutOfBoundsException e) { return len + 1; } return bytes; } private int deltaCompression(byte[] seed, byte[] buff, int len) { int bytes = 0; try { for (int i = 0; i < len;) { int sameCount; int diffCount; for (sameCount = 0; i < len && current[i] == seed[i]; i++) { sameCount++; } for (diffCount = 0; i < len && current[i] != seed[i]; i++) { diffCount++; } for (; diffCount != 0;) { int diffToWrite = (diffCount > 8) ? 8 : diffCount; int sameToWrite = (sameCount > 31) ? 31 : sameCount; buff[bytes++] = (byte)(((diffToWrite - 1) << 5) | sameToWrite); sameCount -= sameToWrite; if (sameToWrite == 31) { for (; sameCount >= 255; sameCount -= 255) { buff[bytes++] = (byte)255; } buff[bytes++] = (byte)sameCount; sameCount = 0; } System.arraycopy(current, i - diffCount, buff, bytes, diffToWrite); bytes += diffToWrite; diffCount -= diffToWrite; } } } catch (ArrayIndexOutOfBoundsException e) { return len + 1; } return bytes; } private int tiffCompression(byte[] encodedTagged, int len) { int literalCount = 0; int bytes = 0; try { for (int from = 0; from < len;) { int repeatLength; int repeatValue = current[from]; for (repeatLength = 1; repeatLength < 128 && from + repeatLength < len && current[from + repeatLength] == repeatValue;) { repeatLength++; } if (literalCount == 128 || (repeatLength > 2 && literalCount > 0)) { encodedTagged[bytes++] = (byte)(literalCount - 1); System.arraycopy(current, from - literalCount, encodedTagged, bytes, literalCount); bytes += literalCount; literalCount = 0; } if (repeatLength > 2) { encodedTagged[bytes++] = (byte)(1 - repeatLength); encodedTagged[bytes++] = current[from]; from += repeatLength; } else { literalCount++; from++; } } if (literalCount > 0) { encodedTagged[bytes++] = (byte)(literalCount - 1); System.arraycopy(current, (3 * len) - literalCount, encodedTagged, bytes, literalCount); bytes += literalCount; } } catch (ArrayIndexOutOfBoundsException e) { return len + 1; } return bytes; } public void addBit(boolean bit) { //Set image bit for black if (bit) { ib |= shiftBit; } shiftBit >>= 1; if (shiftBit == 0) { add8Bits(ib); shiftBit = 0x80; ib = 0; } } public void add8Bits(byte b) { current[currentIndex++] = b; if (b != 0) { len = currentIndex; } } public void endLine() throws IOException { if (len == 0) { whiteLines++; } else { if (whiteLines > 0) { writeCommand("*b" + whiteLines + "Y"); whiteLines = 0; } int unencodedCount = len; int runCount = runCompression(encodedRun, len); int tiffCount = tiffCompression(encodedTagged, len); int deltaCount = deltaCompression(seed, encodedDelta, Math.max(len, seedLen)); int bestCount = Math.min(unencodedCount, Math.min(runCount, Math.min(tiffCount, deltaCount))); int bestCompression; if (bestCount == unencodedCount) { bestCompression = 0; } else if (bestCount == runCount) { bestCompression = 1; } else if (bestCount == tiffCount) { bestCompression = 2; } else { bestCompression = 3; } if (compression != bestCompression) { compression = bestCompression; writeCommand("*b" + compression + "M"); } if (bestCompression == 0) { writeCommand("*b" + unencodedCount + "W"); out.write(current, 0, unencodedCount); } else if (bestCompression == 1) { writeCommand("*b" + runCount + "W"); out.write(encodedRun, 0, runCount); } else if (bestCompression == 2) { writeCommand("*b" + tiffCount + "W"); out.write(encodedTagged, 0, tiffCount); } else if (bestCompression == 3) { writeCommand("*b" + deltaCount + "W"); out.write(encodedDelta, 0, deltaCount); } if (current == buff1) { seed = buff1; current = buff2; } else { seed = buff2; current = buff1; } seedLen = len; } shiftBit = 0x80; ib = 0; len = 0; currentIndex = 0; } } }