/*
 * 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: PCLGenerator.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

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.
/** * 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
/** The ESC (escape) character */
public static final char ESC = '\033';
A list of all supported resolutions in PCL (values in dpi)
/** 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.
/** * 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.
Params:
  • out – the OutputStream to write the PCL stream to
/** * Main constructor. * @param out the OutputStream to write the PCL stream to */
public PCLGenerator(OutputStream out) { this.out = new CountingOutputStream(out); }
Main constructor.
Params:
  • out – the OutputStream to write the PCL stream to
  • maxResolution – the maximum resolution to encode bitmap images at
/** * 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()); } }
Returns:the OutputStream that this generator writes to
/** @return the OutputStream that this generator writes to */
public OutputStream getOutputStream() { return this.out; }
Returns the currently active text encoding.
Returns:the text encoding
/** * Returns the currently active text encoding. * @return the text encoding */
public String getTextEncoding() { return ISO_8859_1; }
Returns:the maximum resolution to encode bitmap images at
/** @return the maximum resolution to encode bitmap images at */
public int getMaximumBitmapResolution() { return this.maxBitmapResolution; }
Writes a PCL escape command to the output stream.
Params:
  • cmd – the command (without the ESCAPE character)
Throws:
/** * 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.
Params:
  • s – the text
Throws:
/** * 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
Params:
  • bytes – The bytes
Throws:
/** * 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.
Params:
  • value – value to format
Returns:the formatted value
/** * 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.
Params:
  • value – value to format
Returns:the formatted 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
/** * 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
/** * 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
/** * 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
/** * 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.
Params:
  • value – the resolution value (units per inch)
Throws:
/** * 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
Params:
  • value – the resolution value (units per inch)
Throws:
/** * 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.
Params:
  • selector – the integer representing the page size
Throws:
/** * 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.
Params:
  • selector – the integer representing the paper source/tray
Throws:
/** * 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.
Params:
  • selector – the integer representing the output bin
Throws:
/** * 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.
Params:
  • selector – the integer representing the duplexing mode of the page
Throws:
/** * 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
/** * 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.
Params:
  • numberOfLines – the number of lines (See PCL specification for details)
Throws:
/** * 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.
Params:
  • numberOfLines – the number of lines
Throws:
/** * 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).
Params:
  • value – the VMI value
Throws:
/** * 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.
Params:
  • x – the X coordinate (in millipoints)
  • y – the Y coordinate (in millipoints)
Throws:
/** * 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
/** * 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
/** * 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.
Params:
  • rotate – the rotation angle (counterclockwise), one of 0, 90, 180 and 270.
Throws:
/** * 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.
Params:
  • restorePreviousHPGL2Cursor – true if the previous HP GL/2 pen position should be restored, false if the current position is maintained
Throws:
/** * 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.
Params:
  • restorePreviousPCLCursor – true if the previous PCL cursor position should be restored, false if the current position is maintained
Throws:
/** * 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.
Params:
  • w – the width in millipoints
  • h – the height in millipoints
  • col – the fill color
Throws:
/** * 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.
Params:
  • col – the color to create the pattern for
  • patternID – the pattern ID to use
  • ditherMatrixSize – the size of the Bayer dither matrix to use (4 or 8 supported)
Throws:
/** * 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.
Params:
  • transparent – true if transparent, false for opaque
Throws:
/** * 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.
Params:
  • transparent – true if transparent, false for opaque
Throws:
/** * 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.
Params:
  • source – source transparency: true if transparent, false for opaque
  • pattern – pattern transparency: true if transparent, false for opaque
Throws:
/** * 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.
Params:
  • r – the red component
  • g – the green component
  • b – the blue component
Returns:the gray value
/** * 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).
Params:
  • col – the color
Returns:the PCL shade value (100=black)
/** * 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).
Params:
  • col – the color
Throws:
/** * 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
Params:
  • patternID – the pattern ID (<ESC>*c#G command)
  • pattern – the pattern type (<ESC>*v#T command)
Throws:
/** * Select the current pattern * @param patternID the pattern ID (&lt;ESC&gt;*c#G command) * @param pattern the pattern type (&lt;ESC&gt;*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.
Params:
  • quality – a quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest)
/** * 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.
Returns:the quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest)
/** * 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.
Params:
  • img – the image
Returns:true if it's a monochrome image
/** * 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.
Params:
  • img – the image
Returns:true if it's a grayscale image
/** * 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).
Returns:true if JAI is available
/** * 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.
Params:
  • resolution – the input resolution
  • 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.
Returns:the resulting PCL resolution (one of 75, 100, 150, 200, 300, 600)
/** * 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.
Params:
  • img – the bitmap image
  • targetDim – the target Dimention (in mpt)
  • sourceTransparency – true if the background should not be erased
Throws:
/** * 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.
Params:
  • img – the bitmap image (must be 1-bit b/w)
  • resolution – the resolution of the image (must be a PCL resolution)
Throws:
/** * 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; } } }