FontUtils static font maps make boxable unsafe for concurrent PDF generation → The TrueType font null does not contain a 'cmap' table
Summary
be.quodlibet.boxable.utils.FontUtils keeps the default bold/italic fonts in a process-global static map. addDefaultFonts(...) overwrites it and Paragraph reads bold/italic fonts back from it — so building two PDFs concurrently lets one document's fonts leak into another, intermittently crashing at save time:
java.io.IOException: The TrueType font null does not contain a 'cmap' table
at org.apache.fontbox.ttf.TTFSubsetter.<init>(TTFSubsetter.java:100)
at org.apache.pdfbox.pdmodel.font.PDType0Font.subset(PDType0Font.java:310)
at org.apache.pdfbox.pdmodel.PDDocument.subsetDesignatedFonts(PDDocument.java:1045)
at org.apache.pdfbox.pdmodel.PDDocument.save(PDDocument.java:1033)
Environment: boxable 1.7.3, PDFBox/FontBox 3.0.7, Java 17.
Mechanism
Needs a drawn <b> segment + concurrency:
Paragraph resolves the font for <b> segments from FontUtils.getDefaultfonts() (static), not from the Cell's own setFontBold(...).
- Each build calls
addDefaultFonts(...) with its own PDFonts, overwriting the shared map.
- Thread B's
addDefaultFonts replaces the map mid-build of thread A, so A draws a <b> segment with B's bold font.
PDPageContentStream.setFont(font) registers a subset font in the current document, so B's font ends up in both documents' subset sets.
- The first save subsets and closes that font's
TrueTypeFont; the second save re-subsets the now-closed font → unreadable cmap/name (hence the null name).
(fontMetrics is also a plain HashMap mutated without synchronization — a related race.)
Reproduction
Two threads, each with its own PDDocument and freshly-loaded subset fonts, drawing a cell containing <b>:
Runnable job = () -> {
for (int i = 0; i < 50; i++) {
try (PDDocument doc = new PDDocument()) {
PDFont regular = PDType0Font.load(doc, new File("DejaVuSans.ttf"));
PDFont bold = PDType0Font.load(doc, new File("DejaVuSans-Bold.ttf"));
FontUtils.addDefaultFonts(regular, bold,
new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE),
new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD_OBLIQUE));
PDPage page = new PDPage(PDRectangle.LETTER);
doc.addPage(page);
BaseTable table = new BaseTable(750, 750, 50, 500, 40, doc, page, true, true);
Row<PDPage> row = table.createRow(15);
Cell<PDPage> cell = row.createCell(100, "Hello <b>BOLD</b> world");
cell.setFont(regular);
cell.setFontBold(bold);
table.draw();
doc.save(new ByteArrayOutputStream()); // subsetting happens here
} catch (Exception e) { e.printStackTrace(); }
}
};
Thread t1 = new Thread(job), t2 = new Thread(job);
t1.start(); t2.start(); t1.join(); t2.join();
Throws the cmap IOException intermittently; the same workload on a single thread never fails.
Suggested fix
Have Cell/Paragraph use the Cell's own fontBold/fontItalic (already stored via setFontBold/setFontItalic) instead of the static FontUtils.getDefaultfonts(), removing the cross-document leak. At minimum, make the maps thread-safe and document that the current design isn't safe for concurrent document generation.
FontUtilsstatic font maps make boxable unsafe for concurrent PDF generation →The TrueType font null does not contain a 'cmap' tableSummary
be.quodlibet.boxable.utils.FontUtilskeeps the default bold/italic fonts in a process-globalstaticmap.addDefaultFonts(...)overwrites it andParagraphreads bold/italic fonts back from it — so building two PDFs concurrently lets one document's fonts leak into another, intermittently crashing at save time:Environment: boxable 1.7.3, PDFBox/FontBox 3.0.7, Java 17.
Mechanism
Needs a drawn
<b>segment + concurrency:Paragraphresolves the font for<b>segments fromFontUtils.getDefaultfonts()(static), not from theCell's ownsetFontBold(...).addDefaultFonts(...)with its ownPDFonts, overwriting the shared map.addDefaultFontsreplaces the map mid-build of thread A, so A draws a<b>segment with B's bold font.PDPageContentStream.setFont(font)registers a subset font in the current document, so B's font ends up in both documents' subset sets.TrueTypeFont; the second save re-subsets the now-closed font → unreadablecmap/name(hence thenullname).(
fontMetricsis also a plainHashMapmutated without synchronization — a related race.)Reproduction
Two threads, each with its own
PDDocumentand freshly-loaded subset fonts, drawing a cell containing<b>:Throws the
cmapIOExceptionintermittently; the same workload on a single thread never fails.Suggested fix
Have
Cell/Paragraphuse theCell's ownfontBold/fontItalic(already stored viasetFontBold/setFontItalic) instead of the staticFontUtils.getDefaultfonts(), removing the cross-document leak. At minimum, make the maps thread-safe and document that the current design isn't safe for concurrent document generation.