001package jmri.jmrit.operations.trains; 002 003import java.awt.*; 004import java.io.*; 005import java.nio.charset.StandardCharsets; 006import java.util.ArrayList; 007import java.util.List; 008 009import javax.print.attribute.standard.Sides; 010import javax.swing.ImageIcon; 011import javax.swing.JLabel; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import jmri.jmrit.operations.setup.Setup; 017import jmri.jmrit.operations.trains.trainbuilder.TrainCommon; 018import jmri.util.davidflanagan.CompatibleHardcopyWriter; 019 020/** 021 * Used for printing train Manifests and switch lists. Text can have color, bold 022 * and italic control characters. 023 * 024 * @author Daniel Boudreau (C) 2025, 2026 025 */ 026public class TrainPrintManifest extends TrainCommon { 027 028 protected static final char SPACE_CHAR = ' '; 029 private static boolean isPrintingStyleDone = false; 030 private static boolean isPrintingColor = false; 031 private static boolean isTextSizeDone = false; 032 private static Color color; 033 private static int _fontSize; 034 035 /** 036 * Print or preview a train Manifest or switch list. 037 * 038 * @param file File to be printed or previewed 039 * @param name Title of document 040 * @param isPreview true if preview 041 * @param fontName optional font to use when printing document 042 * @param logoURL optional pathname for logo 043 * @param printerName optional default printer name 044 * @param orientation Setup.LANDSCAPE, Setup.PORTRAIT, or Setup.HANDHELD 045 * @param fontSize font size 046 * @param isPrintHeader when true print page header 047 * @param sides two sides long or short can be null 048 */ 049 public static void printReport(File file, String name, boolean isPreview, String fontName, String logoURL, 050 String printerName, String orientation, int fontSize, boolean isPrintHeader, Sides sides) { 051 052 double leftmargin = .5; 053 double rightmargin = .5; 054 double topmargin = .5; 055 double bottommargin = .5; 056 057 // get hand held or half page dimensions in DPI 058 Dimension pageSize = getFullPageSizeDPI(orientation); 059 060 if (orientation.equals(Setup.RECEIPT)) { 061 leftmargin = .2; 062 rightmargin = .2; 063 } 064 065 try (CompatibleHardcopyWriter writer = new CompatibleHardcopyWriter(new Frame(), name, fontSize, leftmargin, 066 rightmargin, topmargin, bottommargin, isPreview, printerName, orientation.equals(Setup.LANDSCAPE), 067 isPrintHeader, sides, pageSize); 068 BufferedReader in = new BufferedReader(new InputStreamReader( 069 new FileInputStream(file), StandardCharsets.UTF_8));) { 070 071 // set font 072 if (!fontName.isEmpty()) { 073 writer.setFontName(fontName); 074 } 075 076 _fontSize = fontSize; 077 078 if (logoURL != null && !logoURL.equals(Setup.NONE)) { 079 ImageIcon icon = new ImageIcon(logoURL); 080 if (icon.getIconWidth() == -1) { 081 log.error("Logo not found: {}", logoURL); 082 } else { 083 writer.write(icon.getImage(), new JLabel(icon)); 084 } 085 } 086 087 List<String> lines = new ArrayList<>(); 088 String line; 089 while (true) { 090 line = in.readLine(); 091 if (line == null) { 092 if (isPreview) { 093 // need to do this in case the input file was empty to create preview 094 writer.write(" "); 095 } 096 break; 097 } 098 lines.add(line); 099 if (line.isBlank()) { 100 print(writer, lines, false); 101 } 102 } 103 print(writer, lines, true); // last block 104 } catch (FileNotFoundException e) { 105 log.error("Build file doesn't exist", e); 106 } catch (CompatibleHardcopyWriter.PrintCanceledException ex) { 107 log.debug("Print canceled"); 108 } catch (IOException e) { 109 log.warn("Exception printing: {}", e.getLocalizedMessage()); 110 } 111 } 112 113 // this routine checks to see if the text between line spaces will fit on the page 114 private static void print(CompatibleHardcopyWriter writer, List<String> lines, boolean lastBlock) 115 throws IOException { 116 int lineSize = getNumberOfLines(writer, lines); 117 if (Setup.isPrintNoPageBreaksEnabled() && 118 writer.getCurrentLineNumber() != 0 && 119 writer.getLinesPerPage() - writer.getCurrentLineNumber() < (lastBlock ? lineSize : lineSize - 1)) { 120 writer.pageBreak(); 121 } 122 // check for exact page break 123 if (writer.getLinesPerPage() - writer.getCurrentLineNumber() == lineSize - 1) { 124 // eliminate blank line after page break 125 String s = lines.get(lines.size() - 1); 126 if (s.isBlank()) { 127 lines.remove(lines.size() - 1); 128 } 129 } 130 // use line feed for all lines? 131 if (lastBlock && writer.getLinesPerPage() - writer.getCurrentLineNumber() < lineSize) { 132 lastBlock = false; // yes 133 } 134 135 isPrintingColor = false; 136 color = null; 137 138 for (String line : lines) { 139 // determine if there's a line separator 140 if (printHorizontalLineSeparator(writer, line)) { 141 color = null; 142 continue; 143 } 144 145 // font size change? 146 line = setFontSize(writer, line); 147 148 // bold or italic text? 149 line = printStyle(writer, line); 150 151 // color text without bold or italic text? 152 line = printColor(writer, line); 153 154 line = printVerticalLineSeparator(writer, line); 155 156 // color can be null 157 writer.write(color, line); 158 159 // no line feed if last line of file, eliminates blank page 160 if (!lastBlock || writer.getCurrentLineNumber() < writer.getLinesPerPage() - 1) { 161 writer.write(NEW_LINE); 162 } 163 164 // done text size change? 165 if (isTextSizeDone) { 166 writer.setFont(null, null, _fontSize); 167 isTextSizeDone = false; 168 } 169 170 // done bold or italic text? 171 if (isPrintingStyleDone) { 172 writer.setFontStyle(Font.PLAIN); 173 isPrintingStyleDone = false; 174 } 175 } 176 lines.clear(); 177 } 178 179 /* 180 * When determining the number of lines to print, we need to ignore any 181 * horizontal lines. 182 */ 183 private static int getNumberOfLines(CompatibleHardcopyWriter writer, List<String> lines) { 184 int numberLines = lines.size(); 185 for (String line : lines) { 186 if (isHorizontalLineSpearator(writer, line)) { 187 numberLines--; 188 } 189 } 190 return numberLines; 191 } 192 193 /* 194 * Returns true if horizontal line was printed 195 */ 196 private static boolean printHorizontalLineSeparator(CompatibleHardcopyWriter writer, String line) { 197 boolean horizontalLineSeparatorFound = isHorizontalLineSpearator(writer, line); 198 if (horizontalLineSeparatorFound) { 199 int lineOffset = Setup.getHorizontalLineAdjustment(); 200 float vStart = writer.getCurrentVPos() + lineOffset; 201 float hEnd = writer.getPrintablePagesizePoints().width; 202 writer.writeLine(vStart, 0, vStart, hEnd); 203 } 204 return horizontalLineSeparatorFound; 205 } 206 207 /* 208 * Determines if horizontal line. Requires the number of horizontal line 209 * characters equal to the page width and no other characters in the line. 210 * The smallest horizontal line is when the 2.25 wide paper is selected and 211 * largest font 18. About 12 horizontal line characters. 212 */ 213 private static boolean isHorizontalLineSpearator(CompatibleHardcopyWriter writer, String line) { 214 int count = 0; 215 for (char c : line.toCharArray()) { 216 if (c == HORIZONTAL_LINE_CHAR) { 217 count++; 218 } else { 219 count = 0; 220 break; // all characters need to be horizontal char 221 } 222 } 223 // one less character most likely not necessary 224 return count >= writer.getCharactersPerLine() - 1; 225 } 226 227 private static String printVerticalLineSeparator(CompatibleHardcopyWriter writer, String line) { 228 if (line.contains(Character.toString(VERTICAL_LINE_CHAR))) { 229 // make a frame (two column format) 230 int lineOffset = Setup.getHorizontalLineAdjustment(); 231 float vStart = writer.getCurrentVPos() + lineOffset; 232 float hEnd = writer.getPrintablePagesizePoints().width; 233 writer.writeLine(vStart, 0, vStart + writer.getLineHeight(), 0); 234 writer.writeLine(vStart, hEnd / 2, vStart + writer.getLineHeight(), hEnd / 2); 235 writer.writeLine(vStart, hEnd, vStart + writer.getLineHeight(), hEnd); 236 line = line.replace(VERTICAL_LINE_CHAR, SPACE_CHAR); 237 } 238 return line; 239 } 240 241 private static String printStyle(CompatibleHardcopyWriter writer, String line) throws IOException { 242 // If monospaced font, it is possible to style or color a subset of words in the line. 243 if (writer.isMonospaced() && 244 (line.contains(TEXT_BOLD) || 245 line.contains(TEXT_BOLD_END))) { 246 line = printingStyleWords(writer, line, TEXT_BOLD, TEXT_BOLD_END, Font.BOLD); 247 } else if (writer.isMonospaced() && 248 (line.contains(TEXT_ITALIC) || 249 line.contains(TEXT_ITALIC_END))) { 250 line = printingStyleWords(writer, line, TEXT_ITALIC, TEXT_ITALIC_END, Font.ITALIC); 251 } else { 252 if (line.contains(TEXT_ITALIC)) { 253 writer.setFontStyle(Font.ITALIC); // italicize the entire line 254 } 255 if (line.contains(TEXT_BOLD)) { 256 writer.setFontStyle(Font.BOLD); // bold the entire line 257 } 258 if (line.contains(TEXT_BOLD_END) || line.contains(TEXT_ITALIC_END)) { 259 isPrintingStyleDone = true; 260 } 261 if (line.contains(TEXT_ITALIC) || line.contains(TEXT_ITALIC_END)) { 262 line = getTextItalicString(line); // strip the italic characters 263 } 264 if (line.contains(TEXT_BOLD) || line.contains(TEXT_BOLD_END)) { 265 line = getTextBoldString(line); // strip the bold characters 266 } 267 } 268 return line; 269 } 270 271 // text has bold or italic control characters and possibly color control characters 272 private static String printingStyleWords(CompatibleHardcopyWriter writer, String line, String startStyle, 273 String endStyle, int style) throws IOException { 274 if (!isPrintingColor) { 275 color = null; 276 } 277 offset = 0; 278 printStyleWords(writer, line, startStyle, endStyle, style); 279 return ""; // done 280 } 281 282 // where in the line to add words 283 private static int offset; 284 285 private static void printStyleWords(CompatibleHardcopyWriter writer, String line, String startStyle, 286 String endStyle, int style) throws IOException { 287 // determine how many bold or italic words to print 288 List<String> words = getSytleWords(line, startStyle, endStyle); 289 for (String s : words) { 290 if (s.contains(startStyle)) { 291 writer.setFontStyle(style); 292 s = s.substring(s.indexOf(startStyle) + startStyle.length()); 293 } 294 if (s.contains(endStyle)) { 295 writer.setFontStyle(style); 296 String text = s.substring(0, s.indexOf(endStyle)); 297 printWords(writer, text); 298 writer.setFontStyle(Font.PLAIN); 299 300 s = s.substring(s.indexOf(endStyle) + endStyle.length()); 301 } 302 // special case where the line contains both bold and italic words 303 if (s.contains(TEXT_ITALIC)) { 304 printStyleWords(writer, s, TEXT_ITALIC, TEXT_ITALIC_END, Font.ITALIC); 305 } else { 306 printWords(writer, s); 307 } 308 } 309 } 310 311 private static List<String> getSytleWords(String line, String startStyle, String endStyle) { 312 ArrayList<String> list = new ArrayList<>(); 313 String s; 314 while (line.length() > 0) { 315 if (line.contains(startStyle)) { 316 s = line.substring(0, line.indexOf(startStyle)); 317 if (s.length() > 0) { 318 list.add(s); 319 line = line.substring(line.indexOf(startStyle)); 320 } 321 if (line.contains(endStyle)) { 322 s = line.substring(line.indexOf(startStyle), 323 line.indexOf(endStyle, line.indexOf(startStyle)) + endStyle.length()); 324 list.add(s); 325 line = line.substring(line.indexOf(endStyle, line.indexOf(startStyle)) + endStyle.length()); 326 } else { 327 list.add(line); 328 break; 329 } 330 } else { 331 list.add(line); 332 break; //done 333 } 334 } 335 return list; 336 } 337 338 private static void printWords(CompatibleHardcopyWriter writer, String text) throws IOException { 339 if (text.contains(TEXT_COLOR_START)) { 340 printColorWords(writer, text); 341 } else if (text.contains(TEXT_COLOR_END)) { 342 printColorEnd(writer, text); 343 } else { 344 writeColorWords(writer, text); // bold or italic text 345 } 346 } 347 348 private static String printColor(CompatibleHardcopyWriter writer, String line) throws IOException { 349 offset = 0; 350 if (line.contains(TEXT_COLOR_START)) { 351 color = getTextColor(line); 352 // if no TEXT_COLOR_END then printing multiple lines in color 353 isPrintingColor = !line.contains(TEXT_COLOR_END); 354 // could be a color change when using two column format 355 if (line.contains(Character.toString(VERTICAL_LINE_CHAR))) { 356 String s = line.substring(0, line.indexOf(VERTICAL_LINE_CHAR)); 357 s = getOnlyText(s); 358 writer.write(color, s); // 1st half of line printed 359 // get the new color and text 360 line = line.substring(line.indexOf(VERTICAL_LINE_CHAR)); 361 color = getTextColor(line); 362 // pad out string 363 line = tabString(getOnlyText(line), s.length()); 364 } else if (writer.isMonospaced()) { 365 printColorWords(writer, line); 366 line = ""; // done 367 } else { 368 // simple case only one color 369 line = getOnlyText(line); 370 } 371 } else if (line.contains(TEXT_COLOR_END)) { 372 isPrintingColor = false; 373 if (writer.isMonospaced()) { 374 printColorEnd(writer, line); 375 line = ""; //done 376 } else { 377 line = getOnlyText(line); 378 } 379 } else if (!isPrintingColor) { 380 color = null; 381 } 382 return line; 383 } 384 385 private static void printColorEnd(CompatibleHardcopyWriter writer, String line) throws IOException { 386 String s = line.substring(0, line.indexOf(TEXT_COLOR_END)); 387 writeColorWords(writer, s); 388 s = line.substring(line.indexOf(TEXT_COLOR_END) + TEXT_COLOR_END.length()); 389 isPrintingColor = false; 390 color = null; 391 writeColorWords(writer, s); 392 } 393 394 // If monospaced font, it is possible to only color subset of words in the line 395 private static void printColorWords(CompatibleHardcopyWriter writer, String line) throws IOException { 396 for (String words : getColorWords(line)) { 397 color = getTextColor(words); 398 if (words.contains(TEXT_COLOR_START)) { 399 isPrintingColor = true; 400 } 401 if (words.contains(TEXT_COLOR_END)) { 402 isPrintingColor = false; 403 } 404 words = getOnlyText(words); 405 writeColorWords(writer, words); 406 if (!isPrintingColor) { 407 color = null; 408 } 409 } 410 } 411 412 private static List<String> getColorWords(String line) { 413 ArrayList<String> list = new ArrayList<>(); 414 String s; 415 while (line.length() > 0) { 416 if (line.contains(TEXT_COLOR_START)) { 417 s = line.substring(0, line.indexOf(TEXT_COLOR_START)); 418 if (s.length() > 0) { 419 list.add(s); 420 line = line.substring(line.indexOf(TEXT_COLOR_START)); 421 } 422 if (line.contains(TEXT_COLOR_END)) { 423 s = line.substring(line.indexOf(TEXT_COLOR_START), 424 line.indexOf(TEXT_COLOR_END, line.indexOf(TEXT_COLOR_START)) + TEXT_COLOR_END.length()); 425 list.add(s); 426 line = line.substring( 427 line.indexOf(TEXT_COLOR_END, line.indexOf(TEXT_COLOR_START)) + TEXT_COLOR_END.length()); 428 } else { 429 list.add(line); 430 break; 431 } 432 } else { 433 list.add(line); 434 break; //done 435 } 436 } 437 return list; 438 } 439 440 private static void writeColorWords(CompatibleHardcopyWriter writer, String s) throws IOException { 441 String text = tabString(s, offset); 442 writer.write(color, text); 443 offset = +text.length(); 444 } 445 446 private static String setFontSize(CompatibleHardcopyWriter writer, String line) { 447 if (line.contains(TEXT_SIZE_START)) { 448 int size = getFontSize(line); 449 writer.setFont(null, null, size); 450 } 451 if (line.contains(TEXT_SIZE_END)) { 452 isTextSizeDone = true; 453 } 454 return getTextSizeString(line); 455 } 456 457 private static final Logger log = LoggerFactory.getLogger(TrainPrintManifest.class); 458}