Skip to content

FontUtils static font maps make boxable unsafe for concurrent PDF generation → The TrueType font null does not contain a 'cmap' table #305

Description

@Susan123456789

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:

  1. Paragraph resolves the font for <b> segments from FontUtils.getDefaultfonts() (static), not from the Cell's own setFontBold(...).
  2. Each build calls addDefaultFonts(...) with its own PDFonts, overwriting the shared map.
  3. Thread B's addDefaultFonts replaces the map mid-build of thread A, so A draws a <b> segment with B's bold font.
  4. PDPageContentStream.setFont(font) registers a subset font in the current document, so B's font ends up in both documents' subset sets.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions