Skip to content

Commit 84c9237

Browse files
committed
Merge branch 'master' into v4_experiments
# Conflicts: # pom.xml # src/test/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelperTest.java
2 parents c1459d1 + 3ddfd61 commit 84c9237

4 files changed

Lines changed: 138 additions & 6 deletions

File tree

src/main/java/org/sejda/sambox/cos/PDFDocEncoding.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ final class PDFDocEncoding
5353
{
5454
continue;
5555
}
56-
if (i == 0xAD)
57-
{
58-
continue;
59-
}
6056

6157
set(i, (char) i);
6258
}

src/main/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelper.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ else if (field instanceof PDListBox)
664664
}
665665
else
666666
{
667-
PlainText textContent = new PlainText(value);
667+
PlainText textContent = new PlainText(normalizeWhitespace(value));
668668
AppearanceStyle appearanceStyle = new AppearanceStyle();
669669
appearanceStyle.setFont(font);
670670
appearanceStyle.setFontSize(fontSize);
@@ -993,7 +993,8 @@ float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException
993993
}
994994

995995
// fit width
996-
float width = font.getStringWidth(value) * font.getFontMatrix().getScaleX();
996+
float width = font.getStringWidth(normalizeWhitespace(stripNonPrintableChars(value)))
997+
* font.getFontMatrix().getScaleX();
997998
float widthBasedFontSize = contentRect.getWidth() / width * xScalingFactor;
998999

9991000
// fit height
@@ -1004,6 +1005,19 @@ float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException
10041005
return Math.min(heightBasedFontSize, widthBasedFontSize);
10051006
}
10061007

1008+
private String stripNonPrintableChars(String s)
1009+
{
1010+
// TODO: add more non printable chars
1011+
return s.replaceAll("\\u000D", ""); // carriage return \r
1012+
}
1013+
1014+
private String normalizeWhitespace(String s)
1015+
{
1016+
if (s == null)
1017+
return null;
1018+
return s.replaceAll("\\p{Zs}", " ");
1019+
}
1020+
10071021
/*
10081022
* Resolve the cap height.
10091023
*

src/test/java/org/sejda/sambox/cos/PDFDocEncodingTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.sejda.sambox.cos;
1919

20+
import static org.junit.Assert.assertArrayEquals;
2021
import static org.junit.Assert.assertEquals;
2122
import static org.junit.Assert.assertNull;
2223

@@ -132,4 +133,64 @@ public void testPDFBox3864() throws IOException
132133
assertEquals(cs1.getString(), cs2.getString());
133134
}
134135
}
136+
137+
/**
138+
* 0xAD (SOFT HYPHEN) must decode to U+00AD, not U+0000. ISO 32000-1 leaves it undefined, but
139+
* other producers treat it as its ISO-8859-1 value; decoding it to a NUL control character
140+
* corrupts text strings such as form field names.
141+
*/
142+
@Test
143+
public void softHyphenDecodesToU00AD()
144+
{
145+
assertEquals("\u00AD", PDFDocEncoding.toString(new byte[] { (byte) 0xAD }));
146+
}
147+
148+
@Test
149+
public void softHyphenEncodesTo0xAD()
150+
{
151+
assertArrayEquals(new byte[] { (byte) 0xAD }, PDFDocEncoding.getBytes("\u00AD"));
152+
}
153+
154+
@Test
155+
public void softHyphenRoundTrips()
156+
{
157+
byte[] bytes = { (byte) 0xAD };
158+
assertArrayEquals(bytes, PDFDocEncoding.getBytes(PDFDocEncoding.toString(bytes)));
159+
}
160+
161+
/**
162+
* Reproduces the real-world failure: a German field name with two soft hyphens and an a-umlaut,
163+
* stored as PDFDocEncoding, survives a literal round-trip via COSString and is stored
164+
* single-byte (not UTF-16BE).
165+
*/
166+
@Test
167+
public void fieldNameWithSoftHyphensRoundTrips()
168+
{
169+
String fieldName = "Test\u00ADbesch\u00E4test adas Test\u00ADdsadas";
170+
COSString cosString = COSString.parseLiteral(fieldName);
171+
assertEquals(fieldName, cosString.getString());
172+
// representable in PDFDocEncoding, so single-byte; UTF-16BE would be 2*len + 2 (BOM)
173+
assertEquals(fieldName.length(), cosString.getBytes().length);
174+
}
175+
176+
/**
177+
* Bytes genuinely undefined in PDFDocEncoding (0x7F, 0x9F) must decode to the replacement
178+
* character, never to a silent U+0000.
179+
*/
180+
@Test
181+
public void undefinedBytesDecodeToReplacementCharacter()
182+
{
183+
assertEquals("\uFFFD", PDFDocEncoding.toString(new byte[] { (byte) 0x7F }));
184+
assertEquals("\uFFFD", PDFDocEncoding.toString(new byte[] { (byte) 0x9F }));
185+
}
186+
187+
/**
188+
* Code 0x00 is a legitimate mapping to U+0000 and must not be clobbered by the
189+
* replacement-character pre-fill.
190+
*/
191+
@Test
192+
public void nulByteStillDecodesToU0000()
193+
{
194+
assertEquals("\u0000", PDFDocEncoding.toString(new byte[] { 0x00 }));
195+
}
135196
}

src/test/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelperTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.hamcrest.CoreMatchers.startsWith;
44
import static org.hamcrest.MatcherAssert.assertThat;
55
import static org.junit.Assert.assertEquals;
6+
import static org.junit.Assert.assertNotNull;
67

78
import java.io.IOException;
89
import java.util.Collections;
@@ -114,13 +115,73 @@ public void testInvalidAppearanceString() throws IOException
114115
helper.setAppearanceValue("Some text");
115116
}
116117

118+
@Test
119+
public void testCalculateFontSizeWithNonPrintableChars() throws IOException
120+
{
121+
PDTextField field = multilineTextFieldFaux();
122+
123+
AppearanceGeneratorHelper helper = new AppearanceGeneratorHelper(field);
124+
helper.setAppearanceValue("This isnt multiple lines\r");
125+
126+
assertNotNull(field.getWidgets().get(0).getNormalAppearanceStream());
127+
}
128+
129+
@Test
130+
public void testCalculateFontSizeWithExoticWhiteSpace() throws IOException
131+
{
132+
PDTextField field = multilineTextFieldFaux();
133+
134+
AppearanceGeneratorHelper helper = new AppearanceGeneratorHelper(field);
135+
helper.setAppearanceValue("This is\u2002whitespace");
136+
137+
assertNotNull(field.getWidgets().get(0).getNormalAppearanceStream());
138+
}
139+
140+
@Test
141+
public void testComboBoxAppearanceWithExoticWhiteSpace() throws IOException
142+
{
143+
PDComboBox field = comboBox();
144+
145+
// U+2002 (en space) has no glyph in Helvetica / WinAnsiEncoding. Before it is
146+
// normalized to a regular space, generating the combo box appearance threw
147+
// IllegalArgumentException from PDFont.encode (reached via getStringWidth in
148+
// calculateFontSize, and via showText in the rendering path).
149+
//
150+
// setValue drives the exact path from the original crash report:
151+
// PDChoice.setValue -> applyChange -> constructAppearances -> setAppearanceValue
152+
field.setValue("Option\u2002One");
153+
154+
// appearance generation must complete and produce a stream, not silently skip
155+
assertNotNull(field.getWidgets().get(0).getNormalAppearanceStream());
156+
}
157+
158+
private PDComboBox comboBox()
159+
{
160+
PDDocument doc = new PDDocument();
161+
doc.addPage(new PDPage());
162+
PDAcroForm form = new PDAcroForm(doc);
163+
PDComboBox cb = new PDComboBox(form);
164+
cb.setDefaultAppearance("/Helvetica 10.00 Tf 0 g");
165+
cb.getWidgets().get(0).setRectangle(new PDRectangle(2, 2, 100, 20));
166+
return cb;
167+
}
168+
117169
private PDTextField multilineTextField()
118170
{
119171
PDTextField tf = textField();
120172
tf.setMultiline(true);
121173
return tf;
122174
}
123175

176+
private PDTextField multilineTextFieldFaux()
177+
{
178+
PDTextField field = multilineTextField();
179+
// faux multiline, content rectangle not tall enough
180+
field.getWidgets().get(0).setRectangle(new PDRectangle(2, 2, 517, 10));
181+
field.setDefaultAppearance("Helvetica 0 Tf 0 g");
182+
return field;
183+
}
184+
124185
private PDTextField textField()
125186
{
126187
PDDocument doc = new PDDocument();

0 commit comments

Comments
 (0)