diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/FontResolver.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/FontResolver.java index ca54f6e08..79247accf 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/FontResolver.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/FontResolver.java @@ -26,7 +26,4 @@ public interface FontResolver { public FSFont resolveFont(SharedContext renderingContext, FontSpecification spec); - - @Deprecated - public void flushCache(); } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/BaseRendererBuilder.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/BaseRendererBuilder.java index 34be18f78..c5f515ae4 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/BaseRendererBuilder.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/BaseRendererBuilder.java @@ -546,6 +546,35 @@ public TFinalClass useFont(FSSupplier supplier, String fontFamily, return (TFinalClass) this; } + /** + *

Add a font programmatically. If the font is NOT subset, it will be downloaded + * when the renderer is run, otherwise, assuming a font-metrics cache has been configured, + * the font will only be downloaded if required. Therefore, the user could add many fonts, + * confident that only those that are needed will be downloaded and processed.

+ * + *

The InputStream returned by the supplier will be closed by the caller. Fonts + * should generally be subset (Java2D renderer ignores this argument), + * except when used in form controls. FSSupplier is a lambda compatible interface.

+ * + *

Fonts can also be added using a font-face at-rule in the CSS (not + * recommended for Java2D usage).

+ * + *

IMPORTANT: This method is not recommended for use with Java2D. + * To add fonts for use by Java2D, SVG, etc see: + * {@link #useFont(File, String, Integer, FontStyle, boolean, Set)}

+ * + *

For gotchas related to font handling please see: + * Wiki: Fonts

+ * + * @return this for method chaining + */ + public TFinalClass useFont( + FSSupplier supplier, String fontFamily, Integer fontWeight, + FontStyle fontStyle, boolean subset, Set useFontFlags) { + state._fonts.add(new AddedFont(supplier, null, fontWeight, fontFamily, subset, fontStyle, useFontFlags)); + return (TFinalClass) this; + } + /** * Simpler overload for * {@link #useFont(FSSupplier, String, Integer, FontStyle, boolean)} @@ -616,6 +645,17 @@ public enum FSFontUseCase { /** Main document (PDF or Java2D) */ DOCUMENT, SVG, - MATHML + MATHML, + /** + * Use as a fallback font after all supplied fonts have been tried but before + * the built-in fonts have been attempted. + */ + FALLBACK_PRE, + /** + * Use as a fallback fonts after all supplied fonts and the built-in fonts have been + * tried. The same font should not be registered with both FALLBACK_PRE + * and FALLBACK_FINAL. + */ + FALLBACK_FINAL; } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/FontFamily.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/FontFamily.java index b123e4196..bcd8c928e 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/FontFamily.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/outputdevice/helper/FontFamily.java @@ -8,9 +8,15 @@ import com.openhtmltopdf.css.constants.IdentValue; public class FontFamily { - private List _fontDescriptions; + private final List _fontDescriptions = new ArrayList<>(4); + private final String _family; - public FontFamily() { + public FontFamily(String family) { + this._family = family; + } + + public String getFamily() { + return _family; } public List getFontDescriptions() { @@ -18,42 +24,39 @@ public List getFontDescriptions() { } public void addFontDescription(T descr) { - if (_fontDescriptions == null) { - _fontDescriptions = new ArrayList<>(); - } _fontDescriptions.add(descr); - Collections.sort(_fontDescriptions, - new Comparator() { - public int compare(T o1, T o2) { - return o1.getWeight() - o2.getWeight(); - } - }); - } - - public void setName(String fontFamilyName) { + Collections.sort(_fontDescriptions, Comparator.comparing(T::getWeight)); } public T match(int desiredWeight, IdentValue style) { - if (_fontDescriptions == null) { - throw new RuntimeException("fontDescriptions is null"); + if (_fontDescriptions.isEmpty()) { + return null; + } else if (_fontDescriptions.size() == 1) { + return _fontDescriptions.get(0); } - List candidates = new ArrayList<>(); + List candidates = new ArrayList<>(_fontDescriptions.size()); - for (T description : _fontDescriptions) { - if (description.getStyle() == style) { - candidates.add(description); - } + // First try only matching style. + getStyleMatches(style, candidates); + + // Then try changing italic to oblique. + if (candidates.isEmpty() && style == IdentValue.ITALIC) { + getStyleMatches(IdentValue.OBLIQUE, candidates); } - if (candidates.size() == 0) { - if (style == IdentValue.ITALIC) { - return match(desiredWeight, IdentValue.OBLIQUE); - } else if (style == IdentValue.OBLIQUE) { - return match(desiredWeight, IdentValue.NORMAL); - } else { - candidates.addAll(_fontDescriptions); - } + // Then try changing oblique to normal. + if (candidates.isEmpty() && style == IdentValue.OBLIQUE) { + getStyleMatches(IdentValue.NORMAL, candidates); + } + + // Still nothing! + if (candidates.isEmpty()) { + candidates.addAll(_fontDescriptions); + } + + if (candidates.size() == 1) { + return candidates.get(0); } T result = findByWeight(candidates, desiredWeight, SM_EXACT); @@ -69,6 +72,14 @@ public T match(int desiredWeight, IdentValue style) { } } + private void getStyleMatches(IdentValue style, List candidates) { + for (T description : _fontDescriptions) { + if (description.getStyle() == style) { + candidates.add(description); + } + } + } + private static final int SM_EXACT = 1; private static final int SM_LIGHTER_OR_DARKER = 2; private static final int SM_DARKER_OR_LIGHTER = 3; diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/issue-641-font-fallback.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/issue-641-font-fallback.pdf new file mode 100644 index 000000000..feb26da21 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/text/issue-641-font-fallback.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/text/issue-641-font-fallback.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/issue-641-font-fallback.html new file mode 100644 index 000000000..701246bf4 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/text/issue-641-font-fallback.html @@ -0,0 +1,22 @@ + + + + + + +
+Text should look normal! +
+ + +
+Text should look bold! +
+ + + diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java index 210ae49c0..c1c37bb83 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/TextVisualRegressionTest.java @@ -8,7 +8,9 @@ import java.awt.Shape; import java.awt.geom.Rectangle2D; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.util.EnumSet; import java.util.Map; import static org.junit.Assert.assertTrue; @@ -22,6 +24,7 @@ import com.openhtmltopdf.extend.FSObjectDrawer; import com.openhtmltopdf.extend.FSObjectDrawerFactory; import com.openhtmltopdf.extend.OutputDevice; +import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FSFontUseCase; import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FontStyle; import com.openhtmltopdf.render.RenderingContext; import com.openhtmltopdf.visualtest.TestSupport; @@ -682,4 +685,38 @@ public void testIssue472AddSemiTransparentWatermark() throws IOException { builder.useObjectDrawerFactory(new WatermarkDrawerFactory()); })); } + + /** + * Tests the ability to use fallback fonts. + */ + @Test + public void testIssue641FontFallback() throws IOException { + assertTrue(vtester.runTest("issue-641-font-fallback", builder -> { + builder.useFont( + new File("target/test/visual-tests/SourceSansPro-Regular.ttf"), + "SourceSans", 400, FontStyle.NORMAL, true, + EnumSet.of(FSFontUseCase.DOCUMENT)); + builder.useFont( + new File("target/test/visual-tests/Karla-Bold.ttf"), + "Karla", 700, FontStyle.NORMAL, true, + EnumSet.of(FSFontUseCase.FALLBACK_PRE)); + })); + } + + /** + * Tests the ability to use fallback fonts via input streams. + */ + @Test + public void testIssue641FontFallbackInputStream() throws IOException { + assertTrue(vtester.runTest("issue-641-font-fallback", builder -> { + builder.useFont( + () -> TextVisualRegressionTest.class.getResourceAsStream("/visualtest/html/fonts/SourceSansPro-Regular.ttf"), + "SourceSans", 400, FontStyle.NORMAL, true, + EnumSet.of(FSFontUseCase.DOCUMENT)); + builder.useFont( + () -> TextVisualRegressionTest.class.getResourceAsStream("/visualtest/html/fonts/Karla-Bold.ttf"), + "Karla", 700, FontStyle.NORMAL, true, + EnumSet.of(FSFontUseCase.FALLBACK_PRE)); + })); + } } diff --git a/openhtmltopdf-java2d/src/main/java/com/openhtmltopdf/java2d/Java2DFontResolver.java b/openhtmltopdf-java2d/src/main/java/com/openhtmltopdf/java2d/Java2DFontResolver.java index 51617474b..12d058279 100644 --- a/openhtmltopdf-java2d/src/main/java/com/openhtmltopdf/java2d/Java2DFontResolver.java +++ b/openhtmltopdf-java2d/src/main/java/com/openhtmltopdf/java2d/Java2DFontResolver.java @@ -182,7 +182,6 @@ private void init() { } @Deprecated - @Override public void flushCache() { instanceHash.clear(); availableFontsHash.clear(); @@ -266,7 +265,7 @@ public void addFontFile(File fontFile, String fontFamilyNameOverride, Integer fo private FontFamily getFontFamily(String fontFamilyName) { FontFamily fontFamily = _fontFamilies.get(fontFamilyName); if (fontFamily == null) { - fontFamily = new FontFamily<>(); + fontFamily = new FontFamily<>(fontFamilyName); _fontFamilies.put(fontFamilyName, fontFamily); } return fontFamily; diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFontResolver.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFontResolver.java index 5a987e3a3..b067789c5 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFontResolver.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFontResolver.java @@ -30,23 +30,21 @@ import com.openhtmltopdf.extend.FSSupplier; import com.openhtmltopdf.extend.FontResolver; import com.openhtmltopdf.layout.SharedContext; -import com.openhtmltopdf.outputdevice.helper.FontFaceFontSupplier; -import com.openhtmltopdf.outputdevice.helper.FontFamily; -import com.openhtmltopdf.outputdevice.helper.FontResolverHelper; import com.openhtmltopdf.outputdevice.helper.MinimalFontDescription; import com.openhtmltopdf.pdfboxout.PdfRendererBuilder.PdfAConformance; +import com.openhtmltopdf.pdfboxout.fontstore.AbstractFontStore; +import com.openhtmltopdf.pdfboxout.fontstore.FallbackFontStore; +import com.openhtmltopdf.pdfboxout.fontstore.FontUtil; +import com.openhtmltopdf.pdfboxout.fontstore.MainFontStore; import com.openhtmltopdf.render.FSFont; import com.openhtmltopdf.util.LogMessageId; import com.openhtmltopdf.util.XRLog; import org.apache.fontbox.ttf.TrueTypeCollection; -import org.apache.fontbox.ttf.TrueTypeCollection.TrueTypeFontProcessor; -import org.apache.fontbox.ttf.TrueTypeFont; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; import org.apache.pdfbox.pdmodel.font.PDType0Font; -import org.apache.pdfbox.pdmodel.font.PDType1Font; import java.io.File; import java.io.FilenameFilter; @@ -56,77 +54,50 @@ import java.util.logging.Level; /** - * This class handles all font resolving for the PDF generation. Please note that at the moment only subsetting/embedding - * of fonts work. So you should always set embedded/subset=true for now. + * This class handles all font resolving for the PDF generation. */ public class PdfBoxFontResolver implements FontResolver { - private Map> _fontFamilies; - private Map _fontCache = new HashMap<>(); + public enum FontGroup { + MAIN, + PRE_BUILT_IN_FALLBACK, + FINAL_FALLBACK; + } + private final PDDocument _doc; - private final SharedContext _sharedContext; - private final List _collectionsToClose = new ArrayList<>(); - private final FSCacheEx _fontMetricsCache; - private final PdfAConformance _pdfAConformance; - private final boolean _pdfUaConform; + private final MainFontStore _suppliedFonts; + private final FallbackFontStore _preBuiltinFallbackFonts; + private final AbstractFontStore _builtinFonts; + private final FallbackFontStore _finalFallbackFonts; public PdfBoxFontResolver(SharedContext sharedContext, PDDocument doc, FSCacheEx pdfMetricsCache, PdfAConformance pdfAConformance, boolean pdfUaConform) { - _sharedContext = sharedContext; - _doc = doc; - _fontMetricsCache = pdfMetricsCache; - _pdfAConformance = pdfAConformance; - _pdfUaConform = pdfUaConform; - - // All fonts are required to be embedded in PDF/A documents, so we don't add the built-in fonts, if conformance is required. - _fontFamilies = (_pdfAConformance == PdfAConformance.NONE && !pdfUaConform) ? createInitialFontMap() : new HashMap<>(); - } + this._doc = doc; - @Override - public FSFont resolveFont(SharedContext renderingContext, FontSpecification spec) { - return resolveFont(renderingContext, spec.families, spec.size, spec.fontWeight, spec.fontStyle, spec.variant); - } + this._suppliedFonts = new MainFontStore(sharedContext, doc, pdfMetricsCache); - /** - * Free all font resources (i.e. open files), the document should already be - * closed. - */ - public void close() { - _fontCache.clear(); + this._preBuiltinFallbackFonts = new FallbackFontStore(sharedContext, doc, pdfMetricsCache); - // Close all still open TrueTypeCollections - for (TrueTypeCollection collection : _collectionsToClose) { - try { - collection.close(); - } catch (IOException e) { - //e.printStackTrace(); - } - } - _collectionsToClose.clear(); - } + // All fonts are required to be embedded in PDF/A documents, so we don't add + // the built-in fonts, if conformance is required. + this._builtinFonts = (pdfAConformance == PdfAConformance.NONE && !pdfUaConform) ? + new AbstractFontStore.BuiltinFontStore(doc) : + new AbstractFontStore.EmptyFontStore(); + + this._finalFallbackFonts = new FallbackFontStore(sharedContext, doc, pdfMetricsCache); + } - @Deprecated @Override - public void flushCache() { - _fontFamilies = createInitialFontMap(); - close(); - _fontCache = new HashMap<>(); + public FSFont resolveFont(SharedContext renderingContext, FontSpecification spec) { + return resolveFont(renderingContext, spec.families, spec.size, spec.fontWeight, spec.fontStyle, spec.variant); } - @Deprecated - public void flushFontFaceFonts() { - _fontCache = new HashMap<>(); - - for (Iterator> i = _fontFamilies.values().iterator(); i.hasNext(); ) { - FontFamily family = i.next(); - for (Iterator j = family.getFontDescriptions().iterator(); j.hasNext(); ) { - FontDescription d = j.next(); - if (d.isFromFontFace()) { - j.remove(); - } - } - if (family.getFontDescriptions().size() == 0) { - i.remove(); - } - } + /** + * Free all font resources (i.e. open files), the document should already be + * closed. + */ + public void close() { + FontUtil.tryClose(this._suppliedFonts); + FontUtil.tryClose(this._preBuiltinFallbackFonts); + FontUtil.tryClose(this._finalFallbackFonts); } public void importFontFaces(List fontFaces) { @@ -139,8 +110,6 @@ public void importFontFaces(List fontFaces) { } boolean noSubset = style.isIdent(CSSName.FS_FONT_SUBSET, IdentValue.COMPLETE_FONT); -// boolean embedded = style.isIdent(CSSName.FS_PDF_FONT_EMBED, IdentValue.EMBED); -// String encoding = style.getStringProperty(CSSName.FS_PDF_FONT_ENCODING); String fontFamily = null; IdentValue fontWeight = null; @@ -161,14 +130,22 @@ public void importFontFaces(List fontFaces) { fontStyle = style.getIdent(CSSName.FONT_STYLE); } - addFontFaceFont(fontFamily, fontWeight, fontStyle, src.asString(), !noSubset); + this._suppliedFonts.addFontFaceFont(fontFamily, fontWeight, fontStyle, src.asString(), !noSubset); } } /** - * Add all fonts in the given directory + * @deprecated Use {@link #addFontDirectory(String, boolean, FontGroup)} */ + @Deprecated public void addFontDirectory(String dir, boolean embedded) throws IOException { + addFontDirectory(dir, embedded, FontGroup.MAIN); + } + + /** + * Add all fonts in the given directory + */ + public void addFontDirectory(String dir, boolean embedded, FontGroup fontGroup) throws IOException { File f = new File(dir); if (f.isDirectory()) { File[] files = f.listFiles(new FilenameFilter() { @@ -180,88 +157,116 @@ public boolean accept(File dir, String name) { assert files != null; for (File file : files) { - addFont(file, file.getName(), 400, IdentValue.NORMAL, embedded); + addFont(file, file.getName(), 400, IdentValue.NORMAL, embedded, fontGroup); } } } /** - * Add a font using a FontBox TrueTypeFont. + * Adds a font collection (.ttc in an input stream) to a + * specific font group. */ - private void addFont(TrueTypeFont trueTypeFont, String fontFamilyNameOverride, - Integer fontWeightOverride, IdentValue fontStyleOverride, boolean subset) throws IOException { - - - PDFont font = PDType0Font.load(_doc, trueTypeFont, subset); - - addFontLazy(new PDFontSupplier(font), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); - } - - /** - * Add a font with a lazy loaded PDFont - */ - private void addFontLazy(FSSupplier font, String fontFamilyNameOverride, Integer fontWeightOverride, IdentValue fontStyleOverride, boolean subset) { - FontFamily fontFamily = getFontFamily(fontFamilyNameOverride); - FontDescription descr = new FontDescription( - _doc, - font, - normalizeFontStyle(fontStyleOverride), - normalizeFontWeight(fontWeightOverride), - fontFamilyNameOverride, - false, // isFromFontFace - subset, - _fontMetricsCache); - - if (!subset) { - if (descr.realizeFont()) { - fontFamily.addFontDescription(descr); + public void addFontCollection( + FSSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset, + FontGroup fontGroup) throws IOException { + + try (InputStream inputStream = supplier.supply()) { + TrueTypeCollection collection = new TrueTypeCollection(inputStream); + + if (fontGroup == FontGroup.MAIN) { + this._suppliedFonts.addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } else { + getFallbackFontStore(fontGroup).addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } - } else { - fontFamily.addFontDescription(descr); } } - /** - * Add fonts using a FontBox TrueTypeCollection. - */ - private void addFontCollection(TrueTypeCollection collection, final String fontFamilyNameOverride, - final Integer fontWeightOverride, final IdentValue fontStyleOverride, final boolean subset) - throws IOException { - collection.processAllFonts(new TrueTypeFontProcessor() { - @Override - public void process(TrueTypeFont ttf) throws IOException { - addFont(ttf, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); - } - }); - _collectionsToClose.add(collection); - } - /** * Add fonts using a .ttc TrueTypeCollection + * @deprecated Use {@link #addFontCollection(FSSupplier, String, Integer, IdentValue, boolean, FontGroup)} */ + @Deprecated public void addFontCollection(FSSupplier supplier, final String fontFamilyNameOverride, final Integer fontWeightOverride, final IdentValue fontStyleOverride, final boolean subset) throws IOException { try (InputStream inputStream = supplier.supply()){ TrueTypeCollection collection = new TrueTypeCollection(inputStream); - addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + this._suppliedFonts.addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } } + /** + * Adds a font collection (.ttc file) to a specific group. + */ + public void addFontCollection( + File file, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset, + FontGroup fontGroup) throws IOException { + + TrueTypeCollection collection = new TrueTypeCollection(file); + + if (fontGroup == FontGroup.MAIN) { + this._suppliedFonts.addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } else { + getFallbackFontStore(fontGroup).addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + } + /** * Add fonts using a .ttc TrueTypeCollection + * @deprecated Use {@link #addFontCollection(File, String, Integer, IdentValue, boolean, FontGroup)} */ + @Deprecated public void addFontCollection(File file, final String fontFamilyNameOverride, final Integer fontWeightOverride, final IdentValue fontStyleOverride, final boolean subset) throws IOException { TrueTypeCollection collection = new TrueTypeCollection(file); - addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + this._suppliedFonts.addFontCollection(collection, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + + /** + * Add a font file (truetype) to a specific font group. + */ + public void addFont( + File fontFile, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset, + FontGroup fontGroup) throws IOException { + + if (fontFile.getName().toLowerCase(Locale.US).endsWith(".ttc")) { + // Specialcase for TrueTypeCollections + addFontCollection(fontFile, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset, fontGroup); + } else if (fontGroup == FontGroup.MAIN) { + this._suppliedFonts.addFontLazy(new FilePDFontSupplier(fontFile, _doc), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } else { + getFallbackFontStore(fontGroup).addFontLazy(new FilePDFontSupplier(fontFile, _doc), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + } + + private FallbackFontStore getFallbackFontStore(FontGroup fontGroup) { + assert fontGroup == FontGroup.PRE_BUILT_IN_FALLBACK || + fontGroup == FontGroup.FINAL_FALLBACK; + + return fontGroup == FontGroup.PRE_BUILT_IN_FALLBACK ? + this._preBuiltinFallbackFonts : + this._finalFallbackFonts; } /** * Add a font using a existing file. If the file is a TrueTypeCollection, it * will be handled as such. + * @deprecated Use {@link #addFont(File, String, Integer, IdentValue, boolean, FontGroup)} */ + @Deprecated public void addFont(File fontFile, final String fontFamilyNameOverride, final Integer fontWeightOverride, final IdentValue fontStyleOverride, final boolean subset) throws IOException { /* @@ -275,7 +280,7 @@ public void addFont(File fontFile, final String fontFamilyNameOverride, final In /* * We load the font using the file. */ - addFontLazy(new FilePDFontSupplier(fontFile, _doc), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + this._suppliedFonts.addFontLazy(new FilePDFontSupplier(fontFile, _doc), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } /** @@ -301,262 +306,140 @@ public PDFont supply() { } } - /** - * Add a font using a InputStream. The given file must be a TrueType Font - * (.ttf). If you know the underlying stream is a .ttc file you should use - * {@link #addFontCollection(FSSupplier, String, Integer, IdentValue, boolean)} - */ - public void addFont(FSSupplier supplier, String fontFamilyNameOverride, Integer fontWeightOverride, - IdentValue fontStyleOverride, boolean subset) { - FontFamily fontFamily = getFontFamily(fontFamilyNameOverride); - - FontDescription descr = new FontDescription( - _doc, - supplier, - normalizeFontWeight(fontWeightOverride), - normalizeFontStyle(fontStyleOverride), - fontFamilyNameOverride, - false, // isFromFontFace - subset, - _fontMetricsCache); - - if (!subset) { - if (descr.realizeFont()) { - fontFamily.addFontDescription(descr); - } - } else { - fontFamily.addFontDescription(descr); - } - } - - /** - * Add a font using a PDFontSupplier. Use this method if you need special rules for font-loading (like using a font-cache) - * and subclass the {@link PDFontSupplier}. - */ - public void addFont(PDFontSupplier supplier, String fontFamilyNameOverride, Integer fontWeightOverride, - IdentValue fontStyleOverride, boolean subset) { - // would have prefered to used FSSupplier but sadly that would give us an error - // because the type-ereasure clashes with addFont(FSSupplier ...) - FontFamily fontFamily = getFontFamily(fontFamilyNameOverride); - - FontDescription descr = new FontDescription( - _doc, - supplier, - normalizeFontStyle(fontStyleOverride), - normalizeFontWeight(fontWeightOverride), - fontFamilyNameOverride, - false, // isFromFontFace - subset, - _fontMetricsCache); - - if (!subset) { - if (descr.realizeFont()) { - fontFamily.addFontDescription(descr); - } - } else { - fontFamily.addFontDescription(descr); - } - } - - private int normalizeFontWeight(IdentValue fontWeight) { - return fontWeight != null ? FontResolverHelper.convertWeightToInt(fontWeight) : 400; - } - - private int normalizeFontWeight(Integer fontWeight) { - return fontWeight != null ? fontWeight : 400; - } - - private IdentValue normalizeFontStyle(IdentValue fontStyle) { - return fontStyle != null ? fontStyle : IdentValue.NORMAL; - } - - private void addFontFaceFont(String fontFamilyName, IdentValue fontWeight, IdentValue fontStyle, String uri, boolean subset) { - FSSupplier fontSupplier = new FontFaceFontSupplier(_sharedContext, uri); - FontFamily fontFamily = getFontFamily(fontFamilyName); - - FontDescription description = new FontDescription( - _doc, - fontSupplier, - normalizeFontWeight(fontWeight), - normalizeFontStyle(fontStyle), - fontFamilyName, - true, // isFromFontFace - subset, - _fontMetricsCache); - - if (!subset) { - if (description.realizeFont()) { - fontFamily.addFontDescription(description); - } + /** + * Adds a font specified by an input stream (truetype) to a specific font group. + */ + public void addFont( + FSSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset, + FontGroup fontGroup) { + + if (fontGroup == FontGroup.MAIN) { + this._suppliedFonts.addFont( + supplier, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } else { - fontFamily.addFontDescription(description); + getFallbackFontStore(fontGroup).addFont(supplier, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } } - private FontFamily getFontFamily(String fontFamilyName) { - FontFamily fontFamily = _fontFamilies.get(fontFamilyName); - if (fontFamily == null) { - fontFamily = new FontFamily<>(); - _fontFamilies.put(fontFamilyName, fontFamily); - } - return fontFamily; + /** + * Add a font using a InputStream. The given file must be a TrueType Font + * (.ttf). If you know the underlying stream is a .ttc file you should use + * {@link #addFontCollection(FSSupplier, String, Integer, IdentValue, boolean)} + * + * @deprecated Use {@link #addFont(FSSupplier, String, Integer, IdentValue, boolean, FontGroup)} + */ + @Deprecated + public void addFont( + FSSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) { + + this._suppliedFonts.addFont( + supplier, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } - private FSFont resolveFont(SharedContext ctx, String[] families, float size, IdentValue weight, IdentValue style, IdentValue variant) { - if (!(style == IdentValue.NORMAL || style == IdentValue.OBLIQUE || style == IdentValue.ITALIC)) { - style = IdentValue.NORMAL; - } - - List fonts = new ArrayList<>(3); - - if (families != null) { - for (int i = 0; i < families.length; i++) { - FontDescription font = resolveFont(ctx, families[i], size, weight, style, variant); - if (font != null) { - fonts.add(font); - } - } - } - - if (_pdfAConformance == PdfAConformance.NONE && - !_pdfUaConform) { - // We don't have a final fallback font for PDF/A documents as serif may not be available - // unless the user has explicitly embedded it. - - // For now, we end up with "Serif" built-in font. - // Q: Should this change? - // Q: Should we have a final automatically added font? - fonts.add(resolveFont(ctx, "Serif", size, weight, style, variant)); + /** + * Add a font specified by a PDFontSupplier to a specific font group. + */ + public void addFont( + PDFontSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset, + FontGroup fontGroup) { + + if (fontGroup == FontGroup.MAIN) { + this._suppliedFonts.addFont(supplier, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } else { + getFallbackFontStore(fontGroup).addFont(supplier, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } - - return new PdfBoxFSFont(fonts, size); } - private String normalizeFontFamily(String fontFamily) { - String result = fontFamily; - // strip off the "s if they are there - if (result.startsWith("\"")) { - result = result.substring(1); - } - if (result.endsWith("\"")) { - result = result.substring(0, result.length() - 1); - } - - // normalize the font name - if (result.equalsIgnoreCase("serif")) { - result = "Serif"; - } - else if (result.equalsIgnoreCase("sans-serif")) { - result = "SansSerif"; - } - else if (result.equalsIgnoreCase("monospace")) { - result = "Monospaced"; - } - - return result; + /** + * Add a font using a PDFontSupplier. Use this method if you need special rules for font-loading (like using a font-cache) + * and subclass the {@link PDFontSupplier}. + * + * @deprecated Use {@link #addFont(PDFontSupplier, String, Integer, IdentValue, boolean, FontGroup)} + */ + @Deprecated + public void addFont( + PDFontSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) { + + this._suppliedFonts.addFont(supplier, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); } - private FontDescription resolveFont(SharedContext ctx, String fontFamily, float size, IdentValue weight, IdentValue style, IdentValue variant) { - String normalizedFontFamily = normalizeFontFamily(fontFamily); - String cacheKey = getHashName(normalizedFontFamily, weight, style); - FontDescription result = _fontCache.get(cacheKey); - if (result != null) { - return result; - } - FontFamily family = _fontFamilies.get(normalizedFontFamily); - if (family != null) { - result = family.match(FontResolverHelper.convertWeightToInt(weight), style); - if (result != null) { - _fontCache.put(cacheKey, result); - return result; - } + private FSFont resolveFont(SharedContext ctx, String[] families, float size, IdentValue weight, IdentValue style, IdentValue variant) { + if (!(style == IdentValue.NORMAL || style == IdentValue.OBLIQUE || style == IdentValue.ITALIC)) { + style = IdentValue.NORMAL; } - return null; - } - - protected static String getHashName( - String name, IdentValue weight, IdentValue style) { - return name + "-" + weight + "-" + style; - } - - private static Map> createInitialFontMap() { - HashMap> result = new HashMap<>(); - addCourier(result); - addTimes(result); - addHelvetica(result); - addSymbol(result); - addZapfDingbats(result); - // Try and load the iTextAsian fonts -// if(PdfBoxFontResolver.class.getClassLoader().getResource("com/lowagie/text/pdf/fonts/cjkfonts.properties") != null) { -// addCJKFonts(result); -// } - - return result; - } + List fonts = new ArrayList<>(3); - private static void addCourier(HashMap> result) { - FontFamily courier = new FontFamily<>(); - courier.setName("Courier"); + // Supplied fonts + if (families != null) { + resolveFamilyFont(ctx, families, size, weight, style, variant, fonts, _suppliedFonts); + } - courier.addFontDescription(new FontDescription(PDType1Font.COURIER_BOLD_OBLIQUE, IdentValue.OBLIQUE, 700)); - courier.addFontDescription(new FontDescription(PDType1Font.COURIER_OBLIQUE, IdentValue.OBLIQUE, 400)); - courier.addFontDescription(new FontDescription(PDType1Font.COURIER_BOLD, IdentValue.NORMAL, 700)); - courier.addFontDescription(new FontDescription(PDType1Font.COURIER, IdentValue.NORMAL, 400)); + // Pre-builtin fallback fonts. + fonts.addAll(_preBuiltinFallbackFonts.resolveFonts(ctx, families, size, weight, style, variant)); - result.put("DialogInput", courier); - result.put("Monospaced", courier); - result.put("Courier", courier); - } + // Built-in fonts. + if (families != null) { + resolveFamilyFont(ctx, families, size, weight, style, variant, fonts, _builtinFonts); - private static void addTimes(HashMap> result) { - FontFamily times = new FontFamily<>(); - times.setName("Times"); + FontDescription serif = _builtinFonts.resolveFont(ctx, "Serif", size, weight, style, variant); + if (serif != null) { + fonts.add(serif); + } + } - times.addFontDescription(new FontDescription(PDType1Font.TIMES_BOLD_ITALIC, IdentValue.ITALIC, 700)); - times.addFontDescription(new FontDescription(PDType1Font.TIMES_ITALIC, IdentValue.ITALIC, 400)); - times.addFontDescription(new FontDescription(PDType1Font.TIMES_BOLD, IdentValue.NORMAL, 700)); - times.addFontDescription(new FontDescription(PDType1Font.TIMES_ROMAN, IdentValue.NORMAL, 400)); + // Post built-in fallback fonts. + fonts.addAll(_finalFallbackFonts.resolveFonts(ctx, families, size, weight, style, variant)); - result.put("Serif", times); - result.put("TimesRoman", times); + return new PdfBoxFSFont(fonts, size); } - private static void addHelvetica(HashMap> result) { - FontFamily helvetica = new FontFamily<>(); - helvetica.setName("Helvetica"); - - helvetica.addFontDescription(new FontDescription(PDType1Font.HELVETICA_BOLD_OBLIQUE, IdentValue.OBLIQUE, 700)); - helvetica.addFontDescription(new FontDescription(PDType1Font.HELVETICA_OBLIQUE, IdentValue.OBLIQUE, 400)); - helvetica.addFontDescription(new FontDescription(PDType1Font.HELVETICA_BOLD, IdentValue.NORMAL, 700)); - helvetica.addFontDescription(new FontDescription(PDType1Font.HELVETICA, IdentValue.NORMAL, 400)); - - result.put("Dialog", helvetica); - result.put("SansSerif", helvetica); - result.put("Helvetica", helvetica); + private void resolveFamilyFont( + SharedContext ctx, + String[] families, + float size, + IdentValue weight, + IdentValue style, + IdentValue variant, + List fonts, + AbstractFontStore store) { + + for (int i = 0; i < families.length; i++) { + FontDescription font = store.resolveFont(ctx, families[i], size, weight, style, variant); + + if (font != null) { + fonts.add(font); + } + } } - private static void addSymbol(Map> result) { - FontFamily fontFamily = new FontFamily<>(); - fontFamily.setName("Symbol"); + - fontFamily.addFontDescription(new FontDescription(PDType1Font.SYMBOL, IdentValue.NORMAL, 400)); - result.put("Symbol", fontFamily); - } - private static void addZapfDingbats(Map> result) { - FontFamily fontFamily = new FontFamily<>(); - fontFamily.setName("ZapfDingbats"); - - fontFamily.addFontDescription(new FontDescription(PDType1Font.ZAPF_DINGBATS, IdentValue.NORMAL, 400)); + - result.put("ZapfDingbats", fontFamily); - } + /* TODO: CJK Fonts // fontFamilyName, fontName, encoding @@ -627,22 +510,23 @@ public static class FontDescription implements MinimalFontDescription { private final boolean _isFromFontFace; private final boolean _isSubset; + private PdfBoxRawPDFontMetrics _metrics; private final FSCacheEx _metricsCache; /** * Create a font description from one of the PDF built-in fonts. */ - private FontDescription(PDFont font, IdentValue style, int weight) { + public FontDescription(PDFont font, IdentValue style, int weight) { this(null, font, style, weight); } - + /** * Create a font description from an input stream supplier. * The input stream will only be accessed if {@link #getFont()} or * {@link #getFontMetrics()} (and the font metrics were not available from cache) are called. */ - private FontDescription( + public FontDescription( PDDocument doc, FSSupplier supplier, int weight, IdentValue style, String family, boolean isFromFontFace, boolean isSubset, @@ -685,7 +569,7 @@ private FontDescription(PDDocument doc, PDFont font, IdentValue style, int weigh * Creates a font description from a PDFont supplier. The supplier will only be called upon * if {@link #getFont()} or {@link #getFontMetrics()} (and the font metrics were not available from cache) are called. */ - private FontDescription( + public FontDescription( PDDocument doc, FSSupplier fontSupplier, IdentValue style, int weight, String family, boolean isFromFontFace, boolean isSubset, @@ -702,6 +586,10 @@ private FontDescription( _metrics = getFontMetricsFromCache(family, weight, style); } + public String getFamily() { + return _family; + } + private String createFontMetricsCacheKey(String family, int weight, IdentValue style) { return "font-metrics:" + family + ":" + weight + ":" + style.toString(); } @@ -726,7 +614,7 @@ private boolean loadMetrics() { } } - private boolean realizeFont() { + public boolean realizeFont() { if (_font == null && _fontSupplier != null) { XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_LOADING_FONT_FROM_SUPPLIER, _family, "PDFont"); diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java index 9e91e2c36..28ebfdb77 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java @@ -316,8 +316,6 @@ private void setDocumentP(Document doc, String url, NamespaceHandler nsh) { for (FSDOMMutator domMutator : _domMutators) domMutator.mutateDocument(doc); - getFontResolver().flushFontFaceFonts(); - _sharedContext.setBaseURL(url); _sharedContext.setNamespaceHandler(nsh); _sharedContext.getCss().setDocumentContext(_sharedContext, _sharedContext.getNamespaceHandler(), doc, new NullUserInterface()); diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfRendererBuilder.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfRendererBuilder.java index 78a7b4d7e..5d8c2f8d8 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfRendererBuilder.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfRendererBuilder.java @@ -8,6 +8,8 @@ import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder; import com.openhtmltopdf.outputdevice.helper.PageDimensions; import com.openhtmltopdf.outputdevice.helper.UnicodeImplementation; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontGroup; +import com.openhtmltopdf.render.FSFont; import com.openhtmltopdf.util.LogMessageId; import com.openhtmltopdf.util.XRLog; @@ -93,7 +95,9 @@ public PdfBoxRenderer buildPdfRenderer(Closeable diagnosticConsumer) { } } - if (font.usedFor.contains(FSFontUseCase.DOCUMENT)) { + if (font.usedFor.contains(FSFontUseCase.DOCUMENT) || + font.usedFor.contains(FSFontUseCase.FALLBACK_PRE) || + font.usedFor.contains(FSFontUseCase.FALLBACK_FINAL)) { IdentValue fontStyle = null; if (font.style != null) { @@ -114,19 +118,27 @@ public PdfBoxRenderer buildPdfRenderer(Closeable diagnosticConsumer) { } } + FontGroup group; + if (font.usedFor.contains(FSFontUseCase.FALLBACK_PRE)) { + group = FontGroup.PRE_BUILT_IN_FALLBACK; + } else if (font.usedFor.contains(FSFontUseCase.FALLBACK_FINAL)) { + group = FontGroup.FINAL_FALLBACK; + } else { + group = FontGroup.MAIN; + } // use InputStream supplier if (font.supplier != null) { - resolver.addFont(font.supplier, font.family, font.weight, fontStyle, font.subset); + resolver.addFont(font.supplier, font.family, font.weight, fontStyle, font.subset, group); } // use PDFont supplier else if (font.pdfontSupplier != null) { - resolver.addFont((PDFontSupplier) font.pdfontSupplier, font.family, font.weight, fontStyle, font.subset); + resolver.addFont((PDFontSupplier) font.pdfontSupplier, font.family, font.weight, fontStyle, font.subset, group); } // load via font File else { try { - resolver.addFont(font.fontFile, font.family, font.weight, fontStyle, font.subset); + resolver.addFont(font.fontFile, font.family, font.weight, fontStyle, font.subset, group); } catch (Exception e) { XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.INIT_FONT_COULD_NOT_BE_LOADED, font.fontFile.getPath(), e); } diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/AbstractFontStore.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/AbstractFontStore.java new file mode 100644 index 000000000..9901851b5 --- /dev/null +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/AbstractFontStore.java @@ -0,0 +1,125 @@ +package com.openhtmltopdf.pdfboxout.fontstore; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.font.PDType1Font; + +import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.layout.SharedContext; +import com.openhtmltopdf.outputdevice.helper.FontFamily; +import com.openhtmltopdf.outputdevice.helper.FontResolverHelper; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontDescription; + +public abstract class AbstractFontStore { + public abstract FontDescription resolveFont( + SharedContext ctx, + String fontFamily, + float size, + IdentValue weight, + IdentValue style, + IdentValue variant); + + public static class EmptyFontStore extends AbstractFontStore { + @Override + public FontDescription resolveFont( + SharedContext ctx, String fontFamily, float size, IdentValue weight, + IdentValue style, IdentValue variant) { + return null; + } + } + + public static class BuiltinFontStore extends AbstractFontStore { + final Map> _fontFamilies; + + public BuiltinFontStore(PDDocument doc) { + this._fontFamilies = createInitialFontMap(); + } + + static Map> createInitialFontMap() { + HashMap> result = new HashMap<>(); + addCourier(result); + addTimes(result); + addHelvetica(result); + addSymbol(result); + addZapfDingbats(result); + + return result; + } + + static void addCourier(HashMap> result) { + FontFamily courier = new FontFamily<>("Courier"); + + courier.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.COURIER_BOLD_OBLIQUE, IdentValue.OBLIQUE, 700)); + courier.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.COURIER_OBLIQUE, IdentValue.OBLIQUE, 400)); + courier.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.COURIER_BOLD, IdentValue.NORMAL, 700)); + courier.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.COURIER, IdentValue.NORMAL, 400)); + + result.put("DialogInput", courier); + result.put("Monospaced", courier); + result.put("Courier", courier); + } + + static void addTimes(HashMap> result) { + FontFamily times = new FontFamily<>("Times"); + + times.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.TIMES_BOLD_ITALIC, IdentValue.ITALIC, 700)); + times.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.TIMES_ITALIC, IdentValue.ITALIC, 400)); + times.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.TIMES_BOLD, IdentValue.NORMAL, 700)); + times.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.TIMES_ROMAN, IdentValue.NORMAL, 400)); + + result.put("Serif", times); + result.put("TimesRoman", times); + } + + static void addHelvetica(HashMap> result) { + FontFamily helvetica = new FontFamily<>("Helvetica"); + + helvetica.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.HELVETICA_BOLD_OBLIQUE, IdentValue.OBLIQUE, 700)); + helvetica.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.HELVETICA_OBLIQUE, IdentValue.OBLIQUE, 400)); + helvetica.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.HELVETICA_BOLD, IdentValue.NORMAL, 700)); + helvetica.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.HELVETICA, IdentValue.NORMAL, 400)); + + result.put("Dialog", helvetica); + result.put("SansSerif", helvetica); + result.put("Helvetica", helvetica); + } + + static void addSymbol(Map> result) { + FontFamily fontFamily = new FontFamily<>("Symbol"); + + fontFamily.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.SYMBOL, IdentValue.NORMAL, 400)); + + result.put("Symbol", fontFamily); + } + + static void addZapfDingbats(Map> result) { + FontFamily fontFamily = new FontFamily<>("ZapfDingbats"); + + fontFamily.addFontDescription(new PdfBoxFontResolver.FontDescription(PDType1Font.ZAPF_DINGBATS, IdentValue.NORMAL, 400)); + + result.put("ZapfDingbats", fontFamily); + } + + @Override + public PdfBoxFontResolver.FontDescription resolveFont( + SharedContext ctx, + String fontFamily, + float size, + IdentValue weight, + IdentValue style, + IdentValue variant) { + + String normalizedFontFamily = FontUtil.normalizeFontFamily(fontFamily); + FontFamily family = _fontFamilies.get(normalizedFontFamily); + + if (family != null) { + return family.match(FontResolverHelper.convertWeightToInt(weight), style); + } + + return null; + } + } +} diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/FallbackFontStore.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/FallbackFontStore.java new file mode 100644 index 000000000..fa358098f --- /dev/null +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/FallbackFontStore.java @@ -0,0 +1,198 @@ +package com.openhtmltopdf.pdfboxout.fontstore; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.fontbox.ttf.TrueTypeCollection; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.TrueTypeCollection.TrueTypeFontProcessor; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; + +import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.extend.FSCacheEx; +import com.openhtmltopdf.extend.FSCacheValue; +import com.openhtmltopdf.extend.FSSupplier; +import com.openhtmltopdf.layout.SharedContext; +import com.openhtmltopdf.outputdevice.helper.FontResolverHelper; +import com.openhtmltopdf.pdfboxout.PDFontSupplier; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontDescription; + +public class FallbackFontStore implements Closeable { + private final List fonts = new ArrayList<>(); + private final List _collectionsToClose = new ArrayList<>(); + private final PDDocument _doc; + private final FSCacheEx _fontMetricsCache; + + public FallbackFontStore( + SharedContext sharedContext, + PDDocument doc, + FSCacheEx pdfMetricsCache) { + this._doc = doc; + this._fontMetricsCache = pdfMetricsCache; + } + + private int getFontPriority(FontDescription font, String[] families, IdentValue weight, IdentValue desiredStyle, IdentValue variant) { + String fontFamily = font.getFamily(); + int fontWeight = font.getWeight(); + IdentValue fontStyle = font.getStyle(); + + List desiredFamilies = families != null ? + Arrays.asList(families) : Collections.emptyList(); + int desiredWeight = FontResolverHelper.convertWeightToInt(weight); + + if (fontWeight == desiredWeight && + fontStyle == desiredStyle) { + // Exact match for weight and style. + return getFamilyPriority(fontFamily, desiredFamilies); + } else if (Math.abs(fontWeight - desiredWeight) < 200 && + fontStyle == desiredStyle) { + // Near enough weight match, exact style match. + return 3 + getFamilyPriority(fontFamily, desiredFamilies); + } else if (fontStyle == desiredStyle) { + // No weight match, but style matches. + return 6 + getFamilyPriority(fontFamily, desiredFamilies); + } else { + // Neither weight nor style matches. + return 9 + getFamilyPriority(fontFamily, desiredFamilies); + } + } + + private int getFamilyPriority(String fontFamily, List desiredFamilies) { + if (!desiredFamilies.isEmpty() && + desiredFamilies.get(0).equals(fontFamily)) { + return 1; + } else if (desiredFamilies.contains(fontFamily)) { + return 2; + } else { + return 3; + } + } + + public List resolveFonts( + SharedContext ctx, String[] families, float size, IdentValue weight, IdentValue style, IdentValue variant) { + + if (fonts.size() <= 1) { + // No need to make a copy to sort. + return fonts; + } + + List ret = new ArrayList<>(fonts); + + Collections.sort(ret, Comparator.comparing(font -> getFontPriority(font, families, weight, style, variant))); + + return ret; + } + + public void addFont( + FSSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) { + + FontDescription descr = new FontDescription( + _doc, + supplier, + FontUtil.normalizeFontWeight(fontWeightOverride), + FontUtil.normalizeFontStyle(fontStyleOverride), + fontFamilyNameOverride, + false, // isFromFontFace + subset, + _fontMetricsCache); + + addFont(subset, descr); + } + + public void addFont( + PDFontSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) { + + FontDescription descr = new FontDescription( + _doc, + supplier, + FontUtil.normalizeFontStyle(fontStyleOverride), + FontUtil.normalizeFontWeight(fontWeightOverride), + fontFamilyNameOverride, + false, // isFromFontFace + subset, + _fontMetricsCache); + + addFont(subset, descr); + } + + /** + * Add a font with a lazy loaded PDFont + */ + public void addFontLazy(FSSupplier font, String fontFamilyNameOverride, Integer fontWeightOverride, IdentValue fontStyleOverride, boolean subset) { + FontDescription descr = new FontDescription( + _doc, + font, + FontUtil.normalizeFontStyle(fontStyleOverride), + FontUtil.normalizeFontWeight(fontWeightOverride), + fontFamilyNameOverride, + false, // isFromFontFace + subset, + _fontMetricsCache); + + addFont(subset, descr); + } + + private void addFont(boolean subset, FontDescription descr) { + if (!subset) { + if (descr.realizeFont()) { + fonts.add(descr); + } + } else { + fonts.add(descr); + } + } + + public void close() throws IOException { + for (TrueTypeCollection collection : _collectionsToClose) { + FontUtil.tryClose(collection); + } + _collectionsToClose.clear(); + } + + /** + * Add a font using a FontBox TrueTypeFont. + */ + void addFont( + TrueTypeFont trueTypeFont, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) throws IOException { + + PDFont font = PDType0Font.load(_doc, trueTypeFont, subset); + + addFontLazy(new PDFontSupplier(font), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + + public void addFontCollection( + TrueTypeCollection collection, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) throws IOException { + + collection.processAllFonts(new TrueTypeFontProcessor() { + @Override + public void process(TrueTypeFont ttf) throws IOException { + addFont(ttf, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + }); + _collectionsToClose.add(collection); + } +} diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/FontUtil.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/FontUtil.java new file mode 100644 index 000000000..1003484da --- /dev/null +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/FontUtil.java @@ -0,0 +1,52 @@ +package com.openhtmltopdf.pdfboxout.fontstore; + +import java.io.Closeable; +import java.io.IOException; + +import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.outputdevice.helper.FontResolverHelper; + +public class FontUtil { + public static String normalizeFontFamily(String fontFamily) { + String result = fontFamily; + // strip off the "s if they are there + if (result.startsWith("\"")) { + result = result.substring(1); + } + if (result.endsWith("\"")) { + result = result.substring(0, result.length() - 1); + } + + // normalize the font name + if (result.equalsIgnoreCase("serif")) { + result = "Serif"; + } + else if (result.equalsIgnoreCase("sans-serif")) { + result = "SansSerif"; + } + else if (result.equalsIgnoreCase("monospace")) { + result = "Monospaced"; + } + + return result; + } + + public static int normalizeFontWeight(Integer fontWeight) { + return fontWeight != null ? fontWeight : 400; + } + + public static IdentValue normalizeFontStyle(IdentValue fontStyle) { + return fontStyle != null ? fontStyle : IdentValue.NORMAL; + } + + public static int normalizeFontWeight(IdentValue fontWeight) { + return fontWeight != null ? FontResolverHelper.convertWeightToInt(fontWeight) : 400; + } + + public static void tryClose(Closeable obj) { + try { + obj.close(); + } catch (IOException e) { + } + } +} diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/MainFontStore.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/MainFontStore.java new file mode 100644 index 000000000..6f07c6c3c --- /dev/null +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/fontstore/MainFontStore.java @@ -0,0 +1,189 @@ +package com.openhtmltopdf.pdfboxout.fontstore; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fontbox.ttf.TrueTypeCollection; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.TrueTypeCollection.TrueTypeFontProcessor; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; + +import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.extend.FSCacheEx; +import com.openhtmltopdf.extend.FSCacheValue; +import com.openhtmltopdf.extend.FSSupplier; +import com.openhtmltopdf.layout.SharedContext; +import com.openhtmltopdf.outputdevice.helper.FontFaceFontSupplier; +import com.openhtmltopdf.outputdevice.helper.FontFamily; +import com.openhtmltopdf.outputdevice.helper.FontResolverHelper; +import com.openhtmltopdf.pdfboxout.PDFontSupplier; +import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontDescription; + +public class MainFontStore extends AbstractFontStore implements Closeable { + private final Map> _fontFamilies = new HashMap<>(); + private final FSCacheEx _fontMetricsCache; + private final PDDocument _doc; + private final SharedContext _sharedContext; + private final List _collectionsToClose = new ArrayList<>(); + + public MainFontStore( + SharedContext sharedContext, + PDDocument doc, + FSCacheEx pdfMetricsCache) { + + this._sharedContext = sharedContext; + this._doc = doc; + this._fontMetricsCache = pdfMetricsCache; + } + + public void close() throws IOException { + // Close all still open TrueTypeCollections + for (TrueTypeCollection collection : _collectionsToClose) { + FontUtil.tryClose(collection); + } + _collectionsToClose.clear(); + } + + /** + * Add a font using a FontBox TrueTypeFont. + */ + void addFont(TrueTypeFont trueTypeFont, String fontFamilyNameOverride, + Integer fontWeightOverride, IdentValue fontStyleOverride, boolean subset) throws IOException { + + PDFont font = PDType0Font.load(_doc, trueTypeFont, subset); + + addFontLazy(new PDFontSupplier(font), fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + + /** + * Add a font with a lazy loaded PDFont + */ + public void addFontLazy(FSSupplier font, String fontFamilyNameOverride, Integer fontWeightOverride, IdentValue fontStyleOverride, boolean subset) { + FontFamily fontFamily = getFontFamily(fontFamilyNameOverride); + FontDescription descr = new FontDescription( + _doc, + font, + FontUtil.normalizeFontStyle(fontStyleOverride), + FontUtil.normalizeFontWeight(fontWeightOverride), + fontFamilyNameOverride, + false, // isFromFontFace + subset, + _fontMetricsCache); + + addFontToFamily(subset, fontFamily, descr); + } + + /** + * Add fonts using a FontBox TrueTypeCollection. + */ + public void addFontCollection(TrueTypeCollection collection, final String fontFamilyNameOverride, + final Integer fontWeightOverride, final IdentValue fontStyleOverride, final boolean subset) + throws IOException { + collection.processAllFonts(new TrueTypeFontProcessor() { + @Override + public void process(TrueTypeFont ttf) throws IOException { + addFont(ttf, fontFamilyNameOverride, fontWeightOverride, fontStyleOverride, subset); + } + }); + _collectionsToClose.add(collection); + } + + public void addFontFaceFont(String fontFamilyName, IdentValue fontWeight, IdentValue fontStyle, String uri, boolean subset) { + FSSupplier fontSupplier = new FontFaceFontSupplier(_sharedContext, uri); + FontFamily fontFamily = getFontFamily(fontFamilyName); + + FontDescription description = new FontDescription( + _doc, + fontSupplier, + FontUtil.normalizeFontWeight(fontWeight), + FontUtil.normalizeFontStyle(fontStyle), + fontFamilyName, + true, // isFromFontFace + subset, + _fontMetricsCache); + + addFontToFamily(subset, fontFamily, description); + } + + public void addFont( + FSSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) { + + FontFamily fontFamily = getFontFamily(fontFamilyNameOverride); + + FontDescription descr = new FontDescription( + _doc, + supplier, + FontUtil.normalizeFontWeight(fontWeightOverride), + FontUtil.normalizeFontStyle(fontStyleOverride), + fontFamilyNameOverride, + false, // isFromFontFace + subset, + _fontMetricsCache); + + addFontToFamily(subset, fontFamily, descr); + } + + public void addFont( + PDFontSupplier supplier, + String fontFamilyNameOverride, + Integer fontWeightOverride, + IdentValue fontStyleOverride, + boolean subset) { + + // would have prefered to used FSSupplier but sadly that would give us an error + // because the type-ereasure clashes with addFont(FSSupplier ...) + FontFamily fontFamily = getFontFamily(fontFamilyNameOverride); + + FontDescription descr = new FontDescription( + _doc, + supplier, + FontUtil.normalizeFontStyle(fontStyleOverride), + FontUtil.normalizeFontWeight(fontWeightOverride), + fontFamilyNameOverride, + false, // isFromFontFace + subset, + _fontMetricsCache); + + addFontToFamily(subset, fontFamily, descr); + } + + @Override + public FontDescription resolveFont( + SharedContext ctx, String fontFamily, float size, IdentValue weight, IdentValue style, IdentValue variant) { + + String normalizedFontFamily = FontUtil.normalizeFontFamily(fontFamily); + FontFamily family = _fontFamilies.get(normalizedFontFamily); + + if (family != null) { + return family.match(FontResolverHelper.convertWeightToInt(weight), style); + } + + return null; + } + + private void addFontToFamily(boolean subset, FontFamily fontFamily, FontDescription descr) { + if (!subset) { + if (descr.realizeFont()) { + fontFamily.addFontDescription(descr); + } + } else { + fontFamily.addFontDescription(descr); + } + } + + private FontFamily getFontFamily(String fontFamilyName) { + return _fontFamilies.computeIfAbsent(fontFamilyName, name -> new FontFamily<>(fontFamilyName)); + } + +}