001package jmri.util.davidflanagan; 002 003import java.awt.*; 004import java.awt.event.ActionEvent; 005import java.awt.font.FontRenderContext; 006import java.awt.font.LineMetrics; 007import java.awt.geom.AffineTransform; 008import java.awt.geom.Rectangle2D; 009import java.awt.image.BufferedImage; 010import java.awt.print.*; 011import java.io.IOException; 012import java.io.Writer; 013import java.text.DateFormat; 014import java.util.*; 015import java.util.List; 016 017import javax.print.attribute.*; 018import javax.print.attribute.standard.MediaPrintableArea; 019import javax.print.attribute.standard.OrientationRequested; 020import javax.swing.*; 021import javax.swing.border.EmptyBorder; 022 023import org.python.google.common.annotations.VisibleForTesting; 024 025import jmri.util.JmriJFrame; 026import jmri.util.PaperUtils; 027 028/** 029 * Provide graphic output to a screen/printer. 030 * <p> 031 * This is from Chapter 12 of the O'Reilly Java book by David Flanagan with the 032 * alligator on the front. 033 * <p> 034 * This has been extensively modified by Philip Gladstone to improve the print 035 * preview functionality and to switch to using the Java 1.8 (maybe 1.2) 036 * printing classes. The original code used the Java 1.1 printing classes. 037 * 038 * @author David Flanagan 039 * @author Dennis Miller 040 * @author Philip Gladstone 041 */ 042public class HardcopyWriter extends Writer implements Printable { 043 044 // instance variables 045 protected PrinterJob printerJob; 046 protected PageFormat pageFormat; 047 protected Graphics2D printJobGraphics; 048 protected Graphics2D page; 049 protected String jobname; 050 protected String line = ""; 051 protected int useFontSize = 7; 052 protected String time; 053 protected Dimension pagesizePixels; 054 protected Dimension pagesizePoints; 055 protected Font font, headerfont; 056 protected String useFontName = "Monospaced"; 057 protected boolean isMonospacedFont = true; 058 protected int useFontStyle = Font.PLAIN; 059 protected FontRenderContext neutralFRC; 060 protected FontRenderContext actualFRC; 061 protected FontMetrics headermetrics; 062 protected int x0, y0; 063 protected int height, width; 064 protected int width_including_right_margin; 065 protected int headery; 066 protected double titleTop; // Points down from top of page 067 protected double leftMargin; 068 protected float charwidth; 069 protected float lineheight; 070 protected float lineascent; 071 protected float v_pos = 0; // The offset of the current line from the top margin. 072 protected float max_v_pos = 0; // The maximum offset of the current line from the top margin. 073 protected int pagenum = 0; 074 protected Color color = Color.black; 075 protected boolean printHeader = true; 076 077 protected boolean isPreview; 078 protected BufferedImage previewImage; 079 protected Vector<BufferedImage> pageImages = new Vector<>(3, 3); 080 protected JmriJFrame previewFrame; 081 protected JPanel previewPanel; 082 protected ImageIcon previewIcon = new ImageIcon(); 083 protected JLabel previewLabel = new JLabel(); 084 protected JToolBar previewToolBar = new JToolBar(); 085 protected JButton nextButton; 086 protected JButton previousButton; 087 protected JButton closeButton; 088 protected JLabel pageCount = new JLabel(); 089 090 protected Column[] columns = {new Column(0, Integer.MAX_VALUE, Align.LEFT_WRAP)}; 091 protected int columnIndex = 0; 092 093 protected double pixelScale = 1; 094 protected Integer screenResolution; 095 096 protected List<List<PrintCommand>> pageCommands = new ArrayList<>(); 097 protected List<PrintCommand> currentPageCommands; 098 099 // save state between invocations of write() 100 private boolean last_char_was_return = false; 101 102 public static int DPI = 72; 103 public static String NO_PRINTING_PRINTER = "skipDialog"; 104 105 // Job and Page attributes 106 PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet(); 107 108 // constructor modified to add default printer name, page orientation, print header, print duplex, and page size 109 // All length parameters are in points. 110 // various combinations of parameters can be set. The paper size/shape is set by the pagesize 111 // parameter unless it is null, in which case the specified printer paper size is used. 112 // The page orientation is set by the isLandscape parameter unless it is null, in which 113 // case the specified printer page orientation is used. 114 /** 115 * Constructor for HardcopyWriter 116 * 117 * @param frame The AWT Frame (only required for preview mode) -- 118 * this is used to get the screen resolution. 119 * @param jobname The name to print in the title of the page 120 * @param fontName The name of the font to use (if null, default is 121 * used) 122 * @param fontStyle The style of the font to use (if null, default is 123 * used) 124 * @param fontsize The size of the font to use (if null, default is 125 * used) 126 * @param leftmargin The left margin in points 127 * @param rightmargin The right margin in points 128 * @param topmargin The top margin in points 129 * @param bottommargin The bottom margin in points 130 * @param isPreview Whether to preview the print job 131 * @param printerName The name of the printer to use (if null, default is 132 * used) 133 * @param isLandscape Whether to print in landscape mode (if null, default 134 * is used) 135 * @param isPrintHeader Whether to print the header (if null, default is 136 * used) 137 * @param sides The type of duplexing to use (if null, default is 138 * used) 139 * @param pagesize The size of the page to use (if null, default is 140 * used) 141 * @throws HardcopyWriter.PrintCanceledException If the print job gets 142 * cancelled. 143 */ 144 public HardcopyWriter(Frame frame, String jobname, String fontName, Integer fontStyle, Integer fontsize, 145 double leftmargin, double rightmargin, 146 double topmargin, double bottommargin, boolean isPreview, String printerName, Boolean isLandscape, 147 Boolean isPrintHeader, Attribute sides, Dimension pagesize) 148 throws HardcopyWriter.PrintCanceledException { 149 150 initalize(frame, jobname, fontName, fontStyle, fontsize, leftmargin, rightmargin, topmargin, bottommargin, 151 isPreview, printerName, isLandscape, isPrintHeader, sides, pagesize); 152 } 153 154 // this constructor is only used to determine number of character per line 155 public HardcopyWriter(String fontName, Integer fontStyle, Integer fontsize, double leftmargin, double rightmargin, 156 double topmargin, double bottommargin, Boolean isLandscape, Dimension pagesize) 157 throws HardcopyWriter.PrintCanceledException { 158 initalize(null, "", fontName, fontStyle, fontsize, leftmargin, rightmargin, topmargin, bottommargin, 159 false, NO_PRINTING_PRINTER, isLandscape, false, null, pagesize); 160 } 161 162 private void initalize(Frame frame, String jobname, String fontName, Integer fontStyle, Integer fontsize, 163 double leftmargin, double rightmargin, 164 double topmargin, double bottommargin, boolean isPreview, String printerName, Boolean isLandscape, 165 Boolean isPrintHeader, Attribute sides, Dimension pagesize) 166 throws HardcopyWriter.PrintCanceledException { 167 168 neutralFRC = new FontRenderContext( 169 new AffineTransform(), // No scaling/rotation 170 true, // Anti-aliasing 171 true // Fractional metrics 172 ); 173 174 if (isPreview) { 175 GraphicsConfiguration gc = frame.getGraphicsConfiguration(); 176 AffineTransform at = gc.getDefaultTransform(); 177 pixelScale = at.getScaleX(); 178 } 179 180 // print header? 181 if (isPrintHeader != null) { 182 this.printHeader = isPrintHeader; 183 } 184 185 this.isPreview = isPreview; 186 187 // Get the screen resolution and cache it. This also allows us to override 188 // the default resolution for testing purposes. 189 if (frame == null) { 190 screenResolution = 100; 191 } else { 192 getScreenResolution(); 193 } 194 195 if (pagesize == null) { 196 pagesizePixels = getPagesizePixels(); 197 pagesizePoints = getPagesizePoints(); 198 } else { 199 pixelScale = 1; 200 pagesizePoints = pagesize; 201 // Assume 100 DPI scale factor. This is used for testing only. If !isPreview, then things 202 // are set according to the printer's capabilities. 203 screenResolution = 100; 204 pagesizePixels = new Dimension(pagesizePoints.width * screenResolution / DPI, 205 pagesizePoints.height * screenResolution / DPI); 206 } 207 208 // Save this so that if we print, then we can keep printed stuff out of 209 // the left margin. 210 this.leftMargin = leftmargin; 211 212 // skip printer selection if preview 213 if (!isPreview) { 214 printerJob = PrinterJob.getPrinterJob(); 215 printerJob.setJobName(jobname); 216 printerJob.setPrintable(this); 217 218 PageFormat pageFormat = printerJob.defaultPage(); 219 220 if (isLandscape != null) { 221 pageFormat.setOrientation(isLandscape ? PageFormat.LANDSCAPE : PageFormat.PORTRAIT); 222 attributes.add(isLandscape ? OrientationRequested.LANDSCAPE : OrientationRequested.PORTRAIT); 223 } 224 225 if (sides != null) { 226 attributes.add(sides); 227 } 228 229 Paper paper = new Paper(); 230 if (pagesize != null) { 231 paper.setSize(pagesize.width, pagesize.height); 232 paper.setImageableArea(0, 0, pagesize.width, pagesize.height); 233 pageFormat.setPaper(paper); 234 } else { 235 // Use the default page size but set the imageable area to the full page size 236 paper = pageFormat.getPaper(); 237 paper.setImageableArea(0, 0, paper.getWidth(), paper.getHeight()); 238 pageFormat.setPaper(paper); 239 } 240 241 attributes.add(new MediaPrintableArea(0.0f, 0.0f, (float) paper.getWidth(), (float) paper.getHeight(), 242 MediaPrintableArea.INCH)); 243 244 printerJob.setPrintable(this, pageFormat); 245 246 if (NO_PRINTING_PRINTER.equals(printerName) || printerJob.printDialog(attributes)) { 247 PageFormat updatedPf = printerJob.validatePage(pageFormat); 248 249 double widthPts = updatedPf.getPaper().getWidth(); 250 double heightPts = updatedPf.getPaper().getHeight(); 251 int orientation = updatedPf.getOrientation(); 252 253 if (orientation == PageFormat.LANDSCAPE) { 254 double temp = widthPts; 255 widthPts = heightPts; 256 heightPts = temp; 257 } 258 259 pagesizePoints = new Dimension((int) Math.round(widthPts), (int) Math.round(heightPts)); 260 // For PrinterJob, we often work in points directly. 261 // We'll calculate pagesizePixels for compatibility if needed. 262 pagesizePixels = new Dimension((int) (pagesizePoints.width * getScreenResolution() / 72.0), 263 (int) (pagesizePoints.height * getScreenResolution() / 72.0)); 264 265 if (NO_PRINTING_PRINTER.equals(printerName)) { 266 printerJob = null; 267 } 268 } else { 269 throw new PrintCanceledException("User cancelled print request"); 270 } 271 } else { 272 if (isLandscape != null && isLandscape) { 273 pagesizePoints = new Dimension(pagesizePoints.height, pagesizePoints.width); 274 pagesizePixels = new Dimension(pagesizePixels.height, pagesizePixels.width); 275 } 276 } 277 278 x0 = (int) leftmargin; 279 y0 = (int) topmargin; 280 width = pagesizePoints.width - (int) (leftmargin + rightmargin); 281 height = pagesizePoints.height - (int) (topmargin + bottommargin); 282 283 // Create a graphics context that we can use to get font metrics 284 Graphics2D g = getGraphics(); 285 286 actualFRC = g.getFontRenderContext(); 287 288 if (fontsize != null) { 289 useFontSize = fontsize; 290 } 291 292 if (fontName != null) { 293 useFontName = fontName; 294 } 295 296 if (fontStyle != null) { 297 useFontStyle = fontStyle; 298 } 299 300 // get body font and font size 301 font = new Font(useFontName, useFontStyle, useFontSize); 302 g.setFont(font); 303 refreshMetrics(); 304 305 // header font info 306 headerfont = new Font("SansSerif", Font.ITALIC, useFontSize); 307 headermetrics = g.getFontMetrics(headerfont); 308 headery = y0 - (int) (0.125 * DPI) - headermetrics.getHeight() + headermetrics.getAscent(); 309 titleTop = headery - headermetrics.getAscent(); 310 311 // compute date/time for header 312 DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT); 313 df.setTimeZone(TimeZone.getDefault()); 314 time = df.format(new Date()); 315 316 this.jobname = jobname; 317 318 if (isPreview) { 319 previewFrame = new JmriJFrame(Bundle.getMessage("PrintPreviewTitle") + " " + jobname); 320 previewFrame.getContentPane().setLayout(new BorderLayout()); 321 toolBarInit(); 322 previewToolBar.setFloatable(false); 323 previewFrame.getContentPane().add(previewToolBar, BorderLayout.NORTH); 324 previewPanel = new JPanel(); 325 previewPanel.setSize((int) (pagesizePixels.width / pixelScale), (int) (pagesizePixels.height / pixelScale)); 326 // add the panel to the frame and make visible, otherwise creating the image will fail. 327 // use a scroll pane to handle print images bigger than the window 328 previewFrame.getContentPane().add(new JScrollPane(previewPanel), BorderLayout.CENTER); 329 330 previewFrame.setSize((int) (pagesizePixels.width / pixelScale) + 48, 331 (int) (pagesizePixels.height / pixelScale) + 100); 332 previewFrame.setVisible(true); 333 } 334 } 335 336 /** 337 * Get a graphics context for the current page (or the print job graphics 338 * context if available) Make sure that this is setup with the appropriate 339 * scale factor for the current page. 340 * 341 * @return the graphics context 342 */ 343 private Graphics2D getGraphics() { 344 Graphics2D g = page; 345 346 if (g != null) { 347 return g; 348 } 349 350 BufferedImage img = new BufferedImage(pagesizePixels.width, pagesizePixels.height, BufferedImage.TYPE_INT_RGB); 351 g = img.createGraphics(); 352 if (g == null) { 353 throw new RuntimeException("Could not get graphics context"); 354 } 355 setupGraphics(g, true); 356 return g; 357 } 358 359 private void record(PrintCommand cmd) { 360 currentPageCommands.add(cmd); 361 cmd.execute(page); 362 } 363 364 /** 365 * Create a print preview toolbar. 366 */ 367 protected void toolBarInit() { 368 previousButton = new JButton(Bundle.getMessage("ButtonPreviousPage")); 369 previewToolBar.add(previousButton); 370 previousButton.addActionListener((ActionEvent actionEvent) -> { 371 pagenum--; 372 displayPage(); 373 }); 374 nextButton = new JButton(Bundle.getMessage("ButtonNextPage")); 375 previewToolBar.add(nextButton); 376 nextButton.addActionListener((ActionEvent actionEvent) -> { 377 pagenum++; 378 displayPage(); 379 }); 380 pageCount = new JLabel(Bundle.getMessage("HeaderPageNum", pagenum, pageImages.size())); 381 pageCount.setBorder(new EmptyBorder(0, 10, 0, 10)); 382 previewToolBar.add(pageCount); 383 closeButton = new JButton(Bundle.getMessage("ButtonClose")); 384 previewToolBar.add(closeButton); 385 closeButton.addActionListener((ActionEvent actionEvent) -> { 386 if (page != null) { 387 page.dispose(); 388 } 389 previewFrame.dispose(); 390 }); 391 392 // We want to add the paper size / orientation 393 Dimension mediaSize = pagesizePoints; 394 if (pagesizePixels.width > pagesizePixels.height) { 395 JLabel orientationLabel = new JLabel(Bundle.getMessage("Landscape")); 396 orientationLabel.setBorder(new EmptyBorder(0, 10, 0, 0)); 397 previewToolBar.add(orientationLabel); 398 mediaSize = new Dimension(pagesizePoints.height, pagesizePoints.width); 399 } else { 400 JLabel orientationLabel = new JLabel(Bundle.getMessage("Portrait")); 401 orientationLabel.setBorder(new EmptyBorder(0, 10, 0, 0)); 402 previewToolBar.add(orientationLabel); 403 } 404 String paperSizeName = PaperUtils.getNameFromPoints(mediaSize.width, mediaSize.height); 405 if (paperSizeName != null) { 406 try { 407 // This converts the paper size name to the appropriate locale 408 // but we don't actually know all the possible paper size names. 409 paperSizeName = Bundle.getMessage(paperSizeName); 410 } catch (MissingResourceException e) { 411 log.debug("Paper size name {} not found", paperSizeName); 412 } 413 JLabel paperSizeLabel = new JLabel(paperSizeName); 414 paperSizeLabel.setBorder(new EmptyBorder(0, 5, 0, 10)); 415 previewToolBar.add(paperSizeLabel); 416 } 417 } 418 419 /** 420 * Display a page image in the preview pane. 421 * <p> 422 * Not part of the original HardcopyWriter class. 423 */ 424 protected void displayPage() { 425 // limit the pages to the actual range 426 if (pagenum > pageImages.size()) { 427 pagenum = pageImages.size(); 428 } 429 if (pagenum < 1) { 430 pagenum = 1; 431 } 432 // enable/disable the previous/next buttons as appropriate 433 previousButton.setEnabled(true); 434 nextButton.setEnabled(true); 435 if (pagenum == pageImages.size()) { 436 nextButton.setEnabled(false); 437 } 438 if (pagenum == 1) { 439 previousButton.setEnabled(false); 440 } 441 previewImage = pageImages.elementAt(pagenum - 1); 442 previewFrame.setVisible(false); 443 // previewIcon.setImage(previewImage); 444 previewLabel.setIcon(new RetinaIcon(previewImage, pixelScale)); 445 // put the label in the panel (already has a scroll pane) 446 previewPanel.add(previewLabel); 447 // set the page count info 448 pageCount.setText(Bundle.getMessage("HeaderPageNum", pagenum, pageImages.size())); 449 // repaint the frame but don't use pack() as we don't want resizing 450 previewFrame.invalidate(); 451 previewFrame.revalidate(); 452 previewFrame.setVisible(true); 453 } 454 455 /** 456 * This function measures the size of the text in the current font. It 457 * returns a Rectangle2D. 458 * 459 * @param s The string to be measured (no tabs allowed) 460 * @return The Rectangle2D object in points 461 */ 462 463 public Rectangle2D measure(String s) { 464 Rectangle2D bounds = font.getStringBounds(s, neutralFRC); 465 return bounds; 466 } 467 468 /** 469 * This function returns the bounding box that includes all of the strings 470 * passed (when printed on top of each other) 471 * 472 * @param stringList A collection of Strings 473 * @return The Rectangle2D object in points 474 */ 475 476 public Rectangle2D measure(Collection<String> stringList) { 477 Rectangle2D bounds = null; 478 for (String s : stringList) { 479 Rectangle2D b = measure(s); 480 if (bounds == null) { 481 bounds = b; 482 } else { 483 bounds.add(b); 484 } 485 } 486 return bounds; 487 } 488 489 /** 490 * Get the current page size in points (logical units). 491 * 492 * @return The printable page area in points 493 */ 494 public Dimension getPrintablePagesizePoints() { 495 return new Dimension(width, height); 496 } 497 498 /** 499 * Function to get the current page size if this is a preview. This is the 500 * pagesize in points (logical units). If this is not a preview, it still 501 * returns the page size for the display. It makes use of the PaperUtils 502 * class to get the default paper size (based on locale and/or printer 503 * settings). 504 * 505 * @return The page size in points 506 */ 507 private Dimension getPagesizePoints() { 508 return PaperUtils.getPaperSizeDimension(); 509 } 510 511 /** 512 * Get the screen resolution in pixels per inch. It caches this so that a 513 * single value is used for the entire run of the preview/print. 514 * 515 * @return The screen resolution in pixels per inch. 516 */ 517 private int getScreenResolution() { 518 if (screenResolution == null) { 519 screenResolution = (int) (Toolkit.getDefaultToolkit().getScreenResolution() * pixelScale); 520 } 521 return screenResolution; 522 } 523 524 /** 525 * Function to get the current page size if this is a preview. This is the 526 * pagesize in pixels (and not points). If this is not a preview, it still 527 * returns the page size for the display. 528 * 529 * @return The page size in pixels 530 */ 531 private Dimension getPagesizePixels() { 532 int dpi = getScreenResolution(); 533 Dimension pagesizePoints = getPagesizePoints(); 534 return new Dimension(pagesizePoints.width * dpi / DPI, pagesizePoints.height * dpi / DPI); 535 } 536 537 /** 538 * Function to set the columns for future text output. Output starts in 539 * first first column, and advances to the next column on a tab character. 540 * This either causes truncation of the text or wrapping to the next line 541 * depending on the column alignment type. 542 * <p> 543 * If no columns are set, then the default is one column, left aligned with 544 * the full width of the page 545 * </p> 546 * 547 * @param columns Array of Column objects 548 */ 549 public void setColumns(Column[] columns) { 550 for (int i = 0; i < columns.length; i++) { 551 if (columns[i].maxWidth) { 552 if (i + 1 < columns.length) { 553 columns[i].width = columns[i + 1].position - columns[i].position; 554 } else { 555 columns[i].width = width - columns[i].position; 556 } 557 } 558 if (columns[i].getPosition() + columns[i].getWidth() > width) { 559 throw new IllegalArgumentException("Column off edge of page"); 560 } 561 } 562 if (columns.length == 0) { 563 columns = new Column[]{new Column(0, width, Align.LEFT_WRAP)}; 564 } 565 this.columns = columns; 566 } 567 568 /** 569 * Function to set Columns based on a {@code Collection<Column>} object 570 * <p> 571 * If no columns are set, then the default is one column, left aligned with 572 * the full width of the page 573 * </p> 574 * 575 * @param columns Collection of Column objects 576 */ 577 public void setColumns(Collection<Column> columns) { 578 setColumns(columns.toArray(new Column[0])); 579 } 580 581 /** 582 * Send text to Writer output. Note that the text will be aligned to the 583 * current column (by default, this is one column, left aligned with the 584 * full width of the page) 585 * 586 * @param buffer block of text characters 587 * @param index position to start printing 588 * @param len length (number of characters) of output 589 */ 590 @Override 591 public void write(char[] buffer, int index, int len) { 592 synchronized (this.lock) { 593 // loop through all characters passed to us 594 595 for (int i = index; i < index + len; i++) { 596 // if we haven't begun a new page, do that now 597 ensureOnPage(); 598 599 // if the character is a line terminator, begin a new line 600 // unless its \n after \r 601 if (buffer[i] == '\n') { 602 if (!last_char_was_return) { 603 newline(); 604 } 605 continue; 606 } 607 if (buffer[i] == '\r') { 608 newline(); 609 last_char_was_return = true; 610 continue; 611 } else { 612 last_char_was_return = false; 613 } 614 615 if (buffer[i] == '\f') { 616 flush_column(); 617 pageBreak(); 618 } 619 620 if (buffer[i] == '\t') { 621 // If the tab is in the last column, then we treat it as a conventional 622 // tab -- moving forward to the next multiple of 8 character position. Otherwise 623 // we treat it as a column separator. 624 // This only works for monospaced fonts. We also have to be in a left-aligned 625 // column 626 if (columnIndex == columns.length - 1 && columns[columnIndex].alignment.base == Align.LEFT) { 627 // In the last column and it is LEFT aligned 628 int spaces = 1; // Insert one space by default 629 if (isMonospaced()) { 630 spaces = 8 - (line.length() & 7); 631 } 632 633 line += " ".repeat(spaces); 634 continue; 635 } 636 // Compute where the line should go and output it 637 flush_column(); 638 continue; 639 } 640 641 // if some other non-printing char, ignore it 642 if (Character.isWhitespace(buffer[i]) && !Character.isSpaceChar(buffer[i])) { 643 continue; 644 } 645 646 line += buffer[i]; 647 } 648 if (page != null && line.length() > 0) { 649 // If we have pending text, flush it now 650 flush_column(); 651 } 652 } 653 } 654 655 /** 656 * Flush the current line to the current column. This may be called 657 * recursively if the line is too long for the current column and wrapping 658 * is enabled. 659 */ 660 private void flush_column() { 661 Column column = columns[columnIndex]; 662 663 Rectangle2D bounds = font.getStringBounds(line, neutralFRC); 664 665 int stringStartPos = column.getStartPos(bounds.getWidth()); 666 int columnWidth = Math.min(column.getWidth(), width - stringStartPos); 667 668 if (bounds.getWidth() > columnWidth) { 669 // This text does not fit. This means that we need to split it and wrap it to the next line. 670 671 // First, we need to find where to split the string. 672 // Do this with a binary search to be efficient. We take spaces into account. 673 // We want to get the longest possible string that will fit in the column. 674 int splitPos = 0; 675 int low = 0; 676 int high = line.length(); 677 while (low < high) { 678 int mid = (low + high) / 2; 679 if (font.getStringBounds(line.substring(0, mid), neutralFRC).getWidth() < columnWidth) { 680 // We know that the first mid characters will fit in the column. 681 low = mid + 1; 682 } else { 683 // We know that the first mid characters will not fit in the column. 684 high = mid; 685 } 686 } 687 // low is the first character that causes the string not to fit 688 splitPos = low - 1; 689 690 if (column.isWrap()) { 691 // We have to back up to a space to avoid splitting a word. 692 while (splitPos > 0 && !Character.isSpaceChar(line.charAt(splitPos))) { 693 splitPos--; 694 } 695 if (splitPos == 0) { 696 // We couldn't find a space to split on, so we have to split on a non-space. 697 splitPos = low - 1; 698 } 699 } 700 701 if (splitPos < 1) { 702 splitPos = 1; // Even if it won't fit, we have to output something. 703 } 704 // Now we can split the string and wrap it to the next line. 705 String firstLine = line.substring(0, splitPos); 706 stringStartPos = column.getStartPos(font.getStringBounds(firstLine, actualFRC).getWidth()); 707 708 // We can now output the first line. 709 record(new DrawString(firstLine, x0 + stringStartPos, y0 + v_pos + lineascent)); 710 711 if (column.isWrap()) { 712 // Skip any spaces at the split position 713 while (splitPos < line.length() - 1 && Character.isSpaceChar(line.charAt(splitPos))) { 714 splitPos++; 715 } 716 String secondLine = line.substring(splitPos); 717 // We can now output the second line. 718 v_pos += lineheight; 719 line = secondLine; 720 int saveColumnIndex = columnIndex; 721 flush_column(); 722 // This has already advanced the column index, so we back it up 723 columnIndex = saveColumnIndex; 724 max_v_pos = Math.max(max_v_pos, v_pos); 725 v_pos -= lineheight; 726 } 727 } else { 728 record(new DrawString(line, x0 + stringStartPos, y0 + v_pos + lineascent)); 729 } 730 731 if (columnIndex < columns.length - 1) { 732 columnIndex++; 733 } 734 735 line = ""; 736 } 737 738 /** 739 * Write a given String with the desired color. 740 * <p> 741 * Reset the text color back to the default after the string is written. 742 * This method is really only good for changing the color of a complete line 743 * (or column). 744 * 745 * @param c the color desired for this String 746 * @param s the String 747 * @throws java.io.IOException if unable to write to printer 748 */ 749 public void write(Color c, String s) throws IOException { 750 ensureOnPage(); 751 record(new SetColor(c)); 752 write(s); 753 // note that the above write(s) can cause the page to become null! 754 if (currentPageCommands != null) { 755 record(new SetColor(color)); // reset color 756 } 757 } 758 759 @Override 760 public void flush() { 761 } 762 763 /** 764 * Handle close event of pane. Modified to clean up the added preview 765 * capability. 766 */ 767 @Override 768 public void close() { 769 synchronized (this.lock) { 770 if (page != null) { 771 pageBreak(); 772 } 773 if (isPreview) { 774 // set up first page for display in preview frame 775 // to get the image displayed, put it in an icon and the icon in a label 776 pagenum = 1; 777 displayPage(); 778 } else if (printerJob != null) { 779 try { 780 // This is where the actual printing happens. I wonder if this should 781 // be spun off into its own task to prevent the GUI from freezing 782 // if the printing process is slow. 783 printerJob.print(attributes); 784 } catch (PrinterException e) { 785 log.error("Error printing", e); 786 } 787 } 788 } 789 } 790 791 /** 792 * Free up resources . 793 * <p> 794 * Added so that a preview can be canceled. 795 */ 796 public void dispose() { 797 synchronized (this.lock) { 798 if (page != null) { 799 page.dispose(); 800 } 801 if (previewFrame != null) { 802 previewFrame.dispose(); 803 } 804 if (printerJob != null) { 805 printerJob.cancel(); 806 } 807 } 808 } 809 810 /** 811 * Set the font to be used for the next write operation. This really only is 812 * good for the next line (or column) of output. Use with caution. 813 * <p> 814 * If any of the parameters are null, the current value will be used. 815 * 816 * @param name the name of the font 817 * @param style the style of the font 818 * @param size the size of the font 819 */ 820 public void setFont(String name, Integer style, Integer size) { 821 synchronized (this.lock) { 822 if (style == null) { 823 style = useFontStyle; 824 } 825 if (size == null) { 826 size = useFontSize; 827 } 828 if (name == null) { 829 name = useFontName; 830 } 831 font = new Font(name, style, size); 832 useFontName = name; 833 useFontStyle = style; 834 useFontSize = size; 835 // if a page is pending, set the new font, else newpage() will 836 if (currentPageCommands != null) { 837 record(new SetFont(font)); 838 refreshMetrics(); 839 } 840 } 841 } 842 843 /** 844 * Refresh the font metrics after changing things like font, size, etc. 845 */ 846 private void refreshMetrics() { 847 Rectangle2D bounds = font.getStringBounds("m".repeat(100), neutralFRC); 848 charwidth = (float) (bounds.getWidth() / 100.0); 849 LineMetrics lm = font.getLineMetrics("Your text here", neutralFRC); 850 lineheight = lm.getHeight(); 851 lineascent = lm.getAscent(); 852 853 // compute lines and columns within margins 854 double widthI = font.getStringBounds("i", neutralFRC).getWidth(); 855 double widthM = font.getStringBounds("m", neutralFRC).getWidth(); 856 857 // Using a tiny epsilon check for double precision stability 858 isMonospacedFont = Math.abs(widthI - widthM) < 0.0001; 859 } 860 861 /** 862 * Get the height of a line of text. This is the amount that the vertical 863 * position will advance for each line. 864 * 865 * @return the height of a line of text 866 */ 867 public float getLineHeight() { 868 return this.lineheight; 869 } 870 871 /** 872 * Get the size of the font. 873 * 874 * @return the size of the font 875 */ 876 public int getFontSize() { 877 return this.useFontSize; 878 } 879 880 /** 881 * Get the width of a character. This is only valid for monospaced fonts. 882 * 883 * @return the width of a character, or null if the font is not monospaced 884 */ 885 public Float getCharWidth() { 886 // TODO DAB temp fix to prevent NPE when printing non-monospaced fonts 887 // if (!isMonospaced()) { 888 // return null; 889 // } 890 return this.charwidth; 891 } 892 893 /** 894 * Get the ascent of the font. This is the distance from the baseline to the 895 * top of the font. 896 * 897 * @return the ascent of the font 898 */ 899 public float getLineAscent() { 900 return this.lineascent; 901 } 902 903 /** 904 * sets the default text color 905 * 906 * @param c the new default text color 907 */ 908 public void setTextColor(Color c) { 909 color = c; 910 if (currentPageCommands != null) { 911 record(new SetColor(c)); 912 } 913 } 914 915 /** 916 * End the current page. Subsequent output will be on a new page 917 */ 918 public void pageBreak() { 919 synchronized (this.lock) { 920 if (isPreview && previewImage != null) { 921 pageImages.addElement(previewImage); 922 } 923 if (page != null) { 924 page.dispose(); 925 } 926 page = null; 927 currentPageCommands = null; 928 previewImage = null; 929 } 930 } 931 932 /** 933 * Return the number of columns of characters that fit on a page. 934 * 935 * @return the number of characters in a line or null if the font is not 936 * monospaced 937 */ 938 public Integer getCharactersPerLine() { 939 // TODO DAB temp fix to prevent NPE when printing non-monospaced fonts 940 // if (!isMonospaced()) { 941 // return null; 942 // } 943 int chars_per_line = (int) (width / charwidth); 944 return chars_per_line; 945 } 946 947 /** 948 * This ensures that the required amount of vertical space is available. If 949 * not, a page break is inserted. 950 * 951 * @param points The amount of vertical space to ensure in points. 952 */ 953 public void ensureVerticalSpace(int points) { 954 if (v_pos + points + lineheight >= height) { 955 pageBreak(); 956 } 957 } 958 959 /** 960 * This leaves the required amount of vertical space. If not enough space is 961 * available, a page break is inserted. 962 * 963 * @param points The amount of vertical space to leave in points. 964 */ 965 public void leaveVerticalSpace(float points) { 966 v_pos += points; 967 ensureVerticalSpace(0); 968 } 969 970 /** 971 * Internal method begins a new line method modified by Dennis Miller to add 972 * preview capability 973 */ 974 protected void newline() { 975 if (page != null) { 976 flush_column(); 977 } 978 line = ""; 979 columnIndex = 0; 980 v_pos = Math.max(v_pos, max_v_pos); 981 max_v_pos = 0; 982 v_pos += lineheight; 983 // Note that text is printed *below* the current v_pos, so we need to 984 // check if we have enough space for that line. 985 if (v_pos + lineheight >= height) { 986 pageBreak(); 987 } 988 } 989 990 /** 991 * Ensure that we have a page object. The page is null when we are before 992 * the first page, or between pages. 993 */ 994 protected void ensureOnPage() { 995 if (page == null) { 996 newpage(); 997 } 998 } 999 1000 /** 1001 * Internal method beings a new page and prints the header method modified 1002 * by Dennis Miller to add preview capability 1003 */ 1004 private void newpage() { 1005 pagenum++; 1006 v_pos = 0; 1007 currentPageCommands = new ArrayList<>(); 1008 1009 page = getGraphics(); 1010 1011 if (isPreview) { 1012 previewImage = new BufferedImage(pagesizePixels.width, pagesizePixels.height, BufferedImage.TYPE_INT_RGB); 1013 page = previewImage.createGraphics(); 1014 1015 setupGraphics(page, true); 1016 1017 page.setColor(Color.white); 1018 page.fillRect(0, 0, (int) (pagesizePixels.width * 72.0 / getScreenResolution()), 1019 (int) (pagesizePixels.height * 72.0 / getScreenResolution())); 1020 page.setColor(color); 1021 } else { 1022 // We only need this is non-preview mode. 1023 pageCommands.add(currentPageCommands); 1024 } 1025 1026 if (printHeader) { 1027 record(new SetFont(headerfont)); 1028 record(new DrawString(jobname, x0, headery)); 1029 1030 FontRenderContext frc = page.getFontMetrics().getFontRenderContext(); 1031 1032 String s = "- " + pagenum + " -"; // print page number centered 1033 Rectangle2D bounds = headerfont.getStringBounds(s, frc); 1034 record(new DrawString(s, (int) (x0 + (this.width - bounds.getWidth()) / 2), headery)); 1035 1036 bounds = headerfont.getStringBounds(time, frc); 1037 record(new DrawString(time, (int) (x0 + width - bounds.getWidth()), headery)); 1038 1039 // draw a line under the header 1040 int y = headery + headermetrics.getDescent() + 1; 1041 record(new DrawLine(x0, y, x0 + width, y)); 1042 } 1043 // set basic font 1044 record(new SetFont(font)); 1045 refreshMetrics(); 1046 } 1047 1048 /** 1049 * Gets all the pages as Images. 1050 * 1051 * @return the current page as a BufferedImage 1052 */ 1053 public Vector<BufferedImage> getPageImages() { 1054 return pageImages; 1055 } 1056 1057 /** 1058 * Gets the current page num -- this can be used to determine when a page 1059 * break has happened. The page number may be increased whenever a newline 1060 * is printed. 1061 * 1062 * @return the current page number 1063 */ 1064 public int getPageNum() { 1065 return pagenum + (page == null ? 1 : 0); 1066 } 1067 1068 /** 1069 * Setup the graphics context for preview. We want the subpixel positioning 1070 * for text. 1071 * 1072 * @param g2d the graphics context to setup 1073 * @param applyScale whether to apply the scale factor 1074 */ 1075 private void setupGraphics(Graphics2D g2d, boolean applyScale) { 1076 if (applyScale) { 1077 double scale = getScreenResolution() / (float) DPI; 1078 g2d.scale(scale, scale); 1079 } 1080 1081 // Enable Antialiasing (Smooths the edges) 1082 g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 1083 RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 1084 1085 // Enable Fractional Metrics (Improves character spacing) 1086 g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, 1087 RenderingHints.VALUE_FRACTIONALMETRICS_ON); 1088 1089 // High Quality Rendering 1090 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 1091 RenderingHints.VALUE_RENDER_QUALITY); 1092 1093 // Set Interpolation for the Image (The most important for images) 1094 g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 1095 RenderingHints.VALUE_INTERPOLATION_BICUBIC); 1096 1097 // Enable Antialiasing (Smooths the edges) 1098 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1099 RenderingHints.VALUE_ANTIALIAS_ON); 1100 } 1101 1102 /** 1103 * Write a graphic to the printout. 1104 * <p> 1105 * This was not in the original class, but was added afterwards by Bob 1106 * Jacobsen. Modified by D Miller. Modified by P Gladstone. The image well 1107 * be rendered at 1.5 pixels per point. 1108 * <p> 1109 * The image is positioned on the right side of the paper, at the current 1110 * height. 1111 * 1112 * @param c image to write 1113 * @param i ignored, but maintained for API compatibility 1114 */ 1115 public void write(Image c, Component i) { 1116 writeSpecificSize(c, new Dimension((int) (c.getWidth(null) / 1.5), (int) (c.getHeight(null) / 1.5))); 1117 } 1118 1119 /** 1120 * Write the decoder pro icon to the output. Method added by P Gladstone. 1121 * This actually uses the high resolution image. It also advances the v_pos 1122 * appropriately (unless no_advance is True) 1123 * <p> 1124 * The image is positioned on the right side of the paper, at the current 1125 * height. 1126 * 1127 * @param no_advance if true, do not advance the v_pos 1128 * @return The actual size in points of the icon that was rendered. 1129 */ 1130 public Dimension writeDecoderProIcon(boolean no_advance) { 1131 ImageIcon hiresIcon = 1132 new ImageIcon(HardcopyWriter.class.getResource("/resources/decoderpro_large.png")); 1133 Image icon = hiresIcon.getImage(); 1134 Dimension size = writeSpecificSize(icon, new Dimension(icon.getWidth(null) / 6, icon.getHeight(null) / 6)); 1135 if (!no_advance) { 1136 // Advance the v_pos by the height of the icon 1137 v_pos += size.height + lineheight; 1138 } 1139 return size; 1140 } 1141 1142 /** 1143 * Write the decoder pro icon to the output. Method added by P Gladstone. 1144 * This actually uses the high resolution image. It also advances the v_pos 1145 * appropriately. 1146 * <p> 1147 * The image is positioned on the right side of the paper, at the current 1148 * height. 1149 * 1150 * @return The actual size in points of the icon that was rendered. 1151 */ 1152 public Dimension writeDecoderProIcon() { 1153 return writeDecoderProIcon(false); 1154 } 1155 1156 /** 1157 * Write an image to the output at the current position, scaled to the 1158 * required size. More importantly, it does not save the image object in 1159 * memory, but will re-read it from the disk when it needs to be used. This 1160 * is needed if there are a lot of large images in a printout. 1161 * 1162 * @param icon The ImageIconWrapper object to display 1163 * @param requiredSize The size in points to display the image 1164 * @return The actual size rendered 1165 */ 1166 public Dimension writeSpecificSize(ImageIconWrapper icon, Dimension requiredSize) { 1167 ensureOnPage(); 1168 1169 float widthScale = (float) requiredSize.width / icon.getIconWidth(); 1170 float heightScale = (float) requiredSize.height / icon.getIconHeight(); 1171 float scale = Math.min(widthScale, heightScale); 1172 1173 Dimension d = new Dimension(Math.round(icon.getIconWidth() * scale), Math.round(icon.getIconHeight() * scale)); 1174 1175 int x = x0 + width - d.width; 1176 int y = (int) (y0 + v_pos + lineascent); 1177 1178 if (isPreview) { 1179 float pixelsPerPoint = getScreenResolution() / (float) DPI; 1180 Image c = ImageUtils.getScaledInstance(icon.getImage(), (int) (requiredSize.width * pixelsPerPoint), 1181 (int) (requiredSize.height * pixelsPerPoint)); 1182 1183 record(new DrawImage(c, x, y, d.width, d.height)); 1184 } else { 1185 record(new DrawImageIconFile(icon.getPathName(), x, y, d.width, d.height)); 1186 } 1187 return d; 1188 } 1189 1190 /** 1191 * Write a graphic to the printout at a specific size (in points) 1192 * <p> 1193 * This was not in the original class, but was added afterwards by Kevin 1194 * Dickerson. Heavily modified by P Gladstone. If the image is large and 1195 * there are many images in the printout, then it probably makes sense to 1196 * pass in a HardcopyWriter.ImageIconWrapper object instead. This will save 1197 * memory as it just retains the filename until time comes to actually 1198 * render the image. 1199 * <p> 1200 * The image is positioned on the right side of the paper, at the current 1201 * height. The image aspect ratio is maintained. 1202 * 1203 * @param c the image to print 1204 * @param requiredSize the dimensions (in points) to scale the image to. The 1205 * image will fit inside the bounding box. 1206 * @return the dimensions of the image in points 1207 */ 1208 public Dimension writeSpecificSize(Image c, Dimension requiredSize) { 1209 // if we haven't begun a new page, do that now 1210 ensureOnPage(); 1211 1212 float widthScale = (float) requiredSize.width / c.getWidth(null); 1213 float heightScale = (float) requiredSize.height / c.getHeight(null); 1214 float scale = Math.min(widthScale, heightScale); 1215 1216 Dimension d = new Dimension(Math.round(c.getWidth(null) * scale), Math.round(c.getHeight(null) * scale)); 1217 1218 if (isPreview) { 1219 float pixelsPerPoint = getScreenResolution() / (float) DPI; 1220 c = ImageUtils.getScaledInstance(c, (int) (requiredSize.width * pixelsPerPoint), 1221 (int) (requiredSize.height * pixelsPerPoint)); 1222 d = new Dimension((int) (c.getWidth(null) / pixelsPerPoint), (int) (c.getHeight(null) / pixelsPerPoint)); 1223 } 1224 1225 int x = x0 + width - d.width; 1226 int y = (int) (y0 + v_pos + lineascent); 1227 1228 record(new DrawImage(c, x, y, d.width, d.height)); 1229 return d; 1230 } 1231 1232 /** 1233 * A Method to allow a JWindow to print itself at the current line position 1234 * <p> 1235 * This was not in the original class, but was added afterwards by Dennis 1236 * Miller. 1237 * <p> 1238 * Intended to allow for a graphic printout of the speed table, but can be 1239 * used to print any window. The JWindow is passed to the method and prints 1240 * itself at the current line and aligned at the left margin. The calling 1241 * method should check for sufficient space left on the page and move it to 1242 * the top of the next page if there isn't enough space. 1243 * <p> 1244 * I'm not convinced that this is actually used as the code that invokes it 1245 * is under a test for Java version before 1.5. 1246 * 1247 * @param jW the window to print 1248 */ 1249 public void write(JWindow jW) { 1250 // if we haven't begun a new page, do that now 1251 ensureOnPage(); 1252 1253 int x = x0; 1254 int y = (int) (y0 + v_pos); 1255 record(new PrintWindow(jW, x, y)); 1256 } 1257 1258 /** 1259 * Draw a line on the printout. Calls to this should be replaced by calls to 1260 * `writeExactLine` which doesn't offset the line by strange amounts. 1261 * <p> 1262 * This was not in the original class, but was added afterwards by Dennis 1263 * Miller. 1264 * <p> 1265 * hStart and hEnd represent the horizontal point positions. The lines 1266 * actually start in the middle of the character position (to the left) to 1267 * make it easy to draw vertical lines and space them between printed 1268 * characters. This is (unfortunately) bad for getting something on the 1269 * right margin. 1270 * <p> 1271 * vStart and vEnd represent the vertical point positions. Horizontal lines 1272 * are drawn underneath below the vStart position. They are offset so they 1273 * appear evenly spaced, although they don't take into account any space 1274 * needed for descenders, so they look best with all caps text. If vStart is 1275 * set to the current vPos, then the line is under the current row of text. 1276 * 1277 * @param vStart vertical starting position 1278 * @param hStart horizontal starting position 1279 * @param vEnd vertical ending position 1280 * @param hEnd horizontal ending position 1281 */ 1282 public void writeLine(float vStart, float hStart, float vEnd, float hEnd) { 1283 writeExactLine((vStart + (lineheight - lineascent) / 2), hStart - useFontSize / 4.0f, 1284 (vEnd + (lineheight - lineascent) / 2), hEnd - useFontSize / 4.0f); 1285 } 1286 1287 /** 1288 * Draw a line on the printout. 1289 * <p> 1290 * This was not in the original class, but was added afterwards by Philip 1291 * Gladstone. 1292 * <p> 1293 * hStart and hEnd represent the horizontal point positions. 1294 * <p> 1295 * vStart and vEnd represent the vertical point positions. 1296 * 1297 * @param vStart vertical starting position 1298 * @param hStart horizontal starting position 1299 * @param vEnd vertical ending position 1300 * @param hEnd horizontal ending position 1301 */ 1302 public void writeExactLine(float vStart, float hStart, float vEnd, float hEnd) { 1303 // if we haven't begun a new page, do that now 1304 ensureOnPage(); 1305 1306 int xStart = (int) (x0 + hStart); 1307 int xEnd = (int) (x0 + hEnd); 1308 int yStart = (int) (y0 + vStart); 1309 int yEnd = (int) (y0 + vEnd); 1310 record(new DrawLine(xStart, yStart, xEnd, yEnd)); 1311 1312 // We want to make sure that the lines are within the printable area 1313 if (xStart < leftMargin) { 1314 leftMargin = xStart; 1315 } 1316 } 1317 1318 /** 1319 * Get the current vertical position on the page 1320 * 1321 * @return the current vertical position of the base of the current line on 1322 * the page (in points) 1323 */ 1324 public float getCurrentVPos() { 1325 return v_pos; 1326 } 1327 1328 /** 1329 * Print vertical borders on the current line at the left and right sides of 1330 * the page at pixel positions 0 and width. Border lines are one text line 1331 * in height. ISSUE: Where should these lines be drawn? 1332 * <p> 1333 * This was not in the original class, but was added afterwards by Dennis 1334 * Miller. 1335 */ 1336 public void writeBorders() { 1337 writeLine((int) v_pos, 0, (int) (v_pos + lineheight), 0); 1338 writeLine((int) v_pos, width, (int) (v_pos + lineheight), width); 1339 } 1340 1341 /** 1342 * Increase line spacing by a percentage 1343 * <p> 1344 * This method should be invoked immediately after a new HardcopyWriter is 1345 * created. 1346 * <p> 1347 * This method was added to improve appearance when printing tables 1348 * <p> 1349 * This was not in the original class, added afterwards by DaveDuchamp. 1350 * 1351 * @param percent percentage by which to increase line spacing 1352 */ 1353 public void increaseLineSpacing(int percent) { 1354 float delta = (lineheight * percent) / 100; 1355 lineheight = lineheight + delta; 1356 lineascent = lineascent + delta; 1357 } 1358 1359 /** 1360 * Returns true if the current font is monospaced 1361 * 1362 * @return true if the current font is monospaced. 1363 */ 1364 public boolean isMonospaced() { 1365 return isMonospacedFont; 1366 } 1367 1368 /** 1369 * Get the list of commands for the whole document. 1370 * 1371 * @return the list of commands for the document 1372 */ 1373 public List<List<PrintCommand>> getPageCommands() { 1374 return pageCommands; 1375 } 1376 1377 public static class PrintCanceledException extends Exception { 1378 1379 public PrintCanceledException(String msg) { 1380 super(msg); 1381 } 1382 } 1383 1384 public static class ColumnException extends Exception { 1385 public ColumnException(String msg) { 1386 super(msg); 1387 } 1388 } 1389 1390 /** 1391 * Enum to represent the alignment of text in a column. 1392 */ 1393 public enum Align { 1394 LEFT, 1395 CENTER, 1396 RIGHT, 1397 LEFT_WRAP(LEFT), 1398 CENTER_WRAP(CENTER), 1399 RIGHT_WRAP(RIGHT); 1400 1401 private final Align base; 1402 1403 // Constructor for base values 1404 Align() { 1405 this.base = null; 1406 } 1407 1408 // Constructor for wrapped values 1409 Align(Align base) { 1410 this.base = base; 1411 } 1412 1413 /** 1414 * Gets the base alignment of the column 1415 * 1416 * @return The base alignment of the column 1417 */ 1418 public Align getBase() { 1419 return (base == null) ? this : base; 1420 } 1421 1422 /** 1423 * Gets whether the alignment is a wrap alignment 1424 * 1425 * @return true if the alignment is a wrap alignment 1426 */ 1427 public boolean isWrap() { 1428 return base != null; 1429 } 1430 } 1431 1432 /** 1433 * Class to represent a column in the output. This has a start position, 1434 * width and alignment. This allows left, center or right alignment, with or 1435 * without wrapping. 1436 */ 1437 public static class Column { 1438 int position; 1439 int width; 1440 boolean maxWidth = false; 1441 Align alignment; 1442 1443 /** 1444 * Create a Column with specified position, width and alignment 1445 * 1446 * @param position The position of the column in points 1447 * @param width The width of the column in points 1448 * @param alignment The alignment of the column 1449 */ 1450 public Column(int position, int width, Align alignment) { 1451 this.position = position; 1452 this.width = width; 1453 this.alignment = alignment; 1454 } 1455 1456 /** 1457 * Create a Column with specified position and alignment. The width will 1458 * be calculated up to the next column. 1459 * 1460 * @param position The position of the column in points 1461 * @param alignment The alignment of the column 1462 */ 1463 public Column(int position, Align alignment) { 1464 this.position = position; 1465 this.maxWidth = true; 1466 this.alignment = alignment; 1467 } 1468 1469 /** 1470 * Create a Column with specified position and width with LEFT alignment 1471 * 1472 * @param position The position of the column in points 1473 * @param width The width of the column in points 1474 */ 1475 public Column(int position, int width) { 1476 this(position, width, Align.LEFT); 1477 } 1478 1479 /** 1480 * Sets the width of the column in points 1481 * 1482 * @param width The new width of the column in points 1483 */ 1484 public void setWidth(int width) { 1485 this.width = width; 1486 } 1487 1488 /** 1489 * Gets the width of the column in points 1490 * 1491 * @return The width of the column in points 1492 */ 1493 public int getWidth() { 1494 return width; 1495 } 1496 1497 /** 1498 * Gets the starting position of text of length strlen (in points) 1499 * 1500 * @param strlen The length of the text in points 1501 * @return The starting position of the text in points 1502 */ 1503 public int getStartPos(double strlen) { 1504 switch (alignment.getBase()) { 1505 case LEFT: 1506 return position; 1507 case CENTER: 1508 return (int) (position + width / 2 - strlen / 2); 1509 case RIGHT: 1510 return (int) (position + width - strlen); 1511 default: 1512 throw new IllegalArgumentException("Unknown alignment: " + alignment); 1513 } 1514 } 1515 1516 /** 1517 * Gets the starting position of the column in points 1518 * 1519 * @return The starting position of the column in points 1520 */ 1521 public int getPosition() { 1522 return position; 1523 } 1524 1525 /** 1526 * Gets the alignment of the column 1527 * 1528 * @return The alignment of the column 1529 */ 1530 public Align getAlignment() { 1531 return alignment; 1532 } 1533 1534 /** 1535 * Gets whether the column is a wrap column 1536 * 1537 * @return true if the column is a wrap column 1538 */ 1539 public boolean isWrap() { 1540 return alignment.isWrap(); 1541 } 1542 1543 @Override 1544 public String toString() { 1545 return "Column{" + "position=" + position + ", width=" + width + ", alignment=" + alignment + "}"; 1546 } 1547 1548 /** 1549 * Stretch the columns to fit the specified width. The columns are 1550 * assumed to be sorted by position. The input widths are treated as 1551 * ratios. There is a gap between the columns. 1552 * 1553 * @param columns The columns to stretch 1554 * @param width The width to stretch to in points 1555 * @param gap The gap between the columns in points 1556 * @return The stretched columns 1557 */ 1558 public static ArrayList<Column> stretchColumns(Collection<Column> columns, int width, int gap) { 1559 ArrayList<Column> newColumns = new ArrayList<>(); 1560 double totalWidth = 0; 1561 for (Column column : columns) { 1562 totalWidth += column.getWidth(); 1563 } 1564 1565 double scale = (width - (columns.size() - 1) * gap) / totalWidth; 1566 // Two passes -- the first to compute the starting column numbers 1567 // the second to compute the widths 1568 int accumulatedGap = 0; 1569 int accumulatedWidth = 0; 1570 for (Column column : columns) { 1571 newColumns.add(new Column((int) Math.round(accumulatedWidth * scale) + accumulatedGap, 0, 1572 column.getAlignment())); 1573 accumulatedWidth += column.getWidth(); 1574 accumulatedGap += gap; 1575 } 1576 1577 // Now set the widths 1578 for (int i = 0; i < newColumns.size(); i++) { 1579 Column column = newColumns.get(i); 1580 if (i == newColumns.size() - 1) { 1581 column.setWidth(width - column.getPosition()); 1582 } else { 1583 column.setWidth(newColumns.get(i + 1).getPosition() - column.getPosition() - gap); 1584 } 1585 } 1586 1587 return newColumns; 1588 } 1589 } 1590 1591 /** 1592 * Replay the recorded commands to the graphics context. This is called by 1593 * the PrinterJob. 1594 */ 1595 @Override 1596 public int print(Graphics g, PageFormat pf, int pageIndex) throws PrinterException { 1597 if (pageIndex >= pageCommands.size()) { 1598 return NO_SUCH_PAGE; 1599 } 1600 1601 if (!(g instanceof Graphics2D)) { 1602 throw new PrinterException("Graphics context is not a Graphics2D object: " + g.getClass().getName()); 1603 } 1604 1605 Graphics2D g2d = (Graphics2D) g; 1606 1607 // We already include the margins, but we need to worry about the page header. 1608 double yOffset = pf.getImageableY(); 1609 if (yOffset > titleTop) { 1610 // We have to translate down to make sure that the header is on the page 1611 g2d.translate(0, yOffset - titleTop); 1612 } 1613 double xOffset = pf.getImageableX(); 1614 if (xOffset > leftMargin) { 1615 // We have to translate right to make sure that the left margin is printable. 1616 g2d.translate(xOffset - leftMargin, 0); 1617 } 1618 //g2d.translate(pf.getImageableX(), pf.getImageableY()); 1619 1620 // Setup initial state 1621 g2d.setFont(font); 1622 g2d.setColor(color); 1623 setupGraphics(g2d, false); 1624 1625 for (PrintCommand cmd : pageCommands.get(pageIndex)) { 1626 cmd.execute(g2d); 1627 } 1628 1629 return PAGE_EXISTS; 1630 } 1631 1632 @Override 1633 public String toString() { 1634 return "HardcopyWriter(" + 1635 ", page=" + 1636 page + 1637 ", lineheight=" + 1638 lineheight + 1639 ", lineascent=" + 1640 lineascent + 1641 ", Msize=" + 1642 measure("M") + 1643 ", pagesizePoints=" + 1644 pagesizePoints + 1645 ", pagesizePixels=" + 1646 pagesizePixels + 1647 ")"; 1648 } 1649 1650 protected interface PrintCommand { 1651 void execute(Graphics2D g); 1652 } 1653 1654 protected static class DrawString implements PrintCommand { 1655 String s; 1656 float x, y; 1657 1658 DrawString(String s, float x, float y) { 1659 this.s = s; 1660 this.x = x; 1661 this.y = y; 1662 } 1663 1664 @VisibleForTesting 1665 public String getString() { 1666 return s; 1667 } 1668 1669 @Override 1670 public void execute(Graphics2D g) { 1671 g.drawString(s, x, y); 1672 } 1673 } 1674 1675 protected static class DrawLine implements PrintCommand { 1676 int x1, y1, x2, y2; 1677 1678 DrawLine(int x1, int y1, int x2, int y2) { 1679 this.x1 = x1; 1680 this.y1 = y1; 1681 this.x2 = x2; 1682 this.y2 = y2; 1683 } 1684 1685 @Override 1686 public void execute(Graphics2D g) { 1687 g.drawLine(x1, y1, x2, y2); 1688 } 1689 } 1690 1691 protected static class DrawImage implements PrintCommand { 1692 Image img; 1693 int x, y, width, height; 1694 1695 DrawImage(Image img, int x, int y, int width, int height) { 1696 this.img = img; 1697 this.x = x; 1698 this.y = y; 1699 this.width = width; 1700 this.height = height; 1701 } 1702 1703 @Override 1704 public void execute(Graphics2D g) { 1705 g.drawImage(img, x, y, width, height, null); 1706 } 1707 } 1708 1709 protected static class DrawImageIconFile implements PrintCommand { 1710 String pathName; 1711 int x, y, width, height; 1712 1713 DrawImageIconFile(String pathName, int x, int y, int width, int height) { 1714 this.pathName = pathName; 1715 this.x = x; 1716 this.y = y; 1717 this.width = width; 1718 this.height = height; 1719 } 1720 1721 @Override 1722 public void execute(Graphics2D g) { 1723 // Convert this into a DrawImage call 1724 ImageIcon icon = new ImageIcon(pathName); 1725 Image img = icon.getImage(); 1726 g.drawImage(img, x, y, width, height, null); 1727 } 1728 } 1729 1730 protected static class SetFont implements PrintCommand { 1731 Font font; 1732 1733 SetFont(Font font) { 1734 this.font = font; 1735 } 1736 1737 @Override 1738 public void execute(Graphics2D g) { 1739 g.setFont(font); 1740 } 1741 } 1742 1743 protected static class SetColor implements PrintCommand { 1744 Color color; 1745 1746 SetColor(Color color) { 1747 this.color = color; 1748 } 1749 1750 @Override 1751 public void execute(Graphics2D g) { 1752 g.setColor(color); 1753 } 1754 } 1755 1756 protected static class PrintWindow implements PrintCommand { 1757 JWindow jW; 1758 int x, y; 1759 1760 PrintWindow(JWindow jW, int x, int y) { 1761 this.jW = jW; 1762 this.x = x; 1763 this.y = y; 1764 } 1765 1766 @Override 1767 public void execute(Graphics2D g) { 1768 g.translate(x, y); 1769 jW.setVisible(true); 1770 jW.printAll(g); 1771 jW.setVisible(false); 1772 jW.dispose(); 1773 g.translate(-x, -y); 1774 } 1775 } 1776 1777 public static class ImageIconWrapper extends ImageIcon { 1778 String pathName; 1779 1780 /** 1781 * Class to save and be able to restore the pathname of an ImageIcon. 1782 * 1783 * @param pathName The filename to construct the ImageIcon for. 1784 */ 1785 public ImageIconWrapper(String pathName) { 1786 super(pathName); 1787 this.pathName = pathName; 1788 } 1789 1790 public String getPathName() { 1791 return pathName; 1792 } 1793 } 1794 1795 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HardcopyWriter.class); 1796}