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}