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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  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.image.DataBuffer;
  22. import java.awt.image.DataBufferByte;
  23. import java.awt.image.IndexColorModel;
  24. import java.awt.image.MultiPixelPackedSampleModel;
  25. import java.awt.image.Raster;
  26. import java.awt.image.RenderedImage;
  27. import java.io.DataOutputStream;
  28. import java.io.IOException;
  29. import java.io.OutputStream;
  30. import java.text.DecimalFormat;
  31. import java.text.DecimalFormatSymbols;
  32. import java.util.Locale;
  33. import org.apache.commons.io.IOUtils;
  34. import org.apache.commons.io.output.ByteArrayOutputStream;
  35. import org.apache.xmlgraphics.util.UnitConv;
  36. import org.apache.fop.util.bitmap.BitmapImageUtil;
  37. import org.apache.fop.util.bitmap.DitherUtil;
  38. /**
  39. * This class provides methods for generating PCL print files.
  40. */
  41. public class PCLGenerator {
  42. private static final String US_ASCII = "US-ASCII";
  43. private static final String ISO_8859_1 = "ISO-8859-1";
  44. /** The ESC (escape) character */
  45. public static final char ESC = '\033';
  46. /** A list of all supported resolutions in PCL (values in dpi) */
  47. public static final int[] PCL_RESOLUTIONS = new int[] {75, 100, 150, 200, 300, 600};
  48. private final DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US);
  49. private final DecimalFormat df2 = new DecimalFormat("0.##", symbols);
  50. private final DecimalFormat df4 = new DecimalFormat("0.####", symbols);
  51. private final OutputStream out;
  52. private boolean currentSourceTransparency = true;
  53. private boolean currentPatternTransparency = true;
  54. private int maxBitmapResolution = PCL_RESOLUTIONS[PCL_RESOLUTIONS.length - 1];
  55. private float ditheringQuality = 0.5f;
  56. /**
  57. * true: Standard PCL shades are used (poor quality). false: user-defined pattern are used
  58. * to create custom dither patterns for better grayscale quality.
  59. */
  60. private static final boolean USE_PCL_SHADES = false;
  61. /**
  62. * Main constructor.
  63. * @param out the OutputStream to write the PCL stream to
  64. */
  65. public PCLGenerator(OutputStream out) {
  66. this.out = out;
  67. }
  68. /**
  69. * Main constructor.
  70. * @param out the OutputStream to write the PCL stream to
  71. * @param maxResolution the maximum resolution to encode bitmap images at
  72. */
  73. public PCLGenerator(OutputStream out, int maxResolution) {
  74. this(out);
  75. boolean found = false;
  76. for (int i = 0; i < PCL_RESOLUTIONS.length; i++) {
  77. if (PCL_RESOLUTIONS[i] == maxResolution) {
  78. found = true;
  79. break;
  80. }
  81. }
  82. if (!found) {
  83. throw new IllegalArgumentException("Illegal value for maximum resolution!");
  84. }
  85. this.maxBitmapResolution = maxResolution;
  86. }
  87. /** @return the OutputStream that this generator writes to */
  88. public OutputStream getOutputStream() {
  89. return this.out;
  90. }
  91. /**
  92. * Returns the currently active text encoding.
  93. * @return the text encoding
  94. */
  95. public String getTextEncoding() {
  96. return ISO_8859_1;
  97. }
  98. /** @return the maximum resolution to encode bitmap images at */
  99. public int getMaximumBitmapResolution() {
  100. return this.maxBitmapResolution;
  101. }
  102. /**
  103. * Writes a PCL escape command to the output stream.
  104. * @param cmd the command (without the ESCAPE character)
  105. * @throws IOException In case of an I/O error
  106. */
  107. public void writeCommand(String cmd) throws IOException {
  108. out.write(27); //ESC
  109. out.write(cmd.getBytes(US_ASCII));
  110. }
  111. /**
  112. * Writes raw text (in ISO-8859-1 encoding) to the output stream.
  113. * @param s the text
  114. * @throws IOException In case of an I/O error
  115. */
  116. public void writeText(String s) throws IOException {
  117. out.write(s.getBytes(ISO_8859_1));
  118. }
  119. /**
  120. * Writes raw bytes to the output stream
  121. * @param bytes The bytes
  122. * @throws IOException In case of an I/O error
  123. */
  124. public void writeBytes(byte[] bytes) throws IOException {
  125. out.write(bytes);
  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 (USE_PCL_SHADES
  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. IOUtils.closeQuietly(data);
  402. IOUtils.closeQuietly(baout);
  403. writeCommand("*c4Q"); //temporary pattern
  404. }
  405. /**
  406. * Sets the source transparency mode.
  407. * @param transparent true if transparent, false for opaque
  408. * @throws IOException In case of an I/O error
  409. */
  410. public void setSourceTransparencyMode(boolean transparent) throws IOException {
  411. setTransparencyMode(transparent, currentPatternTransparency);
  412. }
  413. /**
  414. * Sets the pattern transparency mode.
  415. * @param transparent true if transparent, false for opaque
  416. * @throws IOException In case of an I/O error
  417. */
  418. public void setPatternTransparencyMode(boolean transparent) throws IOException {
  419. setTransparencyMode(currentSourceTransparency, transparent);
  420. }
  421. /**
  422. * Sets the transparency modes.
  423. * @param source source transparency: true if transparent, false for opaque
  424. * @param pattern pattern transparency: true if transparent, false for opaque
  425. * @throws IOException In case of an I/O error
  426. */
  427. public void setTransparencyMode(boolean source, boolean pattern) throws IOException {
  428. if (source != currentSourceTransparency && pattern != currentPatternTransparency) {
  429. writeCommand("*v" + (source ? '0' : '1') + "n" + (pattern ? '0' : '1') + "O");
  430. } else if (source != currentSourceTransparency) {
  431. writeCommand("*v" + (source ? '0' : '1') + "N");
  432. } else if (pattern != currentPatternTransparency) {
  433. writeCommand("*v" + (pattern ? '0' : '1') + "O");
  434. }
  435. this.currentSourceTransparency = source;
  436. this.currentPatternTransparency = pattern;
  437. }
  438. /**
  439. * Convert an RGB color value to a grayscale from 0 to 100.
  440. * @param r the red component
  441. * @param g the green component
  442. * @param b the blue component
  443. * @return the gray value
  444. */
  445. public final int convertToGray(int r, int g, int b) {
  446. return BitmapImageUtil.convertToGray(r, g, b);
  447. }
  448. /**
  449. * Convert a Color value to a PCL shade value (0-100).
  450. * @param col the color
  451. * @return the PCL shade value (100=black)
  452. */
  453. public final int convertToPCLShade(Color col) {
  454. float gray = convertToGray(col.getRed(), col.getGreen(), col.getBlue()) / 255f;
  455. return (int)(100 - (gray * 100f));
  456. }
  457. /**
  458. * Selects the current grayscale color (the given color is converted to grayscales).
  459. * @param col the color
  460. * @throws IOException In case of an I/O error
  461. */
  462. public void selectGrayscale(Color col) throws IOException {
  463. if (Color.black.equals(col)) {
  464. selectCurrentPattern(0, 0); //black
  465. } else if (Color.white.equals(col)) {
  466. selectCurrentPattern(0, 1); //white
  467. } else {
  468. if (USE_PCL_SHADES) {
  469. selectCurrentPattern(convertToPCLShade(col), 2);
  470. } else {
  471. defineGrayscalePattern(col, 32, DitherUtil.DITHER_MATRIX_4X4);
  472. selectCurrentPattern(32, 4);
  473. }
  474. }
  475. }
  476. /**
  477. * Select the current pattern
  478. * @param patternID the pattern ID (<ESC>*c#G command)
  479. * @param pattern the pattern type (<ESC>*v#T command)
  480. * @throws IOException In case of an I/O error
  481. */
  482. public void selectCurrentPattern(int patternID, int pattern) throws IOException {
  483. if (pattern > 1) {
  484. writeCommand("*c" + patternID + "G");
  485. }
  486. writeCommand("*v" + pattern + "T");
  487. }
  488. /**
  489. * Sets the dithering quality used when encoding gray or color images. If not explicitely
  490. * set a medium setting (0.5f) is used.
  491. * @param quality a quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest)
  492. */
  493. public void setDitheringQuality(float quality) {
  494. quality = Math.min(Math.max(0f, quality), 1.0f);
  495. this.ditheringQuality = quality;
  496. }
  497. /**
  498. * Returns the dithering quality used when encoding gray or color images.
  499. * @return the quality setting between 0.0f (worst/fastest) and 1.0f (best/slowest)
  500. */
  501. public float getDitheringQuality() {
  502. return this.ditheringQuality;
  503. }
  504. /**
  505. * Indicates whether an image is a monochrome (b/w) image.
  506. * @param img the image
  507. * @return true if it's a monochrome image
  508. */
  509. public static boolean isMonochromeImage(RenderedImage img) {
  510. return BitmapImageUtil.isMonochromeImage(img);
  511. }
  512. /**
  513. * Indicates whether an image is a grayscale image.
  514. * @param img the image
  515. * @return true if it's a grayscale image
  516. */
  517. public static boolean isGrayscaleImage(RenderedImage img) {
  518. return BitmapImageUtil.isGrayscaleImage(img);
  519. }
  520. private static int jaiAvailable = -1; //no synchronization necessary, not critical
  521. /**
  522. * Indicates whether JAI is available. JAI has shown to be reliable when dithering a
  523. * grayscale or color image to monochrome bitmaps (1-bit).
  524. * @return true if JAI is available
  525. */
  526. public static boolean isJAIAvailable() {
  527. if (jaiAvailable < 0) {
  528. try {
  529. String clName = "javax.media.jai.JAI";
  530. Class.forName(clName);
  531. jaiAvailable = 1;
  532. } catch (ClassNotFoundException cnfe) {
  533. jaiAvailable = 0;
  534. }
  535. }
  536. return (jaiAvailable > 0);
  537. }
  538. private int calculatePCLResolution(int resolution) {
  539. return calculatePCLResolution(resolution, false);
  540. }
  541. /**
  542. * Calculates the ideal PCL resolution for a given resolution.
  543. * @param resolution the input resolution
  544. * @param increased true if you want to go to a higher resolution, for example if you
  545. * convert grayscale or color images to monochrome images so dithering has
  546. * a chance to generate better quality.
  547. * @return the resulting PCL resolution (one of 75, 100, 150, 200, 300, 600)
  548. */
  549. private int calculatePCLResolution(int resolution, boolean increased) {
  550. int choice = -1;
  551. for (int i = PCL_RESOLUTIONS.length - 2; i >= 0; i--) {
  552. if (resolution > PCL_RESOLUTIONS[i]) {
  553. int idx = i + 1;
  554. if (idx < PCL_RESOLUTIONS.length - 2) {
  555. idx += increased ? 2 : 0;
  556. } else if (idx < PCL_RESOLUTIONS.length - 1) {
  557. idx += increased ? 1 : 0;
  558. }
  559. choice = idx;
  560. break;
  561. //return PCL_RESOLUTIONS[idx];
  562. }
  563. }
  564. if (choice < 0) {
  565. choice = (increased ? 2 : 0);
  566. }
  567. while (choice > 0 && PCL_RESOLUTIONS[choice] > getMaximumBitmapResolution()) {
  568. choice--;
  569. }
  570. return PCL_RESOLUTIONS[choice];
  571. }
  572. private boolean isValidPCLResolution(int resolution) {
  573. return resolution == calculatePCLResolution(resolution);
  574. }
  575. //Threshold table to convert an alpha channel (8-bit) into a clip mask (1-bit)
  576. private static final byte[] THRESHOLD_TABLE = new byte[256];
  577. static { // Initialize the arrays
  578. for (int i = 0; i < 256; i++) {
  579. THRESHOLD_TABLE[i] = (byte) ((i < 240) ? 255 : 0);
  580. }
  581. }
  582. /* not used
  583. private RenderedImage getMask(RenderedImage img, Dimension targetDim) {
  584. ColorModel cm = img.getColorModel();
  585. if (cm.hasAlpha()) {
  586. BufferedImage alpha = new BufferedImage(img.getWidth(), img.getHeight(),
  587. BufferedImage.TYPE_BYTE_GRAY);
  588. Raster raster = img.getData();
  589. GraphicsUtil.copyBand(raster, cm.getNumColorComponents(), alpha.getRaster(), 0);
  590. BufferedImageOp op1 = new LookupOp(new ByteLookupTable(0, THRESHOLD_TABLE), null);
  591. BufferedImage alphat = op1.filter(alpha, null);
  592. BufferedImage mask;
  593. if (true) {
  594. mask = new BufferedImage(targetDim.width, targetDim.height,
  595. BufferedImage.TYPE_BYTE_BINARY);
  596. } else {
  597. byte[] arr = {(byte)0, (byte)0xff};
  598. ColorModel colorModel = new IndexColorModel(1, 2, arr, arr, arr);
  599. WritableRaster wraster = Raster.createPackedRaster(DataBuffer.TYPE_BYTE,
  600. targetDim.width, targetDim.height, 1, 1, null);
  601. mask = new BufferedImage(colorModel, wraster, false, null);
  602. }
  603. Graphics2D g2d = mask.createGraphics();
  604. try {
  605. AffineTransform at = new AffineTransform();
  606. double sx = targetDim.getWidth() / img.getWidth();
  607. double sy = targetDim.getHeight() / img.getHeight();
  608. at.scale(sx, sy);
  609. g2d.drawRenderedImage(alphat, at);
  610. } finally {
  611. g2d.dispose();
  612. }
  613. return mask;
  614. } else {
  615. return null;
  616. }
  617. }
  618. */
  619. /**
  620. * Paint a bitmap at the current cursor position. The bitmap is converted to a monochrome
  621. * (1-bit) bitmap image.
  622. * @param img the bitmap image
  623. * @param targetDim the target Dimention (in mpt)
  624. * @param sourceTransparency true if the background should not be erased
  625. * @throws IOException In case of an I/O error
  626. */
  627. public void paintBitmap(RenderedImage img, Dimension targetDim, boolean sourceTransparency)
  628. throws IOException {
  629. double targetHResolution = img.getWidth() / UnitConv.mpt2in(targetDim.width);
  630. double targetVResolution = img.getHeight() / UnitConv.mpt2in(targetDim.height);
  631. double targetResolution = Math.max(targetHResolution, targetVResolution);
  632. int resolution = (int)Math.round(targetResolution);
  633. int effResolution = calculatePCLResolution(resolution, true);
  634. Dimension orgDim = new Dimension(img.getWidth(), img.getHeight());
  635. Dimension effDim;
  636. if (targetResolution == effResolution) {
  637. effDim = orgDim; //avoid scaling side-effects
  638. } else {
  639. effDim = new Dimension(
  640. (int)Math.ceil(UnitConv.mpt2px(targetDim.width, effResolution)),
  641. (int)Math.ceil(UnitConv.mpt2px(targetDim.height, effResolution)));
  642. }
  643. boolean scaled = !orgDim.equals(effDim);
  644. boolean monochrome = isMonochromeImage(img);
  645. if (!monochrome) {
  646. //Transparency mask disabled. Doesn't work reliably
  647. /*
  648. final boolean transparencyDisabled = true;
  649. RenderedImage mask = (transparencyDisabled ? null : getMask(img, effDim));
  650. if (mask != null) {
  651. pushCursorPos();
  652. selectCurrentPattern(0, 1); //Solid white
  653. setTransparencyMode(true, true);
  654. paintMonochromeBitmap(mask, effResolution);
  655. popCursorPos();
  656. }
  657. */
  658. RenderedImage red = BitmapImageUtil.convertToMonochrome(
  659. img, effDim, this.ditheringQuality);
  660. selectCurrentPattern(0, 0); //Solid black
  661. setTransparencyMode(sourceTransparency /*|| mask != null*/, true);
  662. paintMonochromeBitmap(red, effResolution);
  663. } else {
  664. RenderedImage effImg = img;
  665. if (scaled) {
  666. effImg = BitmapImageUtil.convertToMonochrome(img, effDim);
  667. }
  668. setSourceTransparencyMode(sourceTransparency);
  669. selectCurrentPattern(0, 0); //Solid black
  670. paintMonochromeBitmap(effImg, effResolution);
  671. }
  672. }
  673. private int toGray(int rgb) {
  674. // see http://www.jguru.com/faq/view.jsp?EID=221919
  675. double greyVal = 0.072169d * (rgb & 0xff);
  676. rgb >>= 8;
  677. greyVal += 0.715160d * (rgb & 0xff);
  678. rgb >>= 8;
  679. greyVal += 0.212671d * (rgb & 0xff);
  680. return (int)greyVal;
  681. }
  682. /**
  683. * Paint a bitmap at the current cursor position. The bitmap must be a monochrome
  684. * (1-bit) bitmap image.
  685. * @param img the bitmap image (must be 1-bit b/w)
  686. * @param resolution the resolution of the image (must be a PCL resolution)
  687. * @throws IOException In case of an I/O error
  688. */
  689. public void paintMonochromeBitmap(RenderedImage img, int resolution) throws IOException {
  690. if (!isValidPCLResolution(resolution)) {
  691. throw new IllegalArgumentException("Invalid PCL resolution: " + resolution);
  692. }
  693. boolean monochrome = isMonochromeImage(img);
  694. if (!monochrome) {
  695. throw new IllegalArgumentException("img must be a monochrome image");
  696. }
  697. setRasterGraphicsResolution(resolution);
  698. writeCommand("*r0f" + img.getHeight() + "t" + img.getWidth() + "s1A");
  699. Raster raster = img.getData();
  700. Encoder encoder = new Encoder(img);
  701. // Transfer graphics data
  702. int imgw = img.getWidth();
  703. IndexColorModel cm = (IndexColorModel)img.getColorModel();
  704. if (cm.getTransferType() == DataBuffer.TYPE_BYTE) {
  705. DataBufferByte dataBuffer = (DataBufferByte)raster.getDataBuffer();
  706. MultiPixelPackedSampleModel packedSampleModel = new MultiPixelPackedSampleModel(
  707. DataBuffer.TYPE_BYTE, img.getWidth(), img.getHeight(), 1);
  708. if (img.getSampleModel().equals(packedSampleModel)
  709. && dataBuffer.getNumBanks() == 1) {
  710. //Optimized packed encoding
  711. byte[] buf = dataBuffer.getData();
  712. int scanlineStride = packedSampleModel.getScanlineStride();
  713. int idx = 0;
  714. int c0 = toGray(cm.getRGB(0));
  715. int c1 = toGray(cm.getRGB(1));
  716. boolean zeroIsWhite = c0 > c1;
  717. for (int y = 0, maxy = img.getHeight(); y < maxy; y++) {
  718. for (int x = 0, maxx = scanlineStride; x < maxx; x++) {
  719. if (zeroIsWhite) {
  720. encoder.add8Bits(buf[idx]);
  721. } else {
  722. encoder.add8Bits((byte)~buf[idx]);
  723. }
  724. idx++;
  725. }
  726. encoder.endLine();
  727. }
  728. } else {
  729. //Optimized non-packed encoding
  730. for (int y = 0, maxy = img.getHeight(); y < maxy; y++) {
  731. byte[] line = (byte[])raster.getDataElements(0, y, imgw, 1, null);
  732. for (int x = 0, maxx = imgw; x < maxx; x++) {
  733. encoder.addBit(line[x] == 0);
  734. }
  735. encoder.endLine();
  736. }
  737. }
  738. } else {
  739. //Safe but slow fallback
  740. for (int y = 0, maxy = img.getHeight(); y < maxy; y++) {
  741. for (int x = 0, maxx = imgw; x < maxx; x++) {
  742. int sample = raster.getSample(x, y, 0);
  743. encoder.addBit(sample == 0);
  744. }
  745. encoder.endLine();
  746. }
  747. }
  748. // End raster graphics
  749. writeCommand("*rB");
  750. }
  751. private class Encoder {
  752. private int imgw;
  753. private int bytewidth;
  754. private byte[] rle; //compressed (RLE)
  755. private byte[] uncompressed; //uncompressed
  756. private int lastcount = -1;
  757. private byte lastbyte;
  758. private int rlewidth;
  759. private byte ib; //current image bits
  760. private int x;
  761. private boolean zeroRow = true;
  762. public Encoder(RenderedImage img) {
  763. imgw = img.getWidth();
  764. bytewidth = (imgw / 8);
  765. if ((imgw % 8) != 0) {
  766. bytewidth++;
  767. }
  768. rle = new byte[bytewidth * 2];
  769. uncompressed = new byte[bytewidth];
  770. }
  771. public void addBit(boolean bit) {
  772. //Set image bit for black
  773. if (bit) {
  774. ib |= 1;
  775. }
  776. //RLE encoding
  777. if ((x % 8) == 7 || ((x + 1) == imgw)) {
  778. finishedByte();
  779. } else {
  780. ib <<= 1;
  781. }
  782. x++;
  783. }
  784. public void add8Bits(byte b) {
  785. ib = b;
  786. finishedByte();
  787. x += 8;
  788. }
  789. private void finishedByte() {
  790. if (rlewidth < bytewidth) {
  791. if (lastcount >= 0) {
  792. if (ib == lastbyte) {
  793. lastcount++;
  794. } else {
  795. rle[rlewidth++] = (byte)(lastcount & 0xFF);
  796. rle[rlewidth++] = lastbyte;
  797. lastbyte = ib;
  798. lastcount = 0;
  799. }
  800. } else {
  801. lastbyte = ib;
  802. lastcount = 0;
  803. }
  804. if (lastcount == 255 || ((x + 1) == imgw)) {
  805. rle[rlewidth++] = (byte)(lastcount & 0xFF);
  806. rle[rlewidth++] = lastbyte;
  807. lastbyte = 0;
  808. lastcount = -1;
  809. }
  810. }
  811. uncompressed[x / 8] = ib;
  812. if (ib != 0) {
  813. zeroRow = false;
  814. }
  815. ib = 0;
  816. }
  817. public void endLine() throws IOException {
  818. if (zeroRow && PCLGenerator.this.currentSourceTransparency) {
  819. writeCommand("*b1Y");
  820. } else if (rlewidth < bytewidth) {
  821. writeCommand("*b1m" + rlewidth + "W");
  822. out.write(rle, 0, rlewidth);
  823. } else {
  824. writeCommand("*b0m" + bytewidth + "W");
  825. out.write(uncompressed);
  826. }
  827. lastcount = -1;
  828. rlewidth = 0;
  829. ib = 0;
  830. x = 0;
  831. zeroRow = true;
  832. }
  833. }
  834. }