You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PCLGenerator.java 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.render.pcl;
  19. import java.awt.Color;
  20. import java.awt.Dimension;
  21. import java.awt.Graphics2D;
  22. import java.awt.geom.AffineTransform;
  23. import java.awt.image.BufferedImage;
  24. import java.awt.image.BufferedImageOp;
  25. import java.awt.image.ByteLookupTable;
  26. import java.awt.image.ColorModel;
  27. import java.awt.image.DataBuffer;
  28. import java.awt.image.DataBufferByte;
  29. import java.awt.image.IndexColorModel;
  30. import java.awt.image.LookupOp;
  31. import java.awt.image.MultiPixelPackedSampleModel;
  32. import java.awt.image.Raster;
  33. import java.awt.image.RenderedImage;
  34. import java.awt.image.WritableRaster;
  35. import java.io.DataOutputStream;
  36. import java.io.IOException;
  37. import java.io.OutputStream;
  38. import java.text.DecimalFormat;
  39. import java.text.DecimalFormatSymbols;
  40. import java.util.Locale;
  41. import org.apache.commons.io.output.ByteArrayOutputStream;
  42. import org.apache.xmlgraphics.image.GraphicsUtil;
  43. import org.apache.xmlgraphics.util.UnitConv;
  44. import org.apache.fop.util.bitmap.BitmapImageUtil;
  45. import org.apache.fop.util.bitmap.DitherUtil;
  46. /**
  47. * This class provides methods for generating PCL print files.
  48. */
  49. public class PCLGenerator {
  50. private static final String US_ASCII = "US-ASCII";
  51. private static final String ISO_8859_1 = "ISO-8859-1";
  52. /** The ESC (escape) character */
  53. public static final char ESC = '\033';
  54. /** A list of all supported resolutions in PCL (values in dpi) */
  55. public static final int[] PCL_RESOLUTIONS = new int[] {75, 100, 150, 200, 300, 600};
  56. private final DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US);
  57. private final DecimalFormat df2 = new DecimalFormat("0.##", symbols);
  58. private final DecimalFormat df4 = new DecimalFormat("0.####", symbols);
  59. private final OutputStream out;
  60. private boolean currentSourceTransparency = true;
  61. private boolean currentPatternTransparency = true;
  62. private int maxBitmapResolution = PCL_RESOLUTIONS[PCL_RESOLUTIONS.length - 1];
  63. private float ditheringQuality = 0.5f;
  64. /**
  65. * true: Standard PCL shades are used (poor quality). false: user-defined pattern are used
  66. * to create custom dither patterns for better grayscale quality.
  67. */
  68. private final boolean usePCLShades = false;
  69. /**
  70. * Main constructor.
  71. * @param out the OutputStream to write the PCL stream to
  72. */
  73. public PCLGenerator(OutputStream out) {
  74. this.out = out;
  75. }
  76. /**
  77. * Main constructor.
  78. * @param out the OutputStream to write the PCL stream to
  79. * @param maxResolution the maximum resolution to encode bitmap images at
  80. */
  81. public PCLGenerator(OutputStream out, int maxResolution) {
  82. this(out);
  83. boolean found = false;
  84. for (int i = 0; i < PCL_RESOLUTIONS.length; i++) {
  85. if (PCL_RESOLUTIONS[i] == maxResolution) {
  86. found = true;
  87. break;
  88. }
  89. }
  90. if (!found) {
  91. throw new IllegalArgumentException("Illegal value for maximum resolution!");
  92. }
  93. this.maxBitmapResolution = maxResolution;
  94. }
  95. /** @return the OutputStream that this generator writes to */
  96. public OutputStream getOutputStream() {
  97. return this.out;
  98. }
  99. /**
  100. * Returns the currently active text encoding.
  101. * @return the text encoding
  102. */
  103. public String getTextEncoding() {
  104. return ISO_8859_1;
  105. }
  106. /** @return the maximum resolution to encode bitmap images at */
  107. public int getMaximumBitmapResolution() {
  108. return this.maxBitmapResolution;
  109. }
  110. /**
  111. * Writes a PCL escape command to the output stream.
  112. * @param cmd the command (without the ESCAPE character)
  113. * @throws IOException In case of an I/O error
  114. */
  115. public void writeCommand(String cmd) throws IOException {
  116. out.write(27); //ESC
  117. out.write(cmd.getBytes(US_ASCII));
  118. }
  119. /**
  120. * Writes raw text (in ISO-8859-1 encoding) to the output stream.
  121. * @param s the text
  122. * @throws IOException In case of an I/O error
  123. */
  124. public void writeText(String s) throws IOException {
  125. out.write(s.getBytes(ISO_8859_1));
  126. }
  127. /**
  128. * Formats a double value with two decimal positions for PCL output.
  129. *
  130. * @param value value to format
  131. * @return the formatted value
  132. */
  133. public final String formatDouble2(double value) {
  134. return df2.format(value);
  135. }
  136. /**
  137. * Formats a double value with four decimal positions for PCL output.
  138. *
  139. * @param value value to format
  140. * @return the formatted value
  141. */
  142. public final String formatDouble4(double value) {
  143. return df4.format(value);
  144. }
  145. /**
  146. * Sends the universal end of language command (UEL).
  147. * @throws IOException In case of an I/O error
  148. */
  149. public void universalEndOfLanguage() throws IOException {
  150. writeCommand("%-12345X");
  151. }
  152. /**
  153. * Resets the printer and restores the user default environment.
  154. * @throws IOException In case of an I/O error
  155. */
  156. public void resetPrinter() throws IOException {
  157. writeCommand("E");
  158. }
  159. /**
  160. * Sends the job separation command.
  161. * @throws IOException In case of an I/O error
  162. */
  163. public void separateJobs() throws IOException {
  164. writeCommand("&l1T");
  165. }
  166. /**
  167. * Sends the form feed character.
  168. * @throws IOException In case of an I/O error
  169. */
  170. public void formFeed() throws IOException {
  171. out.write(12); //=OC ("FF", Form feed)
  172. }
  173. /**
  174. * Sets the unit of measure.
  175. * @param value the resolution value (units per inch)
  176. * @throws IOException In case of an I/O error
  177. */
  178. public void setUnitOfMeasure(int value) throws IOException {
  179. writeCommand("&u" + value + "D");
  180. }
  181. /**
  182. * Sets the raster graphics resolution
  183. * @param value the resolution value (units per inch)
  184. * @throws IOException In case of an I/O error
  185. */
  186. public void setRasterGraphicsResolution(int value) throws IOException {
  187. writeCommand("*t" + value + "R");
  188. }
  189. /**
  190. * Selects the page size.
  191. * @param selector the integer representing the page size
  192. * @throws IOException In case of an I/O error
  193. */
  194. public void selectPageSize(int selector) throws IOException {
  195. writeCommand("&l" + selector + "A");
  196. }
  197. /**
  198. * Selects the paper source. The parameter is usually printer-specific. Usually, "1" is the
  199. * default tray, "2" is the manual paper feed, "3" is the manual envelope feed, "4" is the
  200. * "lower" tray and "7" is "auto-select". Consult the technical reference for your printer
  201. * for all available values.
  202. * @param selector the integer representing the paper source/tray
  203. * @throws IOException In case of an I/O error
  204. */
  205. public void selectPaperSource(int selector) throws IOException {
  206. writeCommand("&l" + selector + "H");
  207. }
  208. /**
  209. * Selects the output bin. The parameter is usually printer-specific. Usually, "1" is the
  210. * default output bin (upper bin) and "2" is the lower (rear) output bin. Some printers
  211. * may support additional output bins. Consult the technical reference for your printer
  212. * for all available values.
  213. * @param selector the integer representing the output bin
  214. * @throws IOException In case of an I/O error
  215. */
  216. public void selectOutputBin(int selector) throws IOException {
  217. writeCommand("&l" + selector + "G");
  218. }
  219. /**
  220. * Selects the duplexing mode for the page.
  221. * The parameter is usually printer-specific.
  222. * "0" means Simplex,
  223. * "1" means Duplex, Long-Edge Binding,
  224. * "2" means Duplex, Short-Edge Binding.
  225. * @param selector the integer representing the duplexing mode of the page
  226. * @throws IOException In case of an I/O error
  227. */
  228. public void selectDuplexMode(int selector) throws IOException {
  229. writeCommand("&l" + selector + "S");
  230. }
  231. /**
  232. * Clears the horizontal margins.
  233. * @throws IOException In case of an I/O error
  234. */
  235. public void clearHorizontalMargins() throws IOException {
  236. writeCommand("9");
  237. }
  238. /**
  239. * The Top Margin command designates the number of lines between
  240. * the top of the logical page and the top of the text area.
  241. * @param numberOfLines the number of lines (See PCL specification for details)
  242. * @throws IOException In case of an I/O error
  243. */
  244. public void setTopMargin(int numberOfLines) throws IOException {
  245. writeCommand("&l" + numberOfLines + "E");
  246. }
  247. /**
  248. * The Text Length command can be used to define the bottom border. See the PCL specification
  249. * for details.
  250. * @param numberOfLines the number of lines
  251. * @throws IOException In case of an I/O error
  252. */
  253. public void setTextLength(int numberOfLines) throws IOException {
  254. writeCommand("&l" + numberOfLines + "F");
  255. }
  256. /**
  257. * Sets the Vertical Motion Index (VMI).
  258. * @param value the VMI value
  259. * @throws IOException In case of an I/O error
  260. */
  261. public void setVMI(double value) throws IOException {
  262. writeCommand("&l" + formatDouble4(value) + "C");
  263. }
  264. /**
  265. * Sets the cursor to a new absolute coordinate.
  266. * @param x the X coordinate (in millipoints)
  267. * @param y the Y coordinate (in millipoints)
  268. * @throws IOException In case of an I/O error
  269. */
  270. public void setCursorPos(double x, double y) throws IOException {
  271. if (x < 0) {
  272. //A negative x value will result in a relative movement so go to "0" first.
  273. //But this will most probably have no effect anyway since you can't paint to the left
  274. //of the logical page
  275. writeCommand("&a0h" + formatDouble2(x / 100) + "h" + formatDouble2(y / 100) + "V");
  276. } else {
  277. writeCommand("&a" + formatDouble2(x / 100) + "h" + formatDouble2(y / 100) + "V");
  278. }
  279. }
  280. /**
  281. * Pushes the current cursor position on a stack (stack size: max 20 entries)
  282. * @throws IOException In case of an I/O error
  283. */
  284. public void pushCursorPos() throws IOException {
  285. writeCommand("&f0S");
  286. }
  287. /**
  288. * Pops the current cursor position from the stack.
  289. * @throws IOException In case of an I/O error
  290. */
  291. public void popCursorPos() throws IOException {
  292. writeCommand("&f1S");
  293. }
  294. /**
  295. * Changes the current print direction while maintaining the current cursor position.
  296. * @param rotate the rotation angle (counterclockwise), one of 0, 90, 180 and 270.
  297. * @throws IOException In case of an I/O error
  298. */
  299. public void changePrintDirection(int rotate) throws IOException {
  300. writeCommand("&a" + rotate + "P");
  301. }
  302. /**
  303. * Enters the HP GL/2 mode.
  304. * @param restorePreviousHPGL2Cursor true if the previous HP GL/2 pen position should be
  305. * restored, false if the current position is maintained
  306. * @throws IOException In case of an I/O error
  307. */
  308. public void enterHPGL2Mode(boolean restorePreviousHPGL2Cursor) throws IOException {
  309. if (restorePreviousHPGL2Cursor) {
  310. writeCommand("%0B");
  311. } else {
  312. writeCommand("%1B");
  313. }
  314. }
  315. /**
  316. * Enters the PCL mode.
  317. * @param restorePreviousPCLCursor true if the previous PCL cursor position should be restored,
  318. * false if the current position is maintained
  319. * @throws IOException In case of an I/O error
  320. */
  321. public void enterPCLMode(boolean restorePreviousPCLCursor) throws IOException {
  322. if (restorePreviousPCLCursor) {
  323. writeCommand("%0A");
  324. } else {
  325. writeCommand("%1A");
  326. }
  327. }
  328. /**
  329. * Generate a filled rectangle at the current cursor position.
  330. *
  331. * @param w the width in millipoints
  332. * @param h the height in millipoints
  333. * @param col the fill color
  334. * @throws IOException In case of an I/O error
  335. */
  336. protected void fillRect(int w, int h, Color col) throws IOException {
  337. if ((w == 0) || (h == 0)) {
  338. return;
  339. }
  340. if (h < 0) {
  341. h *= -1;
  342. } else {
  343. //y += h;
  344. }
  345. setPatternTransparencyMode(false);
  346. if (usePCLShades
  347. || Color.black.equals(col)
  348. || Color.white.equals(col)) {
  349. writeCommand("*c" + formatDouble4(w / 100.0) + "h"
  350. + formatDouble4(h / 100.0) + "V");
  351. int lineshade = convertToPCLShade(col);
  352. writeCommand("*c" + lineshade + "G");
  353. writeCommand("*c2P"); //Shaded fill
  354. } else {
  355. defineGrayscalePattern(col, 32, DitherUtil.DITHER_MATRIX_4X4);
  356. writeCommand("*c" + formatDouble4(w / 100.0) + "h"
  357. + formatDouble4(h / 100.0) + "V");
  358. writeCommand("*c32G");
  359. writeCommand("*c4P"); //User-defined pattern
  360. }
  361. // Reset pattern transparency mode.
  362. setPatternTransparencyMode(true);
  363. }
  364. /**
  365. * Generates a user-defined pattern for a dithering pattern matching the grayscale value
  366. * of the color given.
  367. * @param col the color to create the pattern for
  368. * @param patternID the pattern ID to use
  369. * @param ditherMatrixSize the size of the Bayer dither matrix to use (4 or 8 supported)
  370. * @throws IOException In case of an I/O error
  371. */
  372. public void defineGrayscalePattern(Color col, int patternID, int ditherMatrixSize)
  373. throws IOException {
  374. ByteArrayOutputStream baout = new ByteArrayOutputStream();
  375. DataOutputStream data = new DataOutputStream(baout);
  376. data.writeByte(0); //Format
  377. data.writeByte(0); //Continuation
  378. data.writeByte(1); //Pixel Encoding
  379. data.writeByte(0); //Reserved
  380. data.writeShort(8); //Width in Pixels
  381. data.writeShort(8); //Height in Pixels
  382. //data.writeShort(600); //X Resolution (didn't manage to get that to work)
  383. //data.writeShort(600); //Y Resolution
  384. int gray255 = convertToGray(col.getRed(), col.getGreen(), col.getBlue());
  385. byte[] pattern;
  386. if (ditherMatrixSize == 8) {
  387. pattern = DitherUtil.getBayerDither(DitherUtil.DITHER_MATRIX_8X8, gray255, false);
  388. } else {
  389. //Since a 4x4 pattern did not work, the 4x4 pattern is applied 4 times to an
  390. //8x8 pattern. Maybe this could be changed to use an 8x8 bayer dither pattern
  391. //instead of the 4x4 one.
  392. pattern = DitherUtil.getBayerDither(DitherUtil.DITHER_MATRIX_4X4, gray255, true);
  393. }
  394. data.write(pattern);
  395. if ((baout.size() % 2) > 0) {
  396. baout.write(0);
  397. }
  398. writeCommand("*c" + patternID + "G");
  399. writeCommand("*c" + baout.size() + "W");
  400. baout.writeTo(this.out);
  401. writeCommand("*c4Q"); //temporary pattern
  402. }
  403. /**
  404. * Sets the source transparency mode.
  405. * @param transparent true if transparent, false for opaque
  406. * @throws IOException In case of an I/O error
  407. */
  408. public void setSourceTransparencyMode(boolean transparent) throws IOException {
  409. setTransparencyMode(transparent, currentPatternTransparency);
  410. }
  411. /**
  412. * Sets the pattern transparency mode.
  413. * @param transparent true if transparent, false for opaque
  414. * @throws IOException In case of an I/O error
  415. */
  416. public void setPatternTransparencyMode(boolean transparent) throws IOException {
  417. setTransparencyMode(currentSourceTransparency, transparent);
  418. }
  419. /**
  420. * Sets the transparency modes.
  421. * @param source source transparency: true if transparent, false for opaque
  422. * @param pattern pattern transparency: true if transparent, false for opaque
  423. * @throws IOException In case of an I/O error
  424. */
  425. public void setTransparencyMode(boolean source, boolean pattern) throws IOException {
  426. if (source != currentSourceTransparency && pattern != currentPatternTransparency) {
  427. writeCommand("*v" + (source ? '0' : '1') + "n" + (pattern ? '0' : '1') + "O");
  428. } else if (source != currentSourceTransparency) {
  429. writeCommand("*v" + (source ? '0' : '1') + "N");
  430. } else if (pattern != currentPatternTransparency) {
  431. writeCommand("*v" + (pattern ? '0' : '1') + "O");
  432. }
  433. this.currentSourceTransparency = source;
  434. this.currentPatternTransparency = pattern;
  435. }
  436. /**
  437. * Convert an RGB color value to a grayscale from 0 to 100.
  438. * @param r the red component
  439. * @param g the green component
  440. * @param b the blue component
  441. * @return the gray value
  442. */
  443. public final int convertToGray(int r, int g, int b) {
  444. return BitmapImageUtil.convertToGray(r, g, b);
  445. }
  446. /**
  447. * Convert a Color value to a PCL shade value (0-100).
  448. * @param col the color
  449. * @return the PCL shade value (100=black)
  450. */
  451. public final int convertToPCLShade(Color col) {
  452. float gray = convertToGray(col.getRed(), col.getGreen(), col.getBlue()) / 255f;
  453. return (int)(100 - (gray * 100f));
  454. }
  455. /**
  456. * Selects the current grayscale color (the given color is converted to grayscales).
  457. * @param col the color
  458. * @throws IOException In case of an I/O error
  459. */
  460. public void selectGrayscale(Color col) throws IOException {
  461. if (Color.black.equals(col)) {
  462. selectCurrentPattern(0, 0); //black
  463. } else if (Color.white.equals(col)) {
  464. selectCurrentPattern(0, 1); //white
  465. } else {
  466. if (usePCLShades) {
  467. selectCurrentPattern(convertToPCLShade(col), 2);
  468. } else {
  469. defineGrayscalePattern(col, 32, DitherUtil.DITHER_MATRIX_4X4);
  470. selectCurrentPattern(32, 4);
  471. }
  472. }
  473. }
  474. /**
  475. * Select the current pattern
  476. * @param patternID the pattern ID (<ESC>*c#G command)
  477. * @param pattern the pattern type (<ESC>*v#T command)
  478. * @throws IOException In case of an I/O error
  479. */
  480. public void selectCurrentPattern(int patternID, int pattern) throws IOException {
  481. if (pattern > 1) {
  482. writeCommand("*c" + patternID + "G");
  483. }
  484. writeCommand("*v" + pattern + "T");
  485. }
  486. /**
  487. * Sets the dithering quality used when encoding gray or color images. If not explicitely
  488. * set a medium setting (0.5f) is used.
  489. * @param quality a quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest)
  490. */
  491. public void setDitheringQuality(float quality) {
  492. quality = Math.min(Math.max(0f, quality), 1.0f);
  493. this.ditheringQuality = quality;
  494. }
  495. /**
  496. * Returns the dithering quality used when encoding gray or color images.
  497. * @return the quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest)
  498. */
  499. public float getDitheringQuality() {
  500. return this.ditheringQuality;
  501. }
  502. /**
  503. * Indicates whether an image is a monochrome (b/w) image.
  504. * @param img the image
  505. * @return true if it's a monochrome image
  506. */
  507. public static boolean isMonochromeImage(RenderedImage img) {
  508. return BitmapImageUtil.isMonochromeImage(img);
  509. }
  510. /**
  511. * Indicates whether an image is a grayscale image.
  512. * @param img the image
  513. * @return true if it's a grayscale image
  514. */
  515. public static boolean isGrayscaleImage(RenderedImage img) {
  516. return BitmapImageUtil.isGrayscaleImage(img);
  517. }
  518. private static int jaiAvailable = -1; //no synchronization necessary, not critical
  519. /**
  520. * Indicates whether JAI is available. JAI has shown to be reliable when dithering a
  521. * grayscale or color image to monochrome bitmaps (1-bit).
  522. * @return true if JAI is available
  523. */
  524. public static boolean isJAIAvailable() {
  525. if (jaiAvailable < 0) {
  526. try {
  527. String clName = "javax.media.jai.JAI";
  528. Class.forName(clName);
  529. jaiAvailable = 1;
  530. } catch (ClassNotFoundException cnfe) {
  531. jaiAvailable = 0;
  532. }
  533. }
  534. return (jaiAvailable > 0);
  535. }
  536. private int calculatePCLResolution(int resolution) {
  537. return calculatePCLResolution(resolution, false);
  538. }
  539. /**
  540. * Calculates the ideal PCL resolution for a given resolution.
  541. * @param resolution the input resolution
  542. * @param increased true if you want to go to a higher resolution, for example if you
  543. * convert grayscale or color images to monochrome images so dithering has
  544. * a chance to generate better quality.
  545. * @return the resulting PCL resolution (one of 75, 100, 150, 200, 300, 600)
  546. */
  547. private int calculatePCLResolution(int resolution, boolean increased) {
  548. int choice = -1;
  549. for (int i = PCL_RESOLUTIONS.length - 2; i >= 0; i--) {
  550. if (resolution > PCL_RESOLUTIONS[i]) {
  551. int idx = i + 1;
  552. if (idx < PCL_RESOLUTIONS.length - 2) {
  553. idx += increased ? 2 : 0;
  554. } else if (idx < PCL_RESOLUTIONS.length - 1) {
  555. idx += increased ? 1 : 0;
  556. }
  557. choice = idx;
  558. break;
  559. //return PCL_RESOLUTIONS[idx];
  560. }
  561. }
  562. if (choice < 0) {
  563. choice = (increased ? 2 : 0);
  564. }
  565. while (choice > 0 && PCL_RESOLUTIONS[choice] > getMaximumBitmapResolution()) {
  566. choice--;
  567. }
  568. return PCL_RESOLUTIONS[choice];
  569. }
  570. private boolean isValidPCLResolution(int resolution) {
  571. return resolution == calculatePCLResolution(resolution);
  572. }
  573. //Threshold table to convert an alpha channel (8-bit) into a clip mask (1-bit)
  574. private static final byte[] THRESHOLD_TABLE = new byte[256];
  575. static { // Initialize the arrays
  576. for (int i = 0; i < 256; i++) {
  577. THRESHOLD_TABLE[i] = (byte) ((i < 240) ? 255 : 0);
  578. }
  579. }
  580. private RenderedImage getMask(RenderedImage img, Dimension targetDim) {
  581. ColorModel cm = img.getColorModel();
  582. if (cm.hasAlpha()) {
  583. BufferedImage alpha = new BufferedImage(img.getWidth(), img.getHeight(),
  584. BufferedImage.TYPE_BYTE_GRAY);
  585. Raster raster = img.getData();
  586. GraphicsUtil.copyBand(raster, cm.getNumColorComponents(), alpha.getRaster(), 0);
  587. BufferedImageOp op1 = new LookupOp(new ByteLookupTable(0, THRESHOLD_TABLE), null);
  588. BufferedImage alphat = op1.filter(alpha, null);
  589. BufferedImage mask;
  590. if (true) {
  591. mask = new BufferedImage(targetDim.width, targetDim.height,
  592. BufferedImage.TYPE_BYTE_BINARY);
  593. } else {
  594. byte[] arr = {(byte)0, (byte)0xff};
  595. ColorModel colorModel = new IndexColorModel(1, 2, arr, arr, arr);
  596. WritableRaster wraster = Raster.createPackedRaster(DataBuffer.TYPE_BYTE,
  597. targetDim.width, targetDim.height, 1, 1, null);
  598. mask = new BufferedImage(colorModel, wraster, false, null);
  599. }
  600. Graphics2D g2d = mask.createGraphics();
  601. try {
  602. AffineTransform at = new AffineTransform();
  603. double sx = targetDim.getWidth() / img.getWidth();
  604. double sy = targetDim.getHeight() / img.getHeight();
  605. at.scale(sx, sy);
  606. g2d.drawRenderedImage(alphat, at);
  607. } finally {
  608. g2d.dispose();
  609. }
  610. return mask;
  611. } else {
  612. return null;
  613. }
  614. }
  615. /**
  616. * Paint a bitmap at the current cursor position. The bitmap is converted to a monochrome
  617. * (1-bit) bitmap image.
  618. * @param img the bitmap image
  619. * @param targetDim the target Dimention (in mpt)
  620. * @param sourceTransparency true if the background should not be erased
  621. * @throws IOException In case of an I/O error
  622. */
  623. public void paintBitmap(RenderedImage img, Dimension targetDim, boolean sourceTransparency)
  624. throws IOException {
  625. double targetHResolution = img.getWidth() / UnitConv.mpt2in(targetDim.width);
  626. double targetVResolution = img.getHeight() / UnitConv.mpt2in(targetDim.height);
  627. double targetResolution = Math.max(targetHResolution, targetVResolution);
  628. int resolution = (int)Math.round(targetResolution);
  629. int effResolution = calculatePCLResolution(resolution, true);
  630. Dimension orgDim = new Dimension(img.getWidth(), img.getHeight());
  631. Dimension effDim;
  632. if (targetResolution == effResolution) {
  633. effDim = orgDim; //avoid scaling side-effects
  634. } else {
  635. effDim = new Dimension(
  636. (int)Math.ceil(UnitConv.mpt2px(targetDim.width, effResolution)),
  637. (int)Math.ceil(UnitConv.mpt2px(targetDim.height, effResolution)));
  638. }
  639. boolean scaled = !orgDim.equals(effDim);
  640. boolean monochrome = isMonochromeImage(img);
  641. if (!monochrome) {
  642. //Transparency mask disabled. Doesn't work reliably
  643. final boolean transparencyDisabled = true;
  644. RenderedImage mask = (transparencyDisabled ? null : getMask(img, effDim));
  645. if (mask != null) {
  646. pushCursorPos();
  647. selectCurrentPattern(0, 1); //Solid white
  648. setTransparencyMode(true, true);
  649. paintMonochromeBitmap(mask, effResolution);
  650. popCursorPos();
  651. }
  652. RenderedImage red = BitmapImageUtil.convertToMonochrome(
  653. img, effDim, this.ditheringQuality);
  654. selectCurrentPattern(0, 0); //Solid black
  655. setTransparencyMode(sourceTransparency || mask != null, true);
  656. paintMonochromeBitmap(red, effResolution);
  657. } else {
  658. RenderedImage effImg = img;
  659. if (scaled) {
  660. effImg = BitmapImageUtil.convertToMonochrome(img, effDim);
  661. }
  662. setSourceTransparencyMode(sourceTransparency);
  663. selectCurrentPattern(0, 0); //Solid black
  664. paintMonochromeBitmap(effImg, effResolution);
  665. }
  666. }
  667. private int toGray(int rgb) {
  668. // see http://www.jguru.com/faq/view.jsp?EID=221919
  669. double greyVal = 0.072169d * (rgb & 0xff);
  670. rgb >>= 8;
  671. greyVal += 0.715160d * (rgb & 0xff);
  672. rgb >>= 8;
  673. greyVal += 0.212671d * (rgb & 0xff);
  674. return (int)greyVal;
  675. }
  676. /**
  677. * Paint a bitmap at the current cursor position. The bitmap must be a monochrome
  678. * (1-bit) bitmap image.
  679. * @param img the bitmap image (must be 1-bit b/w)
  680. * @param resolution the resolution of the image (must be a PCL resolution)
  681. * @throws IOException In case of an I/O error
  682. */
  683. public void paintMonochromeBitmap(RenderedImage img, int resolution) throws IOException {
  684. if (!isValidPCLResolution(resolution)) {
  685. throw new IllegalArgumentException("Invalid PCL resolution: " + resolution);
  686. }
  687. boolean monochrome = isMonochromeImage(img);
  688. if (!monochrome) {
  689. throw new IllegalArgumentException("img must be a monochrome image");
  690. }
  691. setRasterGraphicsResolution(resolution);
  692. writeCommand("*r0f" + img.getHeight() + "t" + img.getWidth() + "s1A");
  693. Raster raster = img.getData();
  694. Encoder encoder = new Encoder(img);
  695. // Transfer graphics data
  696. int imgw = img.getWidth();
  697. IndexColorModel cm = (IndexColorModel)img.getColorModel();
  698. if (cm.getTransferType() == DataBuffer.TYPE_BYTE) {
  699. DataBufferByte dataBuffer = (DataBufferByte)raster.getDataBuffer();
  700. MultiPixelPackedSampleModel packedSampleModel = new MultiPixelPackedSampleModel(
  701. DataBuffer.TYPE_BYTE, img.getWidth(), img.getHeight(), 1);
  702. if (img.getSampleModel().equals(packedSampleModel)
  703. && dataBuffer.getNumBanks() == 1) {
  704. //Optimized packed encoding
  705. byte[] buf = dataBuffer.getData();
  706. int scanlineStride = packedSampleModel.getScanlineStride();
  707. int idx = 0;
  708. int c0 = toGray(cm.getRGB(0));
  709. int c1 = toGray(cm.getRGB(1));
  710. boolean zeroIsWhite = c0 > c1;
  711. for (int y = 0, maxy = img.getHeight(); y < maxy; y++) {
  712. for (int x = 0, maxx = scanlineStride; x < maxx; x++) {
  713. if (zeroIsWhite) {
  714. encoder.add8Bits(buf[idx]);
  715. } else {
  716. encoder.add8Bits((byte)~buf[idx]);
  717. }
  718. idx++;
  719. }
  720. encoder.endLine();
  721. }
  722. } else {
  723. //Optimized non-packed encoding
  724. for (int y = 0, maxy = img.getHeight(); y < maxy; y++) {
  725. byte[] line = (byte[])raster.getDataElements(0, y, imgw, 1, null);
  726. for (int x = 0, maxx = imgw; x < maxx; x++) {
  727. encoder.addBit(line[x] == 0);
  728. }
  729. encoder.endLine();
  730. }
  731. }
  732. } else {
  733. //Safe but slow fallback
  734. for (int y = 0, maxy = img.getHeight(); y < maxy; y++) {
  735. for (int x = 0, maxx = imgw; x < maxx; x++) {
  736. int sample = raster.getSample(x, y, 0);
  737. encoder.addBit(sample == 0);
  738. }
  739. encoder.endLine();
  740. }
  741. }
  742. // End raster graphics
  743. writeCommand("*rB");
  744. }
  745. private class Encoder {
  746. private int imgw;
  747. private int bytewidth;
  748. private byte[] rle; //compressed (RLE)
  749. private byte[] uncompressed; //uncompressed
  750. private int lastcount = -1;
  751. private byte lastbyte = 0;
  752. private int rlewidth = 0;
  753. private byte ib = 0; //current image bits
  754. private int x = 0;
  755. private boolean zeroRow = true;
  756. public Encoder(RenderedImage img) {
  757. imgw = img.getWidth();
  758. bytewidth = (imgw / 8);
  759. if ((imgw % 8) != 0) {
  760. bytewidth++;
  761. }
  762. rle = new byte[bytewidth * 2];
  763. uncompressed = new byte[bytewidth];
  764. }
  765. public void addBit(boolean bit) {
  766. //Set image bit for black
  767. if (bit) {
  768. ib |= 1;
  769. }
  770. //RLE encoding
  771. if ((x % 8) == 7 || ((x + 1) == imgw)) {
  772. finishedByte();
  773. } else {
  774. ib <<= 1;
  775. }
  776. x++;
  777. }
  778. public void add8Bits(byte b) {
  779. ib = b;
  780. finishedByte();
  781. x += 8;
  782. }
  783. private void finishedByte() {
  784. if (rlewidth < bytewidth) {
  785. if (lastcount >= 0) {
  786. if (ib == lastbyte) {
  787. lastcount++;
  788. } else {
  789. rle[rlewidth++] = (byte)(lastcount & 0xFF);
  790. rle[rlewidth++] = lastbyte;
  791. lastbyte = ib;
  792. lastcount = 0;
  793. }
  794. } else {
  795. lastbyte = ib;
  796. lastcount = 0;
  797. }
  798. if (lastcount == 255 || ((x + 1) == imgw)) {
  799. rle[rlewidth++] = (byte)(lastcount & 0xFF);
  800. rle[rlewidth++] = lastbyte;
  801. lastbyte = 0;
  802. lastcount = -1;
  803. }
  804. }
  805. uncompressed[x / 8] = ib;
  806. if (ib != 0) {
  807. zeroRow = false;
  808. }
  809. ib = 0;
  810. }
  811. public void endLine() throws IOException {
  812. if (zeroRow && PCLGenerator.this.currentSourceTransparency) {
  813. writeCommand("*b1Y");
  814. } else if (rlewidth < bytewidth) {
  815. writeCommand("*b1m" + rlewidth + "W");
  816. out.write(rle, 0, rlewidth);
  817. } else {
  818. writeCommand("*b0m" + bytewidth + "W");
  819. out.write(uncompressed);
  820. }
  821. lastcount = -1;
  822. rlewidth = 0;
  823. ib = 0;
  824. x = 0;
  825. zeroRow = true;
  826. }
  827. }
  828. }