From 3db7eed11e63a8284d401bd1e8db9616ac0f94df Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 10 Jul 2020 12:08:18 -0400 Subject: [PATCH] Raw HTML density support (#676) Various raw HTML fixes - Uses density defaults closer to expected raw usage - Address issue with zoom allowance on large captures - Allow raw prints to use Pixel density - Further improvements to calcZoom method + fallback to 1x zoom on failures Co-authored-by: Berenz --- js/qz-tray.js | 2 +- src/qz/printer/LanguageType.java | 23 ++++++--- src/qz/printer/PrintOptions.java | 12 +---- src/qz/printer/action/PrintHTML.java | 18 +++++-- src/qz/printer/action/PrintRaw.java | 42 +++++++++++++--- src/qz/printer/action/WebApp.java | 25 ++++----- src/qz/printer/action/WebAppModel.java | 12 ++--- test/qz/printer/action/WebAppTest.java | 70 ++++++++++++++------------ 8 files changed, 122 insertions(+), 82 deletions(-) diff --git a/js/qz-tray.js b/js/qz-tray.js index 7a0195c37..a3aa3546e 100644 --- a/js/qz-tray.js +++ b/js/qz-tray.js @@ -1269,7 +1269,7 @@ var qz = (function() { * * @param {Object} options Default options used by printer configs if not overridden. * - * @param {Object} [option.bounds=null] Bounding box rectangle. + * @param {Object} [options.bounds=null] Bounding box rectangle. * @param {number} [options.bounds.x=0] Distance from left for bounding box starting corner * @param {number} [options.bounds.y=0] Distance from top for bounding box starting corner * @param {number} [options.bounds.width=0] Width of bounding box diff --git a/src/qz/printer/LanguageType.java b/src/qz/printer/LanguageType.java index 795a3fd05..b5eb01451 100644 --- a/src/qz/printer/LanguageType.java +++ b/src/qz/printer/LanguageType.java @@ -20,21 +20,23 @@ */ public enum LanguageType { - ZPL(false, true, "ZPL", "ZPL2", "ZPLII", "ZEBRA"), - EPL(true, true, "EPL", "EPL2", "EPLII"), - CPCL(false, true), - ESCP(false, false, "ESCP", "ESCP2", "ESCPOS", "ESC", "ESC/P", "ESC/P2", "ESCP/P2", "ESC/POS", "ESC\\P", "EPSON"), - EVOLIS(false, false), - UNKNOWN(false, false); + ZPL(false, true, 203, "ZPL", "ZPL2", "ZPLII", "ZEBRA"), + EPL(true, true, 203, "EPL", "EPL2", "EPLII"), + CPCL(false, true, 203), + ESCP(false, false, 180, "ESCP", "ESCP2", "ESCPOS", "ESC", "ESC/P", "ESC/P2", "ESCP/P2", "ESC/POS", "ESC\\P", "EPSON"), + EVOLIS(false, false, 300), + UNKNOWN(false, false, 72); private boolean imgOutputInvert = false; private boolean imgWidthValidated = false; + private double defaultDensity = 72; private List altNames; - LanguageType(boolean imgOutputInvert, boolean imgWidthValidated, String... altNames) { + LanguageType(boolean imgOutputInvert, boolean imgWidthValidated, double defaultDensity, String... altNames) { this.imgOutputInvert = imgOutputInvert; this.imgWidthValidated = imgWidthValidated; + this.defaultDensity = defaultDensity; this.altNames = new ArrayList<>(); Collections.addAll(this.altNames, altNames); @@ -42,7 +44,7 @@ public enum LanguageType { public static LanguageType getType(String type) { for(LanguageType lang : LanguageType.values()) { - if (type.equalsIgnoreCase(lang.name()) || lang.altNames.contains(type)) { + if (lang.name().equalsIgnoreCase(type) || lang.altNames.contains(type)) { return lang; } } @@ -72,4 +74,9 @@ public boolean requiresImageOutputInverted() { public boolean requiresImageWidthValidated() { return imgWidthValidated; } + + public double getDefaultDensity() { + return defaultDensity; + } + } diff --git a/src/qz/printer/PrintOptions.java b/src/qz/printer/PrintOptions.java index a73504114..2efbebdaa 100644 --- a/src/qz/printer/PrintOptions.java +++ b/src/qz/printer/PrintOptions.java @@ -55,9 +55,6 @@ public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities if (!configOpts.isNull("endOfDoc")) { rawOptions.endOfDoc = configOpts.optString("endOfDoc", null); } - if (!configOpts.isNull("language")) { - rawOptions.language = configOpts.optString("language", null); - } if (!configOpts.isNull("perSpool")) { try { rawOptions.perSpool = configOpts.getInt("perSpool"); } catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "perSpool", configOpts.opt("perSpool")); } @@ -160,7 +157,7 @@ public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities bestRes = pr; } } - if(bestRes != null) { + if (bestRes != null) { psOptions.density = bestRes.getFeedResolution(psOptions.units.resSyntax); psOptions.crossDensity = bestRes.getCrossFeedResolution(psOptions.units.resSyntax); } else { @@ -173,7 +170,7 @@ public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities lowestRes = pr; } } - if(lowestRes != null) { + if (lowestRes != null) { psOptions.density = lowestRes.getFeedResolution(psOptions.units.resSyntax); psOptions.crossDensity = lowestRes.getCrossFeedResolution(psOptions.units.resSyntax); } else { @@ -362,7 +359,6 @@ public class Raw { private boolean altPrinting = false; //Alternate printing for linux systems private String encoding = null; //Text encoding / charset private String endOfDoc = null; //End of document character - private String language = null; //Printer language private int perSpool = 1; //Pages per spool private int copies = 1; //Job copies private String jobName = null; //Job name @@ -380,10 +376,6 @@ public String getEndOfDoc() { return endOfDoc; } - public String getLanguage() { - return language; - } - public int getPerSpool() { return perSpool; } diff --git a/src/qz/printer/action/PrintHTML.java b/src/qz/printer/action/PrintHTML.java index c01195129..0ed9d8abf 100644 --- a/src/qz/printer/action/PrintHTML.java +++ b/src/qz/printer/action/PrintHTML.java @@ -112,10 +112,10 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce JSONObject dataOpt = data.getJSONObject("options"); if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) { - pageWidth = dataOpt.optDouble("pageWidth") * (72.0 / pxlOpts.getUnits().as1Inch()); + pageWidth = dataOpt.optDouble("pageWidth") * convertFactor; } if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) { - pageHeight = dataOpt.optDouble("pageHeight") * (72.0 / pxlOpts.getUnits().as1Inch()); + pageHeight = dataOpt.optDouble("pageHeight") * convertFactor; } } @@ -141,7 +141,19 @@ public void print(PrintOutput output, PrintOptions options) throws PrinterExcept for(WebAppModel model : models) { try { images.add(WebApp.raster(model)); } catch(Throwable t) { - throw new PrinterException(t.getMessage()); + if (model.getZoom() > 1 && t instanceof IllegalArgumentException) { + //probably a unrecognized image loader error, try at default zoom + try { + log.warn("Capture failed with increased zoom, attempting with default value"); + model.setZoom(1); + images.add(WebApp.raster(model)); + } + catch(Throwable tt) { + throw new PrinterException(tt.getMessage()); + } + } else { + throw new PrinterException(t.getMessage()); + } } } diff --git a/src/qz/printer/action/PrintRaw.java b/src/qz/printer/action/PrintRaw.java index b80867eb7..60f89b273 100644 --- a/src/qz/printer/action/PrintRaw.java +++ b/src/qz/printer/action/PrintRaw.java @@ -38,6 +38,7 @@ import javax.print.attribute.standard.JobName; import javax.print.event.PrintJobEvent; import javax.print.event.PrintJobListener; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.net.Socket; @@ -108,7 +109,7 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce try { switch(format) { case HTML: - commands.append(getHtmlWrapper(cmd, opt, flavor != PrintingUtilities.Flavor.PLAIN).getImageCommand(opt)); + commands.append(getHtmlWrapper(cmd, opt, flavor != PrintingUtilities.Flavor.PLAIN, options.getPixelOptions()).getImageCommand(opt)); break; case IMAGE: commands.append(getImageWrapper(cmd, opt, flavor != PrintingUtilities.Flavor.BASE64).getImageCommand(opt)); @@ -187,18 +188,47 @@ private ImageWrapper getPdfWrapper(String data, JSONObject opt, boolean fromFile return getWrapper(bi, opt); } - private ImageWrapper getHtmlWrapper(String data, JSONObject opt, boolean fromFile) throws IOException { + private ImageWrapper getHtmlWrapper(String data, JSONObject opt, boolean fromFile, PrintOptions.Pixel pxlOpts) throws IOException { + double density = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()); + if (density <= 1) { + density = LanguageType.getType(opt.optString("language")).getDefaultDensity(); + } + double pageZoom = density / 72.0; + + double pageWidth = opt.optInt("pageWidth") / density * 72; + double pageHeight = opt.optInt("pageHeight") / density * 72; + BufferedImage bi; + WebAppModel model = new WebAppModel(data, !fromFile, pageWidth, pageHeight, false, pageZoom); try { WebApp.initialize(); //starts if not already started - - WebAppModel model = new WebAppModel(data, !fromFile, opt.optInt("pageWidth"), opt.optInt("pageHeight"), false, opt.optDouble("zoom")); bi = WebApp.raster(model); + + // down scale back from web density + double scaleFactor = opt.optDouble("pageWidth", 0) / bi.getWidth(); + BufferedImage scaled = new BufferedImage((int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = scaled.createGraphics(); + g2d.drawImage(bi, 0, 0, (int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), null); + g2d.dispose(); + bi = scaled; } catch(Throwable t) { - log.error("Failed to capture html raster"); - throw new IOException(t); + if (model.getZoom() > 1 && t instanceof IllegalArgumentException) { + //probably a unrecognized image loader error, try at default zoom + try { + log.warn("Capture failed with increased zoom, attempting with default value"); + model.setZoom(1); + bi = WebApp.raster(model); + } + catch(Throwable tt) { + log.error("Failed to capture html raster"); + throw new IOException(tt); + } + } else { + log.error("Failed to capture html raster"); + throw new IOException(t); + } } return getWrapper(bi, opt); diff --git a/src/qz/printer/action/WebApp.java b/src/qz/printer/action/WebApp.java index 144c365fd..2dd6c1797 100644 --- a/src/qz/printer/action/WebApp.java +++ b/src/qz/printer/action/WebApp.java @@ -83,15 +83,6 @@ public class WebApp extends Application { base.getAttributes().setNamedItem(applied); } - // find and set page zoom for increased quality - double usableZoom = calculateSupportedZoom(pageWidth, pageHeight); - if (usableZoom < pageZoom) { - log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom); - pageZoom = usableZoom; - } - webView.setZoom(pageZoom); - log.trace("Zooming in by x{} for increased quality", pageZoom); - //width was resized earlier (for responsive html), then calculate the best fit height // FIXME: Should only be needed when height is unknown but fixes blank vector prints double fittedHeight = findHeight(); @@ -101,6 +92,15 @@ public class WebApp extends Application { pageHeight = fittedHeight; } + // find and set page zoom for increased quality + double usableZoom = calculateSupportedZoom(pageWidth, pageHeight); + if (usableZoom < pageZoom) { + log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom); + pageZoom = usableZoom; + } + webView.setZoom(pageZoom); + log.trace("Zooming in by x{} for increased quality", pageZoom); + adjustSize(pageWidth * pageZoom, pageHeight * pageZoom); //need to check for height again as resizing can cause partial results @@ -327,7 +327,6 @@ public void pulse() { unlatch(null); } catch(Exception e) { - log.error("Caught during snapshot"); unlatch(e); } finally { @@ -429,9 +428,11 @@ private static double calculateSupportedZoom(double width, double height) { long memory = Runtime.getRuntime().maxMemory(); int allowance = (memory / 1048576L) > 1024? 3:2; if (headless) { allowance--; } - long availSpace = (long)((memory << allowance) / 72d); + long availSpace = memory << allowance; - return Math.sqrt(availSpace / (width * height)); + // Memory needed for print is roughly estimated as + // (width * height) [pixels needed] * (pageZoom * 72d) [print density used] * 3 [rgb channels] + return Math.sqrt(availSpace / ((width * height) * (pageZoom * 72d) * 3)); } /** diff --git a/src/qz/printer/action/WebAppModel.java b/src/qz/printer/action/WebAppModel.java index 2195fd4f6..0ed8ceed7 100644 --- a/src/qz/printer/action/WebAppModel.java +++ b/src/qz/printer/action/WebAppModel.java @@ -15,7 +15,7 @@ public WebAppModel(String source, boolean plainText, double width, double height this.plainText = plainText; this.width = width; this.height = height; - this.webWidth = width * (96d/72d); + this.webWidth = width * (96d / 72d); this.webHeight = height * (96d / 72d); this.isScaled = isScaled; this.zoom = zoom; @@ -43,6 +43,7 @@ public double getWidth() { public void setWidth(double width) { this.width = width; + this.webWidth = width * (96d / 72d); } public double getHeight() { @@ -51,24 +52,17 @@ public double getHeight() { public void setHeight(double height) { this.height = height; + this.webHeight = height * (96d / 72d); } public double getWebWidth() { return webWidth; } - public void setWebWidth(double webWidth) { - this.webWidth = webWidth; - } - public double getWebHeight() { return webHeight; } - public void setWebHeight(double webHeight) { - this.webHeight = webHeight; - } - public boolean isScaled() { return isScaled; } diff --git a/test/qz/printer/action/WebAppTest.java b/test/qz/printer/action/WebAppTest.java index e0484d112..e5e5fd48f 100644 --- a/test/qz/printer/action/WebAppTest.java +++ b/test/qz/printer/action/WebAppTest.java @@ -22,41 +22,45 @@ public class WebAppTest { private static final Path RASTER_OUTPUT_DIR = Paths.get("./out"); // see ant ${out.dir} private static final String RASTER_OUTPUT_FORMAT = "png"; - public static void main(String[] args) throws Throwable { - WebApp.initialize(); - - // RASTER// - - cleanup(); - int rasterKnownHeightTests = 1000; - if (args.length > 0) { rasterKnownHeightTests = Integer.parseInt(args[1]); } - int rasterFittedHeightTests = 1000; - if (args.length > 1) { rasterFittedHeightTests = Integer.parseInt(args[2]); } - - if (!testRasterKnownSize(rasterKnownHeightTests)) { - log.error("Testing well defined sizes failed"); - } else if (!testRasterFittedSize(rasterFittedHeightTests)) { - log.error("Testing fit to height sizing failed"); - } else { - log.info("All raster tests passed"); - } + public static void main(String[] args) { + try { + WebApp.initialize(); + cleanup(); + + // RASTER// + + int rasterKnownHeightTests = 1000; + if (args.length > 1) { rasterKnownHeightTests = Integer.parseInt(args[1]); } + int rasterFittedHeightTests = 1000; + if (args.length > 2) { rasterFittedHeightTests = Integer.parseInt(args[2]); } + + if (!testRasterKnownSize(rasterKnownHeightTests)) { + log.error("Testing well defined sizes failed"); + } else if (!testRasterFittedSize(rasterFittedHeightTests)) { + log.error("Testing fit to height sizing failed"); + } else { + log.info("All raster tests passed"); + } - // VECTOR // + // VECTOR // - int vectorKnownHeightPrints = 100; - if (args.length > 3) { vectorKnownHeightPrints = Integer.parseInt(args[3]); } - int vectorFittedHeightPrints = 100; - if (args.length > 4) { vectorFittedHeightPrints = Integer.parseInt(args[4]); } + int vectorKnownHeightPrints = 100; + if (args.length > 3) { vectorKnownHeightPrints = Integer.parseInt(args[3]); } + int vectorFittedHeightPrints = 100; + if (args.length > 4) { vectorFittedHeightPrints = Integer.parseInt(args[4]); } - if (!testVectorKnownPrints(vectorKnownHeightPrints)) { - log.error("Failed vector prints with defined heights"); - } else if (!testVectorFittedPrints(vectorFittedHeightPrints)) { - log.error("Failed vector prints with fit to height sizing"); - } else { - log.info("All vector prints completed"); + if (!testVectorKnownPrints(vectorKnownHeightPrints)) { + log.error("Failed vector prints with defined heights"); + } else if (!testVectorFittedPrints(vectorFittedHeightPrints)) { + log.error("Failed vector prints with fit to height sizing"); + } else { + log.info("All vector prints completed"); + } + } + catch(Throwable t) { + log.error("Tests failed due to an exception", t); } - System.exit(0); //explicit exit since jfx is running in background } @@ -154,7 +158,7 @@ public static boolean testVectorKnownPrints(int trials) throws Throwable { job.endJob(); try { - log.info("Waiting {} seconds for the spooler to catch up.", SPOOLER_WAIT/1000); + log.info("Waiting {} seconds for the spooler to catch up.", SPOOLER_WAIT / 1000); Thread.sleep(SPOOLER_WAIT); } catch(InterruptedException ignore) {} @@ -176,7 +180,7 @@ public static boolean testVectorFittedPrints(int trials) throws Throwable { job.endJob(); try { - log.info("Waiting {} seconds for the spooler to catch up.", SPOOLER_WAIT/1000); + log.info("Waiting {} seconds for the spooler to catch up.", SPOOLER_WAIT / 1000); Thread.sleep(SPOOLER_WAIT); } catch(InterruptedException ignore) {} @@ -243,7 +247,7 @@ private static void cleanup() { for(File file : files) { if (file.getName().endsWith("." + RASTER_OUTPUT_FORMAT) && file.getName().startsWith(String.format("%s-", Constants.DATA_DIR))) { - if(!file.delete()) { + if (!file.delete()) { log.warn("Could not delete {}", file); } }