Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#683] Font reuse and override introduced to support multiple runs against the same target PDF document #684

Open
wants to merge 3 commits into
base: open-dev-v1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.openhtmltopdf.bidi.BidiReorderer;
import com.openhtmltopdf.bidi.BidiSplitterFactory;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.extend.*;
import com.openhtmltopdf.layout.Layer;
import com.openhtmltopdf.swing.NaiveUserAgent;
Expand Down Expand Up @@ -634,9 +635,22 @@ public enum PageSizeUnits {
MM, INCHES
}

public enum FontStyle {
NORMAL, ITALIC, OBLIQUE
}
public enum FontStyle {
NORMAL, ITALIC, OBLIQUE;

public IdentValue toIdentValue() {
switch (this) {
case NORMAL:
return IdentValue.NORMAL;
case ITALIC:
return IdentValue.ITALIC;
case OBLIQUE:
return IdentValue.OBLIQUE;
default:
return null;
}
}
}

/**
* Use cases for fonts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public List<T> getFontDescriptions() {
}

public void addFontDescription(T descr) {
_fontDescriptions.add(descr);
/*
* NOTE: Font is prepended, so newer fonts can override predecessors.
*/
_fontDescriptions.add(0, descr);
Collections.sort(_fontDescriptions, Comparator.comparing(T::getWeight));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@
*/
package com.openhtmltopdf.pdfboxout;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;

import org.apache.fontbox.ttf.TrueTypeCollection;
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 com.openhtmltopdf.css.constants.CSSName;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.css.sheet.FontFaceRule;
Expand All @@ -40,19 +57,6 @@
import com.openhtmltopdf.util.LogMessageId;
import com.openhtmltopdf.util.XRLog;

import org.apache.fontbox.ttf.TrueTypeCollection;
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 java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.logging.Level;

/**
* This class handles all font resolving for the PDF generation.
*/
Expand All @@ -69,20 +73,20 @@ public enum FontGroup {
private final AbstractFontStore _builtinFonts;
private final FallbackFontStore _finalFallbackFonts;

public PdfBoxFontResolver(SharedContext sharedContext, PDDocument doc, FSCacheEx<String, FSCacheValue> pdfMetricsCache, PdfAConformance pdfAConformance, boolean pdfUaConform) {
public PdfBoxFontResolver(SharedContext sharedContext, PDDocument doc, FSCacheEx<String, FSCacheValue> pdfMetricsCache, FontCache fontCache, PdfAConformance pdfAConformance, boolean pdfUaConform) {
this._doc = doc;

this._suppliedFonts = new MainFontStore(sharedContext, doc, pdfMetricsCache);
this._suppliedFonts = new MainFontStore(sharedContext, doc, pdfMetricsCache, fontCache);

this._preBuiltinFallbackFonts = new FallbackFontStore(sharedContext, doc, pdfMetricsCache);
this._preBuiltinFallbackFonts = new FallbackFontStore(sharedContext, doc, pdfMetricsCache, fontCache);

// 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);
this._finalFallbackFonts = new FallbackFontStore(sharedContext, doc, pdfMetricsCache, fontCache);
}

@Override
Expand All @@ -100,6 +104,10 @@ public void close() {
FontUtil.tryClose(this._finalFallbackFonts);
}

public Map<String, PDFont> getFontCache() {
return _suppliedFonts.getFontCache();
}

public void importFontFaces(List<FontFaceRule> fontFaces) {
for (FontFaceRule rule : fontFaces) {
CalculatedStyle style = rule.getCalculatedStyle();
Expand Down Expand Up @@ -491,6 +499,50 @@ private static void addCJKFont(String fontFamilyName, String fontName, String en
}
*/

public static class FontCache extends HashMap<String, PDFont> {
private static final long serialVersionUID = 1L;

@Override
public boolean containsKey(Object family) {
return containsKey((String)family, null, null);
}

public boolean containsKey(String family, Integer weight, IdentValue style) {
return super.containsKey(key(family, weight, style));
}

@Override
public PDFont get(Object family) {
return get((String)family, null, null);
}

public PDFont get(String family, Integer weight, IdentValue style) {
return super.get(key(family, weight, style));
}

@Override
public PDFont put(String family, PDFont value) {
return put(family, null, null, value);
}

public PDFont put(String family, Integer weight, IdentValue style, PDFont value) {
return super.put(key(family, weight, style), value);
}

@Override
public PDFont remove(Object family) {
return remove((String)family, null, null);
}

public PDFont remove(String family, Integer weight, IdentValue style) {
return super.remove(key(family, weight, style));
}

protected String key(String family, Integer weight, IdentValue style) {
return FontUtil.getFontQName(family, weight, style);
}
}

/**
* A <code>FontDescription</code> can exist in multiple states. Firstly the font may
* or may not be realized. Fonts are automatically realized upon calling {@link #getFont()}
Expand All @@ -513,6 +565,7 @@ public static class FontDescription implements MinimalFontDescription {

private PdfBoxRawPDFontMetrics _metrics;
private final FSCacheEx<String, FSCacheValue> _metricsCache;
private FontCache _fontCache;

/**
* Create a font description from one of the PDF built-in fonts.
Expand All @@ -530,7 +583,9 @@ public FontDescription(
PDDocument doc, FSSupplier<InputStream> supplier,
int weight, IdentValue style, String family,
boolean isFromFontFace, boolean isSubset,
FSCacheEx<String, FSCacheValue> metricsCache) {
FSCacheEx<String, FSCacheValue> metricsCache,
FontCache fontCache) {
this._fontSupplier = null;
this._supplier = supplier;
this._weight = weight;
this._style = style;
Expand All @@ -540,19 +595,22 @@ public FontDescription(
this._isSubset = isSubset;
this._metricsCache = metricsCache;
this._metrics = getFontMetricsFromCache(family, weight, style);
this._fontCache = fontCache;
}

/**
* Create a font description when a PDFont is definitely available to begin with.
* Currently only used for PDF built-in fonts.
*/
private FontDescription(PDDocument doc, PDFont font, IdentValue style, int weight) {
_fontSupplier = null;
_font = font;
_style = style;
_weight = weight;
_supplier = null;
_doc = doc;
_metricsCache = null;
_fontCache = null;
_family = null;
_isFromFontFace = false;
_isSubset = false;
Expand All @@ -573,7 +631,8 @@ public FontDescription(
PDDocument doc, FSSupplier<PDFont> fontSupplier,
IdentValue style, int weight, String family,
boolean isFromFontFace, boolean isSubset,
FSCacheEx<String, FSCacheValue> metricsCache) {
FSCacheEx<String, FSCacheValue> metricsCache,
FontCache fontCache) {
_fontSupplier = fontSupplier;
_style = style;
_weight = weight;
Expand All @@ -584,6 +643,7 @@ public FontDescription(
_isSubset = isSubset;
_metricsCache = metricsCache;
_metrics = getFontMetricsFromCache(family, weight, style);
_fontCache = fontCache;
}

public String getFamily() {
Expand Down Expand Up @@ -614,45 +674,67 @@ private boolean loadMetrics() {
}
}

/**
* Resolves the font corresponding to this descriptor.
*
* @return Whether the font exists.
*/
public boolean realizeFont() {
if (_font == null && _fontSupplier != null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_LOADING_FONT_FROM_SUPPLIER, _family, "PDFont");

_font = _fontSupplier.supply();
_fontSupplier = null;
if (!isMetricsAvailable()) {
// If we already have metrics, they must have come from the cache.
return loadMetrics();
if (_font == null) {
if (_fontCache != null) {
// Font cache match?
if ((_font = _fontCache.get(_family, _weight, _style)) != null) {
// Dropping useless suppliers...
_fontCache = null;
_fontSupplier = null;
_supplier = null;
}
}
}

if (_font == null && _supplier != null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_LOADING_FONT_FROM_SUPPLIER, _family, "InputStream");

InputStream is = _supplier.supply();
_supplier = null; // We only try once.

if (is == null) {
return false;

/*
* NOTE: _fontSupplier and _supplier are mutually exclusive.
*/
if (_fontSupplier != null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_LOADING_FONT_FROM_SUPPLIER, _family, "PDFont");

_font = _fontSupplier.supply();
_fontSupplier = null /* Only tried once */;
} else if (_supplier != null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_LOADING_FONT_FROM_SUPPLIER, _family, "InputStream");

try (InputStream is = _supplier.supply()) {
_supplier = null /* Only tried once */;

if (is != null) {
try {
_font = PDType0Font.load(_doc, is, _isSubset);
} catch (IOException e) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.EXCEPTION_COULD_NOT_LOAD_FONT, _family);
}
}
} catch (IOException e) {
/* TODO: log failed close()? */
}
}

try {
_font = PDType0Font.load(_doc, is, _isSubset);


if (_font != null) {
if (_fontCache != null) {
_fontCache.put(_family, _weight, _style, _font);
_fontCache = null /* Only used once */;
}

if (!isMetricsAvailable()) {
return loadMetrics();
/*
* TODO: The old implementation returned loadMetrics() result just one time,
* thus violating the idempotence of this method, as subsequent calls didn't
* return it again. Now the idempotence is honored, but we have to verify
* whether any weak side effect (relying on the old wrong behavior) is
* disrupted (although, according to my tests, it seems to work flawlessly).
*/
loadMetrics();
}
} catch (IOException e) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.EXCEPTION_COULD_NOT_LOAD_FONT, _family);
return false;
} finally {
try {
is.close();
} catch (IOException e) { }
}
}

return _font != null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,14 @@ public class PdfBoxRenderer implements Closeable, PageSupplier {

private final Closeable diagnosticConsumer;

private PdfRendererBuilderState state;

/**
* This method is constantly changing as options are added to the builder.
*/
PdfBoxRenderer(BaseDocument doc, UnicodeImplementation unicode,
PageDimensions pageSize, PdfRendererBuilderState state, Closeable diagnosticConsumer) {
this.state = state;

this.diagnosticConsumer = diagnosticConsumer;

Expand Down Expand Up @@ -198,7 +201,7 @@ public class PdfBoxRenderer implements Closeable, PageSupplier {
userAgent.setSharedContext(_sharedContext);
_outputDevice.setSharedContext(_sharedContext);

PdfBoxFontResolver fontResolver = new PdfBoxFontResolver(_sharedContext, _pdfDoc, state._caches.get(CacheStore.PDF_FONT_METRICS), state._pdfAConformance, state._pdfUaConform);
PdfBoxFontResolver fontResolver = new PdfBoxFontResolver(_sharedContext, _pdfDoc, state._caches.get(CacheStore.PDF_FONT_METRICS), state._fontCache, state._pdfAConformance, state._pdfUaConform);
_sharedContext.setFontResolver(fontResolver);

PdfBoxReplacedElementFactory replacedElementFactory = new PdfBoxReplacedElementFactory(_outputDevice, state._svgImpl, state._objectDrawerFactory, state._mathmlImpl);
Expand Down Expand Up @@ -270,6 +273,20 @@ else if (doc.file != null) {
this._os = state._os;
}

/**
* Creates a new builder inheriting this configuration.
*/
public PdfRendererBuilder toBuilder() {
PdfRendererBuilderState newState = state.clone();
/*
* NOTE: Old input references are cleared to ensure a clean new run.
*/
newState._document = null;
newState._file = null;
newState._html = null;
return new PdfRendererBuilder(newState);
}

public Document getDocument() {
return _doc;
}
Expand Down
Loading