diff --git a/js/qz-tray.js b/js/qz-tray.js
index 8f71f6104..c578684db 100644
--- a/js/qz-tray.js
+++ b/js/qz-tray.js
@@ -1529,6 +1529,8 @@ var qz = (function() {
* @param {string} [data.options.pageRanges] Optional with [pdf] formats. Comma-separated list of page ranges to include.
* @param {boolean} [data.options.ignoreTransparency=false] Optional with [pdf] formats. Instructs transparent PDF elements to be ignored.
* Transparent PDF elements are known to degrade performance and quality when printing.
+ * @param {boolean} [data.options.altFontRendering=false] Optional with [pdf] formats. Instructs PDF to be rendered using PDFBOX 1.8 techniques.
+ * Drastically improves low-DPI PDF print quality on Windows.
* @param {...*} [arguments] Additionally three more parameters can be specified:
* {boolean} [resumeOnError=false] Whether the chain should continue printing if it hits an error on one the the prints.
* {string|Array} [signature] Pre-signed signature(s) of the JSON string for containing call, params, and timestamp.
diff --git a/sample.html b/sample.html
index b8511cdf2..8b9ccedf3 100755
--- a/sample.html
+++ b/sample.html
@@ -613,9 +613,13 @@
Pixel Printing
-
+
+
+
+
+
@@ -3231,7 +3235,8 @@
Options
pageWidth: $("#pPxlWidth").val(),
pageHeight: $("#pPxlHeight").val(),
pageRanges: $("#pPxlRange").val(),
- ignoreTransparency: $("#pPxlTransparent").prop('checked')
+ ignoreTransparency: $("#pPxlTransparent").prop('checked'),
+ altFontRendering: $("#pPxlAltFontRendering").prop('checked')
};
} else {
return {
diff --git a/src/qz/printer/action/PrintPDF.java b/src/qz/printer/action/PrintPDF.java
index 14d293c1e..a4772c45d 100644
--- a/src/qz/printer/action/PrintPDF.java
+++ b/src/qz/printer/action/PrintPDF.java
@@ -36,7 +36,6 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -51,6 +50,7 @@ public class PrintPDF extends PrintPixel implements PrintProcessor {
private double docWidth = 0;
private double docHeight = 0;
private boolean ignoreTransparency = false;
+ private boolean altFontRendering = false;
public PrintPDF() {
@@ -83,6 +83,7 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce
}
ignoreTransparency = dataOpt.optBoolean("ignoreTransparency", false);
+ altFontRendering = dataOpt.optBoolean("altFontRendering", false);
if (!dataOpt.isNull("pageRanges")) {
String[] ranges = dataOpt.optString("pageRanges", "").split(",");
@@ -252,7 +253,7 @@ public void print(PrintOutput output, PrintOptions options) throws PrinterExcept
}
}
- PDFWrapper wrapper = new PDFWrapper(doc, scale, false, ignoreTransparency,
+ PDFWrapper wrapper = new PDFWrapper(doc, scale, false, ignoreTransparency, altFontRendering,
(float)(useDensity * pxlOpts.getUnits().as1Inch()),
false, pxlOpts.getOrientation(), hints);
@@ -320,5 +321,6 @@ public void cleanup() {
docWidth = 0;
docHeight = 0;
ignoreTransparency = false;
+ altFontRendering = false;
}
}
diff --git a/src/qz/printer/action/pdf/OpaquePDFRenderer.java b/src/qz/printer/action/pdf/OpaquePDFRenderer.java
deleted file mode 100644
index b6721c511..000000000
--- a/src/qz/printer/action/pdf/OpaquePDFRenderer.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package qz.printer.action.pdf;
-
-import org.apache.pdfbox.contentstream.operator.MissingOperandException;
-import org.apache.pdfbox.contentstream.operator.Operator;
-import org.apache.pdfbox.contentstream.operator.graphics.GraphicsOperatorProcessor;
-import org.apache.pdfbox.cos.COSBase;
-import org.apache.pdfbox.cos.COSName;
-import org.apache.pdfbox.pdmodel.MissingResourceException;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.graphics.PDXObject;
-import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
-import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
-import org.apache.pdfbox.rendering.PDFRenderer;
-import org.apache.pdfbox.rendering.PageDrawer;
-import org.apache.pdfbox.rendering.PageDrawerParameters;
-
-import java.io.IOException;
-import java.util.List;
-
-public class OpaquePDFRenderer extends PDFRenderer {
-
- public OpaquePDFRenderer(PDDocument document) {
- super(document);
- }
-
- @Override
- protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException {
- return new OpaquePageDrawer(parameters);
- }
-
- // override drawer to make use of customized draw object
- private class OpaquePageDrawer extends PageDrawer {
- public OpaquePageDrawer(PageDrawerParameters parameters) throws IOException {
- super(parameters);
-
- addOperator(new OpaqueDrawObject());
- }
- }
-
- // override draw object to remove any calls to show transparency
- private class OpaqueDrawObject extends GraphicsOperatorProcessor {
-
- public OpaqueDrawObject() { }
-
- public void process(Operator operator, List operands) throws IOException {
- if (operands.isEmpty()) {
- throw new MissingOperandException(operator, operands);
- } else {
- COSBase base0 = operands.get(0);
- if (base0 instanceof COSName) {
- COSName objectName = (COSName)base0;
- PDXObject xobject = context.getResources().getXObject(objectName);
-
- if (xobject == null) {
- throw new MissingResourceException("Missing XObject: " + objectName.getName());
- } else {
- if (xobject instanceof PDImageXObject) {
- PDImageXObject image = (PDImageXObject)xobject;
- context.drawImage(image);
- } else if (xobject instanceof PDFormXObject) {
- try {
- context.increaseLevel();
- if (context.getLevel() <= 25) {
- PDFormXObject form = (PDFormXObject)xobject;
- context.showForm(form);
- }
-
- //LOG.error("recursion is too deep, skipping form XObject");
- }
- finally {
- context.decreaseLevel();
- }
- }
-
- }
- }
- }
- }
-
- public String getName() {
- return "Do";
- }
-
- }
-}
-
diff --git a/src/qz/printer/action/pdf/PDFWrapper.java b/src/qz/printer/action/pdf/PDFWrapper.java
index 5f8a5cb4a..42297a145 100644
--- a/src/qz/printer/action/pdf/PDFWrapper.java
+++ b/src/qz/printer/action/pdf/PDFWrapper.java
@@ -26,20 +26,14 @@ public class PDFWrapper implements Printable {
private PDFPrintable printable;
- public PDFWrapper(PDDocument document, Scaling scaling, boolean showPageBorder, boolean ignoreTransparency, float dpi, boolean center, PrintOptions.Orientation orientation, RenderingHints hints) {
+ public PDFWrapper(PDDocument document, Scaling scaling, boolean showPageBorder, boolean ignoreTransparency, boolean useAlternateFontRendering, float dpi, boolean center, PrintOptions.Orientation orientation, RenderingHints hints) {
this.document = document;
this.scaling = scaling;
if (orientation != null) {
this.orientation = orientation.getAsOrientRequested();
}
- PDFRenderer renderer;
- if (ignoreTransparency) {
- renderer = new OpaquePDFRenderer(document);
- } else {
- renderer = new PDFRenderer(document);
- }
-
+ PDFRenderer renderer = new ParamPdfRenderer(document, useAlternateFontRendering, ignoreTransparency);
printable = new PDFPrintable(document, scaling, showPageBorder, dpi, center, renderer);
printable.setRenderingHints(hints);
}
diff --git a/src/qz/printer/action/pdf/ParamPdfRenderer.java b/src/qz/printer/action/pdf/ParamPdfRenderer.java
new file mode 100644
index 000000000..4236e2fa7
--- /dev/null
+++ b/src/qz/printer/action/pdf/ParamPdfRenderer.java
@@ -0,0 +1,47 @@
+package qz.printer.action.pdf;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.rendering.PDFRenderer;
+import org.apache.pdfbox.rendering.PageDrawer;
+import org.apache.pdfbox.rendering.PageDrawerParameters;
+import qz.printer.rendering.OpaqueDrawObject;
+import qz.printer.rendering.OpaqueGraphicStateParameters;
+import qz.printer.rendering.PdfFontPageDrawer;
+
+import java.io.IOException;
+
+public class ParamPdfRenderer extends PDFRenderer {
+
+ private boolean useAlternateFontRendering;
+ private boolean ignoreTransparency;
+
+ public ParamPdfRenderer(PDDocument document, boolean useAlternateFontRendering, boolean ignoreTransparency) {
+ super(document);
+
+ this.useAlternateFontRendering = useAlternateFontRendering;
+ this.ignoreTransparency = ignoreTransparency;
+ }
+
+ @Override
+ protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException {
+ if (useAlternateFontRendering) {
+ return new PdfFontPageDrawer(parameters, ignoreTransparency);
+ } else if(ignoreTransparency) {
+ return new OpaquePageDrawer(parameters);
+ }
+ // Fallback to default PageDrawer
+ return new PageDrawer(parameters);
+ }
+
+ // override drawer to make use of customized draw object
+ private static class OpaquePageDrawer extends PageDrawer {
+ public OpaquePageDrawer(PageDrawerParameters parameters) throws IOException {
+ super(parameters);
+
+ // Note: These must match PdfFontPageDrawer's ignoreTransparency condition
+ addOperator(new OpaqueDrawObject());
+ addOperator(new OpaqueGraphicStateParameters());
+ }
+ }
+}
+
diff --git a/src/qz/printer/rendering/FontManager.java b/src/qz/printer/rendering/FontManager.java
new file mode 100644
index 000000000..8dc182942
--- /dev/null
+++ b/src/qz/printer/rendering/FontManager.java
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package qz.printer.rendering;
+
+import java.awt.*;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Properties;
+
+/**
+ * FontManager class pulled from PDFBOX 1.8
+ * with the help of Alexander Scherbatiy
+ */
+
+public class FontManager {
+ // HashMap with all known fonts
+ private static HashMap envFonts = new HashMap<>();
+ private static Properties fontMapping = new Properties();
+
+ static {
+ loadFonts();
+ loadBasefontMapping();
+ loadFontMapping();
+ }
+
+ private FontManager() {}
+
+ /**
+ * Get the font for the given fontname.
+ *
+ * @param font The name of the font.
+ * @return The font we are looking for or a similar font or null if nothing is found.
+ */
+ public static java.awt.Font getAwtFont(String font) {
+ String fontname = normalizeFontname(font);
+ if (envFonts.containsKey(fontname)) {
+ return envFonts.get(fontname);
+ }
+
+ return null;
+ }
+
+ /**
+ * Load all available fonts from the environment.
+ */
+ private static void loadFonts() {
+ for(Font font : GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()) {
+ String family = normalizeFontname(font.getFamily());
+ String psname = normalizeFontname(font.getPSName());
+
+ if (isBoldItalic(font)) {
+ envFonts.put(family + "bolditalic", font);
+ } else if (isBold(font)) {
+ envFonts.put(family + "bold", font);
+ } else if (isItalic(font)) {
+ envFonts.put(family + "italic", font);
+ } else {
+ envFonts.put(family, font);
+ }
+
+ if (!family.equals(psname)) {
+ envFonts.put(normalizeFontname(font.getPSName()), font);
+ }
+ }
+ }
+
+ /**
+ * Normalize the fontname.
+ *
+ * @param fontname The name of the font.
+ * @return The normalized name of the font.
+ */
+ private static String normalizeFontname(String fontname) {
+ // Terminate all whitespaces, commas and hyphens
+ String normalizedFontname = fontname.toLowerCase().replaceAll(" ", "").replaceAll(",", "").replaceAll("-", "");
+ // Terminate trailing characters up to the "+".
+ // As far as I know, these characters are used in names of embedded fonts
+ // If the embedded font can't be read, we'll try to find it here
+ if (normalizedFontname.contains("+")) {
+ normalizedFontname = normalizedFontname.substring(normalizedFontname.indexOf("+") + 1);
+ }
+ // normalize all kinds of fonttypes. There are several possible version which have to be normalized
+ // e.g. Arial,Bold Arial-BoldMT Helevtica-oblique ...
+ boolean isBold = normalizedFontname.contains("bold");
+ boolean isItalic = normalizedFontname.contains("italic") || normalizedFontname.contains("oblique");
+ normalizedFontname = normalizedFontname.toLowerCase().replaceAll("bold", "")
+ .replaceAll("italic", "").replaceAll("oblique", "");
+ if (isBold) {
+ normalizedFontname += "bold";
+ }
+ if (isItalic) {
+ normalizedFontname += "italic";
+ }
+
+ return normalizedFontname;
+ }
+
+
+ /**
+ * Add a font-mapping.
+ *
+ * @param font The name of the font.
+ * @param mappedName The name of the mapped font.
+ */
+ private static boolean addFontMapping(String font, String mappedName) {
+ String fontname = normalizeFontname(font);
+ // is there already a font mapping ?
+ if (envFonts.containsKey(fontname)) {
+ return false;
+ }
+ String mappedFontname = normalizeFontname(mappedName);
+ // is the mapped font available ?
+ if (!envFonts.containsKey(mappedFontname)) {
+ return false;
+ }
+ envFonts.put(fontname, envFonts.get(mappedFontname));
+ return true;
+ }
+
+ /**
+ * Load the mapping for the well knwon font-substitutions.
+ */
+ private static void loadFontMapping() {
+ boolean addedMapping = true;
+ // There could be some recursive mappings in the fontmapping, so that we have to
+ // read the list until no more additional mapping is added to it
+ while(addedMapping) {
+ int counter = 0;
+ Enumeration