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