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