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 keys = fontMapping.keys(); + while(keys.hasMoreElements()) { + String key = (String)keys.nextElement(); + if (addFontMapping(key, (String)fontMapping.get(key))) { + counter++; + } + } + if (counter == 0) { + addedMapping = false; + } + } + } + + /** + * Mapping for the basefonts. + */ + private static void loadBasefontMapping() { + // use well known substitutions if the environments doesn't provide native fonts for the 14 standard fonts + // Times-Roman -> Serif + if (!addFontMapping("Times-Roman", "TimesNewRoman")) { + addFontMapping("Times-Roman", "Serif"); + } + if (!addFontMapping("Times-Bold", "TimesNewRoman,Bold")) { + addFontMapping("Times-Bold", "Serif.bold"); + } + if (!addFontMapping("Times-Italic", "TimesNewRoman,Italic")) { + addFontMapping("Times-Italic", "Serif.italic"); + } + if (!addFontMapping("Times-BoldItalic", "TimesNewRoman,Bold,Italic")) { + addFontMapping("Times-BoldItalic", "Serif.bolditalic"); + } + // Helvetica -> SansSerif + if (!addFontMapping("Helvetica", "Helvetica")) { + addFontMapping("Helvetica", "SansSerif"); + } + if (!addFontMapping("Helvetica-Bold", "Helvetica,Bold")) { + addFontMapping("Helvetica-Bold", "SansSerif.bold"); + } + if (!addFontMapping("Helvetica-Oblique", "Helvetica,Italic")) { + addFontMapping("Helvetica-Oblique", "SansSerif.italic"); + } + if (!addFontMapping("Helvetica-BoldOblique", "Helvetica,Bold,Italic")) { + addFontMapping("Helvetica-BoldOblique", "SansSerif.bolditalic"); + } + // Courier -> Monospaced + if (!addFontMapping("Courier", "Courier")) { + addFontMapping("Courier", "Monospaced"); + } + if (!addFontMapping("Courier-Bold", "Courier,Bold")) { + addFontMapping("Courier-Bold", "Monospaced.bold"); + } + if (!addFontMapping("Courier-Oblique", "Courier,Italic")) { + addFontMapping("Courier-Oblique", "Monospaced.italic"); + } + if (!addFontMapping("Courier-BoldOblique", "Courier,Bold,Italic")) { + addFontMapping("Courier-BoldOblique", "Monospaced.bolditalic"); + } + // some well known (??) substitutions found on fedora linux + addFontMapping("Symbol", "StandardSymbolsL"); + addFontMapping("ZapfDingbats", "Dingbats"); + } + + /** + * Try to determine if the font has both a BOLD and an ITALIC-type. + * + * @param font The font. + * @return font has BOLD and ITALIC-type or not + */ + private static boolean isBoldItalic(java.awt.Font font) { + return isBold(font) && isItalic(font); + } + + /** + * Try to determine if the font has a BOLD-type. + * + * @param font The font. + * @return font has BOLD-type or not + */ + private static boolean isBold(java.awt.Font font) { + String name = font.getName().toLowerCase(); + if (name.contains("bold")) { + return true; + } + + String psname = font.getPSName().toLowerCase(); + return psname.contains("bold"); + } + + /** + * Try to determine if the font has an ITALIC-type. + * + * @param font The font. + * @return font has ITALIC-type or not + */ + private static boolean isItalic(java.awt.Font font) { + String name = font.getName().toLowerCase(); + // oblique is the same as italic + if (name.contains("italic") || name.contains("oblique")) { + return true; + } + + String psname = font.getPSName().toLowerCase(); + return psname.contains("italic") || psname.contains("oblique"); + } + +} diff --git a/src/qz/printer/rendering/OpaqueDrawObject.java b/src/qz/printer/rendering/OpaqueDrawObject.java new file mode 100644 index 000000000..2b73b0fd5 --- /dev/null +++ b/src/qz/printer/rendering/OpaqueDrawObject.java @@ -0,0 +1,60 @@ +package qz.printer.rendering; + +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.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; + +import java.io.IOException; +import java.util.List; + +// override draw object to remove any calls to show transparency +public 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/rendering/OpaqueGraphicStateParameters.java b/src/qz/printer/rendering/OpaqueGraphicStateParameters.java new file mode 100644 index 000000000..f7532e799 --- /dev/null +++ b/src/qz/printer/rendering/OpaqueGraphicStateParameters.java @@ -0,0 +1,77 @@ +/* + * 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.io.IOException; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.contentstream.operator.OperatorProcessor; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.contentstream.operator.OperatorName; +import org.apache.pdfbox.contentstream.PDFStreamEngine; +import org.apache.pdfbox.contentstream.operator.MissingOperandException; + +/** + * gs: Set parameters from graphics state parameter dictionary. + * + * @author Ben Litchfield + */ +public class OpaqueGraphicStateParameters extends OperatorProcessor +{ + private static final Log LOG = LogFactory.getLog(OpaqueGraphicStateParameters.class); + + @Override + public void process(Operator operator, List arguments) throws IOException + { + if (arguments.isEmpty()) + { + throw new MissingOperandException(operator, arguments); + } + COSBase base0 = arguments.get(0); + if (!(base0 instanceof COSName)) + { + return; + } + + // set parameters from graphics state parameter dictionary + COSName graphicsName = (COSName) base0; + PDFStreamEngine context = getContext(); + PDExtendedGraphicsState gs = context.getResources().getExtGState(graphicsName); + if (gs == null) + { + LOG.error("name for 'gs' operator not found in resources: /" + graphicsName.getName()); + return; + } + + // PDFBOX-5605: Disable alpha for lines, etc + gs.setNonStrokingAlphaConstant(1f); + gs.setStrokingAlphaConstant(1f); + + gs.copyIntoGraphicsState( context.getGraphicsState() ); + } + + @Override + public String getName() + { + return OperatorName.SET_GRAPHICS_STATE_PARAMS; + } +} \ No newline at end of file diff --git a/src/qz/printer/rendering/PdfFontPageDrawer.java b/src/qz/printer/rendering/PdfFontPageDrawer.java new file mode 100644 index 000000000..7ed114c5b --- /dev/null +++ b/src/qz/printer/rendering/PdfFontPageDrawer.java @@ -0,0 +1,198 @@ +package qz.printer.rendering; + + +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.font.*; +import org.apache.pdfbox.rendering.PageDrawer; +import org.apache.pdfbox.rendering.PageDrawerParameters; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.util.Vector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * PageDrawer overrides derived from PDFBOX 1.8 + * with the help of Alexander Scherbatiy + */ + +public class PdfFontPageDrawer extends PageDrawer { + + private static final Logger log = LoggerFactory.getLogger(PdfFontPageDrawer.class); + + private String fallbackFont = "helvetica"; //todo - definable parameter? + private final Map fonts = new HashMap<>(); + + public PdfFontPageDrawer(PageDrawerParameters parameters, boolean ignoresTransparency) throws IOException { + super(parameters); + + if (ignoresTransparency) { + // Note: These must match ParamPdfRenderer's OpaquePageDrawer + addOperator(new OpaqueDrawObject()); + addOperator(new OpaqueGraphicStateParameters()); + } + } + + @Override + protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) throws IOException { + // fall-back to draw Glyph when awt font has not been found + + AffineTransform at = textRenderingMatrix.createAffineTransform(); + at.concatenate(font.getFontMatrix().createAffineTransform()); + + Graphics2D graphics = getGraphics(); + setClip(); + + AffineTransform prevTx = graphics.getTransform(); + stretchNonEmbeddedFont(at, font, code, displacement); + // Probably relates to DEFAULT_FONT_MATRIX transform from PDFont + at.scale(100, -100); + graphics.transform(at); + + graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); + graphics.setPaint(getNonStrokingPaint()); + + Font prevFont = graphics.getFont(); + Font awtFont = getAwtFont(font); + graphics.setFont(awtFont); + + graphics.drawString(font.toUnicode(code), 0, 0); + + graphics.setFont(prevFont); + graphics.setTransform(prevTx); + } + + private void stretchNonEmbeddedFont(AffineTransform at, PDFont font, int code, Vector displacement) throws IOException { + // Stretch non-embedded glyph if it does not match the height/width contained in the PDF. + // Vertical fonts have zero X displacement, so the following code scales to 0 if we don't skip it. + if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code)) { + float fontWidth = font.getWidthFromFont(code); + if (fontWidth > 0 && Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001) { + float pdfWidth = displacement.getX() * 1000; + at.scale(pdfWidth / fontWidth, 1); + } + } + } + + private Font cacheFont(PDFont font, Font awtFont) { + fonts.put(font, awtFont); + return awtFont; + } + + private Font getAwtFont(PDFont font) throws IOException { + Font awtFont = fonts.get(font); + + if (awtFont != null) { + return awtFont; + } + + if (font instanceof PDType0Font) { + return cacheFont(font, getPDType0AwtFont((PDType0Font)font)); + } + + if (font instanceof PDType1Font) { + return cacheFont(font, getPDType1AwtFont((PDType1Font)font)); + } + + String msg = String.format("Not yet implemented: %s", font.getClass().getName()); + throw new UnsupportedOperationException(msg); + } + + public Font getPDType0AwtFont(PDType0Font font) throws IOException { + Font awtFont = null; + PDCIDFont descendantFont = font.getDescendantFont(); + + if (descendantFont != null) { + + if (descendantFont instanceof PDCIDFontType2) { + awtFont = getPDCIDAwtFontType2((PDCIDFontType2)descendantFont); + } + if (awtFont != null) { + /* + * Fix Oracle JVM Crashes. + * Tested with Oracle JRE 6.0_45-b06 and 7.0_21-b11 + */ + awtFont.canDisplay(1); + } + } + + if (awtFont == null) { + awtFont = FontManager.getAwtFont(fallbackFont); + log.debug("Using font {} instead of {}", awtFont.getName(), descendantFont.getFontDescriptor().getFontName()); + } + + return awtFont.deriveFont(10f); + } + + private Font getPDType1AwtFont(PDType1Font font) throws IOException { + Font awtFont = null; + String baseFont = font.getBaseFont(); + PDFontDescriptor fd = font.getFontDescriptor(); + + if (fd != null) { + if (fd.getFontFile() != null) { + try { + // create a type1 font with the embedded data + awtFont = Font.createFont(Font.TYPE1_FONT, fd.getFontFile().createInputStream()); + } + catch(java.awt.FontFormatException e) { + log.debug("Can't read the embedded type1 font {}", fd.getFontName()); + } + } + if (awtFont == null) { + // check if the font is part of our environment + if (fd.getFontName() != null) { + awtFont = FontManager.getAwtFont(fd.getFontName()); + } + if (awtFont == null) { + log.debug("Can't find the specified font {}", fd.getFontName()); + } + } + } else { + // check if the font is part of our environment + awtFont = FontManager.getAwtFont(baseFont); + if (awtFont == null) { + log.debug("Can't find the specified basefont {}", baseFont); + } + } + + if (awtFont == null) { + // we can't find anything, so we have to use the standard font + awtFont = FontManager.getAwtFont(fallbackFont); + log.debug("Using font {} instead", awtFont.getName()); + } + + return awtFont.deriveFont(20f); + } + + public Font getPDCIDAwtFontType2(PDCIDFontType2 font) throws IOException { + Font awtFont = null; + PDFontDescriptor fd = font.getFontDescriptor(); + PDStream ff2Stream = fd.getFontFile2(); + + if (ff2Stream != null) { + try { + // create a font with the embedded data + awtFont = Font.createFont(Font.TRUETYPE_FONT, ff2Stream.createInputStream()); + } + catch(java.awt.FontFormatException f) { + log.debug("Can't read the embedded font {}", fd.getFontName()); + } + if (awtFont == null) { + if (fd.getFontName() != null) { + awtFont = FontManager.getAwtFont(fd.getFontName()); + } + if (awtFont != null) { + log.debug("Using font {} instead", awtFont.getName()); + } + } + } + + return awtFont; + } +}