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