From b8bf9b155061c420279d97c95e6290546c0b177d Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Wed, 7 Oct 2026 10:12:36 +1100 Subject: [PATCH] FOP-3356: simulate-style does not slant an italic face or stroke a bold one, and strokes at 1/35 em PDFPainter.startSimulateStyle synthesised a style from the font entry's simulate-style flag and the triplet alone: a bold triplet was stroked with a fixed 0.31543pt line and an italic one sheared by 0.3333, whatever the face. So an italic face registered for a bold italic triplet was slanted a second time, a bold face registered for a bold italic one was stroked again, and the stroke, right at about 11pt, was too light at larger sizes and too heavy at smaller ones. Now an italic triplet is sheared only where the face's italic angle is 0, and a bold one stroked only where the face's weight class is below 700 (an unknown weight counting as not bold, as before). The stroke is the font size / 35, which is what Word draws for a synthetic bold at every size; at 11.04pt it is the former 0.31543. Only the painted glyphs change, never layout. Tests: PDFPainterTestCase, an italic face not sheared, a bold face not stroked, the stroke at 22.08pt (0.63086); each fails before. Co-Authored-By: Claude Opus 5.5 --- .../org/apache/fop/render/pdf/PDFPainter.java | 20 ++++-- .../fop/render/pdf/PDFPainterTestCase.java | 62 +++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java index 3ac5505fc66..34cb12adb22 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFPainter.java @@ -607,15 +607,22 @@ private void drawTextWithDX(int x, int y, String text, FontTriplet triplet, endSimulateStyle(tf, triplet); } + /** + * Synthesises the style a triplet names from the face it is registered on, where the font entry asks for it + * (simulate-style), as Word synthesises a style a family lacks: a bold triplet is stroked at 1/35 em, and an + * italic one sheared by 0.3333, each only where the face is not bold, or not italic, already. + * @return the shear for the text matrix + */ private double startSimulateStyle(Typeface tf, FontTriplet triplet) { double shear = 0; boolean simulateStyle = tf instanceof CustomFont && ((CustomFont) tf).getSimulateStyle(); if (simulateStyle) { - if (triplet.getWeight() == 700) { + CustomFont face = (CustomFont) tf; + if (simulatesBold(face, triplet)) { generator.updateColor(state.getTextColor(), false, null); - generator.add("2 Tr 0.31543 w\n"); + generator.add("2 Tr " + PDFNumber.doubleOut(state.getFontSize() / 1000.0 / 35, 5) + " w\n"); } - if (triplet.getStyle().equals("italic")) { + if (triplet.getStyle().equals("italic") && face.getItalicAngle() == 0) { shear = 0.3333; } } @@ -624,11 +631,16 @@ private double startSimulateStyle(Typeface tf, FontTriplet triplet) { private void endSimulateStyle(Typeface tf, FontTriplet triplet) { boolean simulateStyle = tf instanceof CustomFont && ((CustomFont) tf).getSimulateStyle(); - if (simulateStyle && triplet.getWeight() == 700) { + if (simulateStyle && simulatesBold((CustomFont) tf, triplet)) { generator.add("0 Tr\n"); } } + /** A bold triplet on a face that is not bold already (its weight class below 700, or unknown). */ + private static boolean simulatesBold(CustomFont face, FontTriplet triplet) { + return triplet.getWeight() == 700 && face.getWeight() < 700; + } + private static int[] paZero = new int[4]; private void drawTextWithDP(int x, int y, String text, FontTriplet triplet, diff --git a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java index bd6cf5b2488..a4afd430772 100644 --- a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java +++ b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFPainterTestCase.java @@ -44,6 +44,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.endsWith; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; @@ -290,14 +291,67 @@ public void testSimulateStyle() throws IFException { fi.addMetrics("f1", font); pdfDocumentHandler.setFontInfo(fi); MyPDFPainter pdfPainter = new MyPDFPainter(pdfDocumentHandler, null); - pdfPainter.setFont("a", "italic", 700, null, 12, null); + pdfPainter.setFont("a", "italic", 700, null, 11040, null); + pdfPainter.drawText(0, 0, 0, 0, null, "test"); + + assertEquals(sb.toString(), "BT\n/f1 11.04 Tf\n1 0 0.3333 -1 0 0 Tm [<0000000000000000>] TJ\n"); + verify(pdfContentGenerator).add("2 Tr 0.31543 w\n"); + verify(pdfContentGenerator).add("0 Tr\n"); + } + + /** The output of a bold italic triplet on a face, at a size, for the simulated-style tests. */ + private String simulateStyle(MultiByteFont font, int size) throws IFException { + final StringBuilder sb = new StringBuilder(); + pdfDocumentHandler = makePDFDocumentHandler(sb); + FontInfo fi = new FontInfo(); + fi.addFontProperties("f1", new FontTriplet("a", "italic", 700)); + font.setSimulateStyle(true); + fi.addMetrics("f1", font); + pdfDocumentHandler.setFontInfo(fi); + MyPDFPainter pdfPainter = new MyPDFPainter(pdfDocumentHandler, null); + pdfPainter.setFont("a", "italic", 700, null, size, null); pdfPainter.drawText(0, 0, 0, 0, null, "test"); + return sb.toString(); + } - assertEquals(sb.toString(), "BT\n/f1 0.012 Tf\n1 0 0.3333 -1 0 0 Tm [<0000000000000000>] TJ\n"); + /** + * An italic face is not slanted again, but is stroked, as Word draws the italic face of a family that has no + * bold one. + * @throws IFException if the painting fails + */ + @Test + public void testSimulateStyleItalicFaceIsNotSheared() throws IFException { + MultiByteFont font = new MultiByteFont(null, null); + font.setItalicAngle(-11); + assertEquals("BT\n/f1 11.04 Tf\n1 0 0 -1 0 0 Tm [<0000000000000000>] TJ\n", simulateStyle(font, 11040)); verify(pdfContentGenerator).add("2 Tr 0.31543 w\n"); verify(pdfContentGenerator).add("0 Tr\n"); } + /** + * A bold face is not stroked again, but is sheared, as Word draws the bold face of a family that has no bold + * italic one. + * @throws IFException if the painting fails + */ + @Test + public void testSimulateStyleBoldFaceIsNotStroked() throws IFException { + MultiByteFont font = new MultiByteFont(null, null); + font.setWeight(700); + assertEquals("BT\n/f1 11.04 Tf\n1 0 0.3333 -1 0 0 Tm [<0000000000000000>] TJ\n", simulateStyle(font, 11040)); + verify(pdfContentGenerator, never()).add(contains(" Tr ")); + verify(pdfContentGenerator, never()).add("0 Tr\n"); + } + + /** + * The simulated bold's stroke is 1/35 em, Word's at every size: 0.63086 at 22.08pt. + * @throws IFException if the painting fails + */ + @Test + public void testSimulateStyleStrokeScalesWithSize() throws IFException { + simulateStyle(new MultiByteFont(null, null), 22080); + verify(pdfContentGenerator).add("2 Tr 0.63086 w\n"); + } + @Test public void testSoftHyphenNotReplaced() throws IFException { pdfDocumentHandler = makePDFDocumentHandler(new StringBuilder()); @@ -376,7 +430,7 @@ public void testSimulateStyleColor() throws Exception { fi.addMetrics("f1", font); pdfDocumentHandler.setFontInfo(fi); PDFPainter pdfPainter = new PDFPainter(pdfDocumentHandler, null); - pdfPainter.setFont("a", "italic", 700, null, 12, Color.red); + pdfPainter.setFont("a", "italic", 700, null, 11040, Color.red); pdfPainter.drawText(0, 0, 0, 0, null, "test"); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -389,7 +443,7 @@ public void testSimulateStyleColor() throws Exception { + "1 0 0 -1 0 0 cm\n" + "1 0 0 rg\n" + "BT\n" - + "/f1 0.012 Tf\n" + + "/f1 11.04 Tf\n" + "1 0 0 RG\n" + "2 Tr 0.31543 w\n" + "1 0 0.3333 -1 0 0 Tm [<0000000000000000>] TJ\n"