001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Dimension; 007import java.awt.image.BufferedImage; 008import java.io.File; 009import java.io.FileNotFoundException; 010import java.io.IOException; 011import java.nio.file.Files; 012import java.nio.file.Paths; 013import java.util.ArrayList; 014import java.util.List; 015import java.util.Locale; 016import java.util.Optional; 017import java.util.function.DoubleSupplier; 018import java.util.logging.Level; 019 020import javax.imageio.ImageIO; 021 022import org.openstreetmap.gui.jmapviewer.OsmMercator; 023import org.openstreetmap.josm.CLIModule; 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.data.Bounds; 026import org.openstreetmap.josm.data.ProjectionBounds; 027import org.openstreetmap.josm.data.coor.EastNorth; 028import org.openstreetmap.josm.data.coor.LatLon; 029import org.openstreetmap.josm.data.coor.conversion.LatLonParser; 030import org.openstreetmap.josm.data.osm.DataSet; 031import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; 032import org.openstreetmap.josm.data.projection.Projection; 033import org.openstreetmap.josm.data.projection.Projections; 034import org.openstreetmap.josm.gui.mappaint.RenderingHelper.StyleData; 035import org.openstreetmap.josm.io.IllegalDataException; 036import org.openstreetmap.josm.io.OsmReader; 037import org.openstreetmap.josm.spi.preferences.Config; 038import org.openstreetmap.josm.spi.preferences.MemoryPreferences; 039import org.openstreetmap.josm.tools.I18n; 040import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider; 041import org.openstreetmap.josm.tools.Logging; 042import org.openstreetmap.josm.tools.RightAndLefthandTraffic; 043 044import gnu.getopt.Getopt; 045import gnu.getopt.LongOpt; 046 047/** 048 * Command line interface for rendering osm data to an image file. 049 * 050 * @since 12906 051 */ 052public class RenderingCLI implements CLIModule { 053 054 /** 055 * The singleton instance of this class. 056 */ 057 public static final RenderingCLI INSTANCE = new RenderingCLI(); 058 059 private static final double PIXEL_PER_METER = 96 / 2.54 * 100; // standard value of 96 dpi display resolution 060 private static final int DEFAULT_MAX_IMAGE_SIZE = 20000; 061 062 private boolean argDebug; 063 private boolean argTrace; 064 private String argInput; 065 private String argOutput; 066 private List<StyleData> argStyles; 067 private Integer argZoom; 068 private Double argScale; 069 private Bounds argBounds; 070 private LatLon argAnchor; 071 private Double argWidthM; 072 private Double argHeightM; 073 private Integer argWidthPx; 074 private Integer argHeightPx; 075 private String argProjection; 076 private Integer argMaxImageSize; 077 078 private enum Option { 079 HELP(false, 'h'), 080 DEBUG(false, '*'), 081 TRACE(false, '*'), 082 INPUT(true, 'i'), 083 STYLE(true, 's'), 084 SETTING(true, '*'), 085 OUTPUT(true, 'o'), 086 ZOOM(true, 'z'), 087 SCALE(true, '*'), 088 BOUNDS(true, 'b'), 089 ANCHOR(true, '*'), 090 WIDTH_M(true, '*'), 091 HEIGHT_M(true, '*'), 092 WIDTH_PX(true, '*'), 093 HEIGHT_PX(true, '*'), 094 PROJECTION(true, '*'), 095 MAX_IMAGE_SIZE(true, '*'); 096 097 private final String name; 098 private final boolean requiresArg; 099 private final char shortOption; 100 101 Option(boolean requiresArgument, char shortOption) { 102 this.name = name().toLowerCase(Locale.US).replace('_', '-'); 103 this.requiresArg = requiresArgument; 104 this.shortOption = shortOption; 105 } 106 107 /** 108 * Replies the option name 109 * @return The option name, in lowercase 110 */ 111 public String getName() { 112 return name; 113 } 114 115 /** 116 * Determines if this option requires an argument. 117 * @return {@code true} if this option requires an argument, {@code false} otherwise 118 */ 119 public boolean requiresArgument() { 120 return requiresArg; 121 } 122 123 /** 124 * Replies the short option (single letter) associated with this option. 125 * @return the short option or '*' if there is no short option 126 */ 127 public char getShortOption() { 128 return shortOption; 129 } 130 131 LongOpt toLongOpt() { 132 return new LongOpt(getName(), requiresArgument() ? LongOpt.REQUIRED_ARGUMENT : LongOpt.NO_ARGUMENT, null, getShortOption()); 133 } 134 } 135 136 /** 137 * Data class to hold return values for {@link #determineRenderingArea(DataSet)}. 138 * 139 * Package private access for unit tests. 140 */ 141 static class RenderingArea { 142 public Bounds bounds; 143 public double scale; // in east-north units per pixel (unlike the --scale option, which is in meter per meter) 144 } 145 146 RenderingCLI() { 147 // hide constructor (package private access for unit tests) 148 } 149 150 @Override 151 public String getActionKeyword() { 152 return "render"; 153 } 154 155 @Override 156 public void processArguments(String[] argArray) { 157 try { 158 parseArguments(argArray); 159 initialize(); 160 DataSet ds = loadDataset(); 161 RenderingArea area = determineRenderingArea(ds); 162 RenderingHelper rh = new RenderingHelper(ds, area.bounds, area.scale, argStyles); 163 checkPreconditions(rh); 164 BufferedImage image = rh.render(); 165 writeImageToFile(image); 166 } catch (FileNotFoundException e) { 167 if (Logging.isDebugEnabled()) { 168 e.printStackTrace(); 169 } 170 System.err.println(tr("Error - file not found: ''{0}''", e.getMessage())); 171 System.exit(1); 172 } catch (IllegalArgumentException | IllegalDataException | IOException e) { 173 if (Logging.isDebugEnabled()) { 174 e.printStackTrace(); 175 } 176 if (e.getMessage() != null) { 177 System.err.println(tr("Error: {0}", e.getMessage())); 178 } 179 System.exit(1); 180 } 181 System.exit(0); 182 } 183 184 /** 185 * Parse command line arguments and do some low-level error checking. 186 * @param argArray the arguments array 187 */ 188 void parseArguments(String[] argArray) { 189 Getopt.setI18nHandler(I18n::tr); 190 Logging.setLogLevel(Level.INFO); 191 192 LongOpt[] opts = new LongOpt[Option.values().length]; 193 StringBuilder optString = new StringBuilder(); 194 for (Option o : Option.values()) { 195 opts[o.ordinal()] = o.toLongOpt(); 196 if (o.getShortOption() != '*') { 197 optString.append(o.getShortOption()); 198 if (o.requiresArgument()) { 199 optString.append(':'); 200 } 201 } 202 } 203 204 Getopt getopt = new Getopt("JOSM rendering", argArray, optString.toString(), opts); 205 206 StyleData currentStyle = new StyleData(); 207 argStyles = new ArrayList<>(); 208 209 int c; 210 while ((c = getopt.getopt()) != -1) { 211 switch (c) { 212 case 'h': 213 showHelp(); 214 System.exit(0); 215 case 'i': 216 argInput = getopt.getOptarg(); 217 break; 218 case 's': 219 if (currentStyle.styleUrl != null) { 220 argStyles.add(currentStyle); 221 currentStyle = new StyleData(); 222 } 223 currentStyle.styleUrl = getopt.getOptarg(); 224 break; 225 case 'o': 226 argOutput = getopt.getOptarg(); 227 break; 228 case 'z': 229 try { 230 argZoom = Integer.valueOf(getopt.getOptarg()); 231 } catch (NumberFormatException nfe) { 232 throw new IllegalArgumentException( 233 tr("Expected integer number for option {0}, but got ''{1}''", "--zoom", getopt.getOptarg()), nfe); 234 } 235 if (argZoom < 0) 236 throw new IllegalArgumentException( 237 tr("Expected integer number >= 0 for option {0}, but got ''{1}''", "--zoom", getopt.getOptarg())); 238 break; 239 case 'b': 240 if (!"auto".equals(getopt.getOptarg())) { 241 try { 242 argBounds = new Bounds(getopt.getOptarg(), ",", Bounds.ParseMethod.LEFT_BOTTOM_RIGHT_TOP, false); 243 } catch (IllegalArgumentException iae) { // NOPMD 244 throw new IllegalArgumentException(tr("Unable to parse {0} parameter: {1}", "--bounds", iae.getMessage()), iae); 245 } 246 } 247 break; 248 case '*': 249 switch (Option.values()[getopt.getLongind()]) { 250 case DEBUG: 251 argDebug = true; 252 break; 253 case TRACE: 254 argTrace = true; 255 break; 256 case SETTING: 257 String keyval = getopt.getOptarg(); 258 String[] comp = keyval.split(":"); 259 if (comp.length != 2) 260 throw new IllegalArgumentException( 261 tr("Expected key and value, separated by '':'' character for option {0}, but got ''{1}''", 262 "--setting", getopt.getOptarg())); 263 currentStyle.settings.put(comp[0].trim(), comp[1].trim()); 264 break; 265 case SCALE: 266 try { 267 argScale = JosmDecimalFormatSymbolsProvider.parseDouble(getopt.getOptarg()); 268 } catch (NumberFormatException nfe) { 269 throw new IllegalArgumentException( 270 tr("Expected floating point number for option {0}, but got ''{1}''", "--scale", getopt.getOptarg()), nfe); 271 } 272 break; 273 case ANCHOR: 274 String[] parts = getopt.getOptarg().split(","); 275 if (parts.length != 2) 276 throw new IllegalArgumentException( 277 tr("Expected two coordinates, separated by comma, for option {0}, but got ''{1}''", 278 "--anchor", getopt.getOptarg())); 279 try { 280 double lon = LatLonParser.parseCoordinate(parts[0]); 281 double lat = LatLonParser.parseCoordinate(parts[1]); 282 argAnchor = new LatLon(lat, lon); 283 } catch (IllegalArgumentException iae) { // NOPMD 284 throw new IllegalArgumentException(tr("In option {0}: {1}", "--anchor", iae.getMessage()), iae); 285 } 286 break; 287 case WIDTH_M: 288 try { 289 argWidthM = JosmDecimalFormatSymbolsProvider.parseDouble(getopt.getOptarg()); 290 } catch (NumberFormatException nfe) { 291 throw new IllegalArgumentException( 292 tr("Expected floating point number for option {0}, but got ''{1}''", "--width-m", getopt.getOptarg()), nfe); 293 } 294 if (argWidthM <= 0) throw new IllegalArgumentException( 295 tr("Expected floating point number > 0 for option {0}, but got ''{1}''", "--width-m", getopt.getOptarg())); 296 break; 297 case HEIGHT_M: 298 try { 299 argHeightM = JosmDecimalFormatSymbolsProvider.parseDouble(getopt.getOptarg()); 300 } catch (NumberFormatException nfe) { 301 throw new IllegalArgumentException( 302 tr("Expected floating point number for option {0}, but got ''{1}''", "--height-m", getopt.getOptarg()), nfe); 303 } 304 if (argHeightM <= 0) throw new IllegalArgumentException( 305 tr("Expected floating point number > 0 for option {0}, but got ''{1}''", "--width-m", getopt.getOptarg())); 306 break; 307 case WIDTH_PX: 308 try { 309 argWidthPx = Integer.valueOf(getopt.getOptarg()); 310 } catch (NumberFormatException nfe) { 311 throw new IllegalArgumentException( 312 tr("Expected integer number for option {0}, but got ''{1}''", "--width-px", getopt.getOptarg()), nfe); 313 } 314 if (argWidthPx <= 0) throw new IllegalArgumentException( 315 tr("Expected integer number > 0 for option {0}, but got ''{1}''", "--width-px", getopt.getOptarg())); 316 break; 317 case HEIGHT_PX: 318 try { 319 argHeightPx = Integer.valueOf(getopt.getOptarg()); 320 } catch (NumberFormatException nfe) { 321 throw new IllegalArgumentException( 322 tr("Expected integer number for option {0}, but got ''{1}''", "--height-px", getopt.getOptarg()), nfe); 323 } 324 if (argHeightPx <= 0) throw new IllegalArgumentException( 325 tr("Expected integer number > 0 for option {0}, but got ''{1}''", "--height-px", getopt.getOptarg())); 326 break; 327 case PROJECTION: 328 argProjection = getopt.getOptarg(); 329 break; 330 case MAX_IMAGE_SIZE: 331 try { 332 argMaxImageSize = Integer.valueOf(getopt.getOptarg()); 333 } catch (NumberFormatException nfe) { 334 throw new IllegalArgumentException( 335 tr("Expected integer number for option {0}, but got ''{1}''", "--max-image-size", getopt.getOptarg()), nfe); 336 } 337 if (argMaxImageSize < 0) throw new IllegalArgumentException( 338 tr("Expected integer number >= 0 for option {0}, but got ''{1}''", "--max-image-size", getopt.getOptarg())); 339 break; 340 default: 341 throw new AssertionError("Unexpected option index: " + getopt.getLongind()); 342 } 343 break; 344 case '?': 345 throw new IllegalArgumentException(); // getopt error 346 default: 347 throw new AssertionError("Unrecognized option: " + c); 348 } 349 } 350 if (currentStyle.styleUrl != null) { 351 argStyles.add(currentStyle); 352 } 353 } 354 355 /** 356 * Displays help on the console 357 */ 358 public static void showHelp() { 359 System.out.println(getHelp()); 360 } 361 362 private static String getHelp() { 363 return tr("JOSM rendering command line interface")+"\n\n"+ 364 tr("Usage")+":\n"+ 365 "\tjava -jar josm.jar render <options>\n\n"+ 366 tr("Description")+":\n"+ 367 tr("Renders data and saves the result to an image file.")+"\n\n"+ 368 tr("Options")+":\n"+ 369 "\t--help|-h "+tr("Show this help")+"\n"+ 370 "\t--input|-i <file> "+tr("Input data file name (.osm)")+"\n"+ 371 "\t--output|-o <file> "+tr("Output image file name (.png); defaults to ''{0}''", "out.png")+"\n"+ 372 "\t--style|-s <file> "+tr("Style file to use for rendering (.mapcss or .zip)")+"\n"+ 373 "\t "+tr("This option can be repeated to load multiple styles.")+"\n"+ 374 "\t--setting <key>:<value> "+tr("Style setting (in JOSM accessible in the style list dialog right click menu)")+"\n"+ 375 "\t "+tr("Applies to the last style loaded with the {0} option.", "--style")+"\n"+ 376 "\t--zoom|-z <lvl> "+tr("Select zoom level to render. (integer value, 0=entire earth, 18=street level)")+"\n"+ 377 "\t--scale <scale> "+tr("Select the map scale")+"\n"+ 378 "\t "+tr("A value of 10000 denotes a scale of 1:10000 (1 cm on the map equals 100 m on the ground; " 379 + "display resolution: 96 dpi)")+"\n"+ 380 "\t "+tr("Options {0} and {1} are mutually exclusive.", "--zoom", "--scale")+"\n"+ 381 "\t--bounds|-b auto|<min_lon>,<min_lat>,<max_lon>,<max_lat>\n"+ 382 "\t "+tr("Area to render, default value is ''{0}''", "auto")+"\n"+ 383 "\t "+tr("With keyword ''{0}'', the downloaded area in the .osm input file will be used (if recorded).", 384 "auto")+"\n"+ 385 "\t--anchor <lon>,<lat> "+tr("Specify bottom left corner of the rendering area")+"\n"+ 386 "\t "+tr("Used in combination with width and height options to determine the area to render.")+"\n"+ 387 "\t--width-m <number> "+tr("Width of the rendered area, in meter")+"\n"+ 388 "\t--height-m <number> "+tr("Height of the rendered area, in meter")+"\n"+ 389 "\t--width-px <number> "+tr("Width of the target image, in pixel")+"\n"+ 390 "\t--height-px <number> "+tr("Height of the target image, in pixel")+"\n"+ 391 "\t--projection <code> "+tr("Projection to use, default value ''{0}'' (web-Mercator)", "epsg:3857")+"\n"+ 392 "\t--max-image-size <number> "+tr("Maximum image width/height in pixel (''{0}'' means no limit), default value: {1}", 393 0, Integer.toString(DEFAULT_MAX_IMAGE_SIZE))+"\n"+ 394 "\n"+ 395 tr("To specify the rendered area and scale, the options can be combined in various ways")+":\n"+ 396 " * --bounds (--zoom|--scale|--width-px|--height-px)\n"+ 397 " * --anchor (--width-m|--width-px) (--height-m|--height-px) (--zoom|--scale)\n"+ 398 " * --anchor --width-m --height-m (--width-px|--height-px)\n"+ 399 " * --anchor --width-px --height-px (--width-m|--height-m)\n"+ 400 tr("If neither ''{0}'' nor ''{1}'' is given, the default value {2} takes effect " 401 + "and the bounds of the download area in the .osm input file are used.", 402 "bounds", "anchor", "--bounds=auto")+"\n\n"+ 403 tr("Examples")+":\n"+ 404 " java -jar josm.jar render -i data.osm -s style.mapcss -z 16\n"+ 405 " josm render -i data.osm -s style.mapcss --scale 5000\n"+ 406 " josm render -i data.osm -s style.mapcss -z 16 -o image.png\n"+ 407 " josm render -i data.osm -s elemstyles.mapcss --setting hide_icons:false -z 16\n"+ 408 " josm render -i data.osm -s style.mapcss -s another_style.mapcss -z 16 -o image.png\n"+ 409 " josm render -i data.osm -s style.mapcss --bounds 21.151,51.401,21.152,51.402 -z 16\n"+ 410 " josm render -i data.osm -s style.mapcss --anchor 21.151,51.401 --width-m 500 --height-m 300 -z 16\n"+ 411 " josm render -i data.osm -s style.mapcss --anchor 21.151,51.401 --width-m 500 --height-m 300 --width-px 1800\n"+ 412 " josm render -i data.osm -s style.mapcss --scale 5000 --projection epsg:4326\n"; 413 } 414 415 /** 416 * Initialization. 417 * 418 * Requires arguments to be parsed already ({@link #parseArguments(java.lang.String[])}). 419 */ 420 void initialize() { 421 Logging.setLogLevel(getLogLevel()); 422 423 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance()); // for right-left-hand traffic cache file 424 Config.setPreferencesInstance(new MemoryPreferences()); 425 Config.getPref().putBoolean("mappaint.auto_reload_local_styles", false); // unnecessary to listen for external changes 426 String projCode = Optional.ofNullable(argProjection).orElse("epsg:3857"); 427 Main.setProjection(Projections.getProjectionByCode(projCode.toUpperCase(Locale.US))); 428 429 RightAndLefthandTraffic.initialize(); 430 } 431 432 private Level getLogLevel() { 433 if (argTrace) { 434 return Logging.LEVEL_TRACE; 435 } else if (argDebug) { 436 return Logging.LEVEL_DEBUG; 437 } else { 438 return Logging.LEVEL_INFO; 439 } 440 } 441 442 /** 443 * Find the area to render and the scale, given certain command line options and the dataset. 444 * @param ds the dataset 445 * @return area to render and the scale 446 */ 447 RenderingArea determineRenderingArea(DataSet ds) { 448 449 Projection proj = Main.getProjection(); 450 Double scale = null; // scale in east-north units per pixel 451 if (argZoom != null) { 452 scale = OsmMercator.EARTH_RADIUS * Math.PI * 2 / Math.pow(2, argZoom) / OsmMercator.DEFAUL_TILE_SIZE / proj.getMetersPerUnit(); 453 } 454 Bounds bounds = argBounds; 455 ProjectionBounds pb = null; 456 457 if (bounds == null) { 458 if (argAnchor != null) { 459 EastNorth projAnchor = proj.latlon2eastNorth(argAnchor); 460 461 double enPerMeter = Double.NaN; 462 DoubleSupplier getEnPerMeter = () -> { 463 double shiftMeter = 10; 464 EastNorth projAnchorShifted = projAnchor.add( 465 shiftMeter / proj.getMetersPerUnit(), shiftMeter / proj.getMetersPerUnit()); 466 LatLon anchorShifted = proj.eastNorth2latlon(projAnchorShifted); 467 return projAnchor.distance(projAnchorShifted) / argAnchor.greatCircleDistance(anchorShifted); 468 }; 469 470 if (scale == null) { 471 if (argScale != null) { 472 enPerMeter = getEnPerMeter.getAsDouble(); 473 scale = argScale * enPerMeter / PIXEL_PER_METER; 474 } else if (argWidthM != null && argWidthPx != null) { 475 enPerMeter = getEnPerMeter.getAsDouble(); 476 scale = argWidthM / argWidthPx * enPerMeter; 477 } else if (argHeightM != null && argHeightPx != null) { 478 enPerMeter = getEnPerMeter.getAsDouble(); 479 scale = argHeightM / argHeightPx * enPerMeter; 480 } else { 481 throw new IllegalArgumentException( 482 tr("Argument {0} given, but scale cannot be determined from remaining arguments", "--anchor")); 483 } 484 } 485 486 double widthEn; 487 if (argWidthM != null) { 488 if (Double.isNaN(enPerMeter)) { 489 enPerMeter = getEnPerMeter.getAsDouble(); 490 } 491 widthEn = argWidthM * enPerMeter; 492 } else if (argWidthPx != null) { 493 widthEn = argWidthPx * scale; 494 } else { 495 throw new IllegalArgumentException( 496 tr("Argument {0} given, expected {1} or {2}", "--anchor", "--width-m", "--width-px")); 497 } 498 499 double heightEn; 500 if (argHeightM != null) { 501 if (Double.isNaN(enPerMeter)) { 502 enPerMeter = getEnPerMeter.getAsDouble(); 503 } 504 heightEn = argHeightM * enPerMeter; 505 } else if (argHeightPx != null) { 506 heightEn = argHeightPx * scale; 507 } else { 508 throw new IllegalArgumentException( 509 tr("Argument {0} given, expected {1} or {2}", "--anchor", "--height-m", "--height-px")); 510 } 511 pb = new ProjectionBounds(projAnchor); 512 pb.extend(new EastNorth(projAnchor.east() + widthEn, projAnchor.north() + heightEn)); 513 bounds = new Bounds(proj.eastNorth2latlon(pb.getMin()), false); 514 bounds.extend(proj.eastNorth2latlon(pb.getMax())); 515 } else { 516 if (ds.getDataSourceBounds().isEmpty()) { 517 throw new IllegalArgumentException(tr("{0} mode, but no bounds found in osm data input file", "--bounds=auto")); 518 } 519 bounds = ds.getDataSourceBounds().get(0); 520 } 521 } 522 523 if (pb == null) { 524 pb = new ProjectionBounds(); 525 pb.extend(proj.latlon2eastNorth(bounds.getMin())); 526 pb.extend(proj.latlon2eastNorth(bounds.getMax())); 527 } 528 529 if (scale == null) { 530 if (argScale != null) { 531 double enPerMeter = pb.getMin().distance(pb.getMax()) / bounds.getMin().greatCircleDistance(bounds.getMax()); 532 scale = argScale * enPerMeter / PIXEL_PER_METER; 533 } else if (argWidthPx != null) { 534 scale = (pb.maxEast - pb.minEast) / argWidthPx; 535 } else if (argHeightPx != null) { 536 scale = (pb.maxNorth - pb.minNorth) / argHeightPx; 537 } else { 538 throw new IllegalArgumentException( 539 tr("Unable to determine scale, one of the options {0}, {1}, {2} or {3} expected", 540 "--zoom", "--scale", "--width-px", "--height-px")); 541 } 542 } 543 544 RenderingArea ra = new RenderingArea(); 545 ra.bounds = bounds; 546 ra.scale = scale; 547 return ra; 548 } 549 550 private DataSet loadDataset() throws IOException, IllegalDataException { 551 if (argInput == null) { 552 throw new IllegalArgumentException(tr("Missing argument - input data file ({0})", "--input|-i")); 553 } 554 try { 555 return OsmReader.parseDataSet(Files.newInputStream(Paths.get(argInput)), null); 556 } catch (IllegalDataException e) { 557 throw new IllegalDataException(tr("In .osm data file ''{0}'' - ", argInput) + e.getMessage(), e); 558 } 559 } 560 561 private void checkPreconditions(RenderingHelper rh) { 562 if (argStyles.isEmpty()) 563 throw new IllegalArgumentException(tr("Missing argument - at least one style expected ({0})", "--style")); 564 565 Dimension imgSize = rh.getImageSize(); 566 Logging.debug("image size (px): {0}x{1}", imgSize.width, imgSize.height); 567 int maxSize = Optional.ofNullable(argMaxImageSize).orElse(DEFAULT_MAX_IMAGE_SIZE); 568 if (maxSize != 0 && (imgSize.width > maxSize || imgSize.height > maxSize)) { 569 throw new IllegalArgumentException( 570 tr("Image dimensions ({0}x{1}) exceeds maximum image size {2} (use option {3} to change limit)", 571 imgSize.width, imgSize.height, maxSize, "--max-image-size")); 572 } 573 } 574 575 private void writeImageToFile(BufferedImage image) throws IOException { 576 String output = Optional.ofNullable(argOutput).orElse("out.png"); 577 ImageIO.write(image, "png", new File(output)); 578 } 579}