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