From 87fb2a97f7af79c243c4f8be5a726eedb769f1d9 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 17 Jul 2026 12:06:09 +0100 Subject: [PATCH 1/3] feat(pptx): warn explicitly whenever a font is substituted in the deck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A font that cannot travel into the .pptx identically now says so, once per family per render pass, on the render logger: standard-14 families warn with their metric-compatible replacement (Helvetica renders as Arial — identical widths, slightly different letterforms); families registered without binary sources and unregistered font names warn that the deck carries a name-only reference that viewers without the font will substitute; license-restricted embeds already warned. An embedded family stays silent — registering binary sources (registerFontFamily) remains the way to guarantee identical glyphs in both formats on any machine. PptxFontMapping.standardReplacementFor exposes the replacement decision; PptxFontSubstitutionWarningTest pins all three outcomes and the once-per-family semantics through a logback appender. --- CHANGELOG.md | 6 ++ .../fixed/pptx/PptxRenderEnvironment.java | 26 ++++++ .../fixed/pptx/handlers/PptxFontMapping.java | 23 ++++++ .../pptx/PptxFontSubstitutionWarningTest.java | 79 +++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a89d6cfc..cee5bdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,12 @@ follow semantic versioning; release dates are ISO 8601. PowerPoint tables, which re-lay-out their content. Contiguous table rows paint all fills before any border or text, the PDF backend's two-pass discipline, and row-spanning cells grow downward through their merged rows. +- The fixed PPTX backend warns explicitly whenever a font cannot travel into + the deck identically — once per family per render: standard-14 families name + their metric-compatible replacement (Helvetica → Arial), families registered + without binary sources and unregistered names are flagged as name-only + references that viewers may substitute, and license-restricted embeds + already degrade with a warning. An embedded family stays silent. - `PptxFixedLayoutBackend.Builder.rasterSlides(int dpi)` — raster-slide mode: every page renders through the PDF backend and lands as one full-slide picture, a pixel-exact copy of the PDF/PNG output for decks that must look diff --git a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java index 2966ba23..101f25e3 100644 --- a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java +++ b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java @@ -65,6 +65,7 @@ public final class PptxRenderEnvironment { private final List bookmarkRecords = new ArrayList<>(); private final Map anchorDestinations = new LinkedHashMap<>(); private final List fragmentLinks = new ArrayList<>(); + private final Set substitutionWarned = new LinkedHashSet<>(); PptxRenderEnvironment(XMLSlideShow show, PptxRenderSession session, @@ -81,6 +82,12 @@ public final class PptxRenderEnvironment { Map metrics = new LinkedHashMap<>(); for (FontFamilyDefinition family : customFontFamilies) { familyNames.put(family.name(), family.wordFamily()); + if (family.fontSourceSet().isEmpty()) { + LOG.warn("render.pptx.font.substitution family={} — registered without binary " + + "sources, the deck references \"{}\" by name only; viewers " + + "without that font installed will substitute their own", + family.name().name(), family.wordFamily()); + } family.fontSourceSet().ifPresent(sources -> { if (embedFontFamily(show, family, sources)) { metrics.put(family.name(), PptxViewerMetrics.load(family, sources)); @@ -156,6 +163,25 @@ public String fontFamily(FontName fontName) { return customFamily; } } + String key = (fontName == null ? FontName.HELVETICA : fontName).name(); + String replacement = PptxFontMapping.standardReplacementFor(fontName); + if (replacement != null) { + if (substitutionWarned.add(key)) { + LOG.warn("render.pptx.font.substitution family={} — a PDF built-in with no " + + "PPTX counterpart, rendered as metric-compatible \"{}\" " + + "(identical widths, slightly different letterforms); register " + + "a binary font family for identical glyphs in both formats", + key, replacement); + } + } else if (substitutionWarned.add(key)) { + // Not registered for this render: the deck can only reference the + // family by name, so a viewer without it installed substitutes. + LOG.warn("render.pptx.font.substitution family={} — not registered with binary " + + "sources for this render, the deck references \"{}\" by name " + + "only; register the family (registerFontFamily) to embed it and " + + "guarantee identical glyphs on every machine", + key, PptxFontMapping.familyFor(fontName)); + } return PptxFontMapping.familyFor(fontName); } diff --git a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java index 5b739faa..a5ae1561 100644 --- a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java +++ b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java @@ -54,6 +54,29 @@ public static String familyFor(FontName fontName) { return stripStyleSuffix(name); } + /** + * Returns the metric-compatible viewer family that replaces a standard-14 + * PostScript font in PPTX output, or {@code null} when the name is not a + * standard-14 family (binary families keep their own name). + * + * @param fontName logical document font + * @return replacement family name, or {@code null} + */ + public static String standardReplacementFor(FontName fontName) { + String lower = (fontName == null ? FontName.HELVETICA : fontName) + .name().toLowerCase(Locale.ROOT); + if (lower.startsWith("helvetica")) { + return "Arial"; + } + if (lower.startsWith("times")) { + return "Times New Roman"; + } + if (lower.startsWith("courier")) { + return "Courier New"; + } + return null; + } + /** * Returns the viewer ascent ratio for a standard-14 replacement font. * diff --git a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java new file mode 100644 index 00000000..a634190f --- /dev/null +++ b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java @@ -0,0 +1,79 @@ +package com.demcha.compose.document.backend.fixed.pptx; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.demcha.compose.document.backend.fixed.pdf.PdfMeasurementResources; +import com.demcha.compose.font.FontFamilyDefinition; +import com.demcha.compose.font.FontName; +import org.apache.poi.xslf.usermodel.XMLSlideShow; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A font that cannot travel into the deck identically must say so: standard-14 + * families warn about their metric-compatible replacement, unregistered names + * warn that the deck carries a name-only reference, and each family warns once + * per render pass. An embedded family stays silent. + */ +class PptxFontSubstitutionWarningTest { + + private static final FontName EMBEDDED = FontName.of("WarnLato"); + private static final FontFamilyDefinition EMBEDDED_FAMILY = FontFamilyDefinition.classpath( + EMBEDDED, "fonts/google/lato/Lato-Regular.ttf") + .wordFamily("Lato") + .build(); + + private ListAppender appender; + private Logger logger; + + @BeforeEach + void attachAppender() { + logger = (Logger) LoggerFactory.getLogger("com.demcha.compose.engine.render"); + appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + } + + @AfterEach + void detachAppender() { + logger.detachAppender(appender); + } + + @Test + void substitutedFamiliesWarnOncePerRenderPassAndEmbeddedFamiliesStaySilent() throws Exception { + try (XMLSlideShow show = new XMLSlideShow(); + PdfMeasurementResources measurement = PdfMeasurementResources.open(List.of(EMBEDDED_FAMILY))) { + PptxRenderSession session = new PptxRenderSession(show, 300, 200, 1); + PptxRenderEnvironment environment = new PptxRenderEnvironment( + show, session, 0, 200, measurement.fontLibrary(), List.of(EMBEDDED_FAMILY)); + + assertThat(environment.fontFamily(FontName.HELVETICA)).isEqualTo("Arial"); + assertThat(environment.fontFamily(FontName.HELVETICA)).isEqualTo("Arial"); + assertThat(environment.fontFamily(FontName.of("Roboto-Regular"))).isEqualTo("Roboto"); + assertThat(environment.fontFamily(EMBEDDED)).isEqualTo("Lato"); + + List warnings = appender.list.stream() + .map(ILoggingEvent::getFormattedMessage) + .filter(message -> message.startsWith("render.pptx.font.substitution")) + .toList(); + assertThat(warnings) + .as("one warning per substituted family, none for the embedded one") + .hasSize(2); + assertThat(warnings.get(0)) + .contains("Helvetica") + .contains("Arial") + .contains("metric-compatible"); + assertThat(warnings.get(1)) + .contains("Roboto-Regular") + .contains("by name"); + assertThat(warnings).noneMatch(message -> message.contains("WarnLato")); + } + } +} From 1fd6fad4bc06ca5a75032aac076abf849c5f9203 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 17 Jul 2026 12:10:06 +0100 Subject: [PATCH 2/3] test(pptx): font-identity specimen pair across four embedded families MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders the same specimen line (regular/bold/italic plus a size ramp) through four embedded families — Lato, Poppins, JetBrains Mono, Spectral — into the PDF/PPTX pair under target/visual-tests/pptx-parity/font-identity/. Both outputs embed the same TTF programs, so a real viewer shows identical glyphs; the specimen exists to check that claim by eye, page against slide. --- .../fixed/pptx/PptxFontShowcaseDemoTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java diff --git a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java new file mode 100644 index 00000000..72bbaa65 --- /dev/null +++ b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java @@ -0,0 +1,114 @@ +package com.demcha.compose.document.backend.fixed.pptx; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontFamilyDefinition; +import com.demcha.compose.font.FontName; +import org.apache.poi.xslf.usermodel.XMLSlideShow; +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Font-identity specimen: four embedded families (sans, geometric sans, mono, + * serif) with regular/bold/italic runs and a size ramp, rendered to the + * PDF/PPTX pair. Both outputs embed the same TTF programs, so a real viewer + * shows identical glyphs; the specimen exists so that claim can be checked by + * eye, page against slide. + */ +class PptxFontShowcaseDemoTest { + + private static final String SPECIMEN = "Grumpy wizards 1234567890 fjord-quiz"; + + private record Family(FontName name, String folder, String file, String viewerFamily) { + } + + private static final List FAMILIES = List.of( + new Family(FontName.of("ShowcaseLato"), "lato", "Lato", "Lato"), + new Family(FontName.of("ShowcasePoppins"), "poppins", "Poppins", "Poppins"), + new Family(FontName.of("ShowcaseJetBrainsMono"), "jetbrainsmono", "JetBrainsMono", + "JetBrains Mono"), + new Family(FontName.of("ShowcaseSpectral"), "spectral", "Spectral", "Spectral")); + + @Test + void writesTheFontIdentitySpecimenPair() throws Exception { + Path output = Path.of("target", "visual-tests", "pptx-parity", "font-identity"); + Files.createDirectories(output); + + try (DocumentSession session = composeSpecimen()) { + byte[] pdf = session.toPdfBytes(); + Files.write(output.resolve("font-identity.pdf"), pdf); + byte[] pptx = session.render(new PptxFixedLayoutBackend()); + Files.write(output.resolve("font-identity.pptx"), pptx); + + var pdfPages = session.toImages(144); + try (XMLSlideShow show = new XMLSlideShow(new ByteArrayInputStream(pptx))) { + assertThat(show.getSlides()).hasSameSizeAs(pdfPages); + for (int i = 0; i < pdfPages.size(); i++) { + ImageIO.write(pdfPages.get(i), "png", + output.resolve("font-identity-page-" + (i + 1) + ".pdf.png").toFile()); + } + } + } + } + + private static DocumentSession composeSpecimen() { + var builder = GraphCompose.document() + .pageSize(560, 420) + .margin(DocumentInsets.of(28)); + for (Family family : FAMILIES) { + builder.registerFontFamily(FontFamilyDefinition.classpath( + family.name(), "fonts/google/" + family.folder() + + "/" + family.file() + "-Regular.ttf") + .boldResource("fonts/google/" + family.folder() + + "/" + family.file() + "-Bold.ttf") + .italicResource("fonts/google/" + family.folder() + + "/" + family.file() + "-Italic.ttf") + .boldItalicResource("fonts/google/" + family.folder() + + "/" + family.file() + "-BoldItalic.ttf") + .wordFamily(family.viewerFamily()) + .build()); + } + DocumentSession session = builder.create(); + session.pageFlow(page -> page.module("specimen", module -> { + module.paragraph(p -> p.text("Embedded font identity — PDF vs PPTX") + .textStyle(DocumentTextStyle.builder() + .fontName(FAMILIES.get(0).name()).size(20) + .decoration(DocumentTextDecoration.BOLD) + .color(DocumentColor.rgb(15, 23, 42)).build())); + for (Family family : FAMILIES) { + module.paragraph(p -> p.text(family.viewerFamily()) + .textStyle(DocumentTextStyle.builder() + .fontName(family.name()).size(9) + .color(DocumentColor.GRAY).build())); + module.paragraph(p -> p.rich(r -> r + .plain(SPECIMEN + " ") + .bold("bold") + .plain(" ") + .italic("italic")) + .textStyle(DocumentTextStyle.builder() + .fontName(family.name()).size(13).build())); + } + module.paragraph(p -> p.text("Size ramp:") + .textStyle(DocumentTextStyle.builder() + .fontName(FAMILIES.get(0).name()).size(9) + .color(DocumentColor.GRAY).build())); + for (int size : new int[]{10, 14, 18, 24}) { + module.paragraph(p -> p.text(size + " pt " + SPECIMEN) + .textStyle(DocumentTextStyle.builder() + .fontName(FAMILIES.get(0).name()).size(size).build())); + } + })); + return session; + } +} From 848f0f9c105510877827b9c8628447af81420e4d Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 17 Jul 2026 12:27:09 +0100 Subject: [PATCH 3/3] refactor(pptx): family-level substitution dedupe, embed-failure key, matrix note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitution warnings now dedupe on the resolved viewer family, so facet-suffixed names (Roboto-Regular, Roboto-Bold) warn once per family per render while the message still names the source font the document asked for. A family whose embed fails also emits the shared render.pptx.font.substitution key — one log filter now catches every substitution — alongside the cause-carrying embed-skip warning. familyFor delegates to standardReplacementFor, collapsing the third prefix switch on the same standard-14 names. The capability matrix states the explicit-substitution contract (the sentence previously missed the file). The specimen demo now asserts the deck actually embeds all four families; the warning test covers the facet dedupe and stops its appender. --- .../architecture/backend-capability-matrix.md | 4 ++++ .../fixed/pptx/PptxRenderEnvironment.java | 23 ++++++++++++++----- .../fixed/pptx/handlers/PptxFontMapping.java | 15 ++++-------- .../fixed/pptx/PptxFontShowcaseDemoTest.java | 14 +++++++---- .../pptx/PptxFontSubstitutionWarningTest.java | 4 ++++ 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index b9abe0d3..d10c4e3a 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -107,3 +107,7 @@ with the fonts installed on the viewing machine. Standard-14 font names are mapp metric-compatible system fonts (Helvetica → Arial, Times → Times New Roman, Courier → Courier New); document-local fonts use their registered viewer-facing `wordFamily` and should be installed on the viewer for exact glyphs. +Every substitution is explicit: the backend logs +`render.pptx.font.substitution` once per family per render whenever a +font travels as a standard-14 replacement or a name-only reference +instead of an embedded program; embedded families render silently. diff --git a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java index 101f25e3..7aa46b9e 100644 --- a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java +++ b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/PptxRenderEnvironment.java @@ -91,6 +91,13 @@ public final class PptxRenderEnvironment { family.fontSourceSet().ifPresent(sources -> { if (embedFontFamily(show, family, sources)) { metrics.put(family.name(), PptxViewerMetrics.load(family, sources)); + } else { + // Same key as every other substitution so one log filter + // catches them all; the embed-skip warning carries the cause. + LOG.warn("render.pptx.font.substitution family={} — could not be embedded, " + + "the deck references \"{}\" by name only; viewers without " + + "that font installed will substitute their own", + family.name().name(), family.wordFamily()); } }); } @@ -163,26 +170,30 @@ public String fontFamily(FontName fontName) { return customFamily; } } - String key = (fontName == null ? FontName.HELVETICA : fontName).name(); + // Dedupe on the resolved viewer family so facet-suffixed names + // (Roboto-Regular, Roboto-Bold) warn once, not once per facet; the + // message still names the source font the document asked for. + String viewerFamily = PptxFontMapping.familyFor(fontName); + String sourceName = (fontName == null ? FontName.HELVETICA : fontName).name(); String replacement = PptxFontMapping.standardReplacementFor(fontName); if (replacement != null) { - if (substitutionWarned.add(key)) { + if (substitutionWarned.add(viewerFamily)) { LOG.warn("render.pptx.font.substitution family={} — a PDF built-in with no " + "PPTX counterpart, rendered as metric-compatible \"{}\" " + "(identical widths, slightly different letterforms); register " + "a binary font family for identical glyphs in both formats", - key, replacement); + sourceName, replacement); } - } else if (substitutionWarned.add(key)) { + } else if (substitutionWarned.add(viewerFamily)) { // Not registered for this render: the deck can only reference the // family by name, so a viewer without it installed substitutes. LOG.warn("render.pptx.font.substitution family={} — not registered with binary " + "sources for this render, the deck references \"{}\" by name " + "only; register the family (registerFontFamily) to embed it and " + "guarantee identical glyphs on every machine", - key, PptxFontMapping.familyFor(fontName)); + sourceName, viewerFamily); } - return PptxFontMapping.familyFor(fontName); + return viewerFamily; } /** diff --git a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java index a5ae1561..68cde5d3 100644 --- a/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java +++ b/render-pptx/src/main/java/com/demcha/compose/document/backend/fixed/pptx/handlers/PptxFontMapping.java @@ -40,18 +40,11 @@ private PptxFontMapping() { * @return PPTX family name */ public static String familyFor(FontName fontName) { - String name = (fontName == null ? FontName.HELVETICA : fontName).name(); - String lower = name.toLowerCase(Locale.ROOT); - if (lower.startsWith("helvetica")) { - return "Arial"; - } - if (lower.startsWith("times")) { - return "Times New Roman"; - } - if (lower.startsWith("courier")) { - return "Courier New"; + String replacement = standardReplacementFor(fontName); + if (replacement != null) { + return replacement; } - return stripStyleSuffix(name); + return stripStyleSuffix((fontName == null ? FontName.HELVETICA : fontName).name()); } /** diff --git a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java index 72bbaa65..bb8ae9c5 100644 --- a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java +++ b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontShowcaseDemoTest.java @@ -52,12 +52,18 @@ void writesTheFontIdentitySpecimenPair() throws Exception { Files.write(output.resolve("font-identity.pptx"), pptx); var pdfPages = session.toImages(144); + for (int i = 0; i < pdfPages.size(); i++) { + ImageIO.write(pdfPages.get(i), "png", + output.resolve("font-identity-page-" + (i + 1) + ".pdf.png").toFile()); + } try (XMLSlideShow show = new XMLSlideShow(new ByteArrayInputStream(pptx))) { assertThat(show.getSlides()).hasSameSizeAs(pdfPages); - for (int i = 0; i < pdfPages.size(); i++) { - ImageIO.write(pdfPages.get(i), "png", - output.resolve("font-identity-page-" + (i + 1) + ".pdf.png").toFile()); - } + var embeddedList = show.getCTPresentation().getEmbeddedFontLst(); + assertThat(embeddedList).as("the deck must embed the specimen fonts").isNotNull(); + assertThat(embeddedList.getEmbeddedFontList()) + .extracting(entry -> entry.getFont().getTypeface()) + .as("all four families travel as embedded programs") + .containsExactlyInAnyOrder("Lato", "Poppins", "JetBrains Mono", "Spectral"); } } } diff --git a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java index a634190f..97abb8bc 100644 --- a/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java +++ b/render-pptx/src/test/java/com/demcha/compose/document/backend/fixed/pptx/PptxFontSubstitutionWarningTest.java @@ -44,6 +44,7 @@ void attachAppender() { @AfterEach void detachAppender() { logger.detachAppender(appender); + appender.stop(); } @Test @@ -57,6 +58,9 @@ void substitutedFamiliesWarnOncePerRenderPassAndEmbeddedFamiliesStaySilent() thr assertThat(environment.fontFamily(FontName.HELVETICA)).isEqualTo("Arial"); assertThat(environment.fontFamily(FontName.HELVETICA)).isEqualTo("Arial"); assertThat(environment.fontFamily(FontName.of("Roboto-Regular"))).isEqualTo("Roboto"); + assertThat(environment.fontFamily(FontName.of("Roboto-Bold"))) + .as("facet of an already-warned family") + .isEqualTo("Roboto"); assertThat(environment.fontFamily(EMBEDDED)).isEqualTo("Lato"); List warnings = appender.list.stream()