Hey, we are using PDFBox for one of our products and have a proposed improvement for PDFBOX-3812.
The problem we are facing with the current implementation of auto-sized multiline text fields is the following: 1. If the rectangle of the multiline text field has a reasonable height for a few lines and only a few characters are put in the text field the font will get too big, resulting in strange looking input. If edited aftewards with Adobe Acrobat the font size is adjusted to 12pt (like the DEFAULT_FONT_SIZE). 2. Words are not broken if they are too long to fit in the width of the textbox. The result is that the part not fitting is cut off. The attached patch tries to fix these issues in the following way: 1. Use DEFAULT_FONT_SIZE instead of MAX_FONT_SIZE to calculate the maximum line height for multiline text fields 2. Test if a word is longer than the width if it is the only word present on a line. If yes a similar alorithm to calculate the font size in 1. is used. If there are any problems with the patch, please let me know. -- Mit freundlichen Grüßen Sebastian Fieber Software Entwickler Mentana-Claimsoft GmbH EIN UNTERNEHMEN DER FP-GRUPPE Griesbergstr. 8 · 31162 Bad Salzdetfurth Trebuser Str. 47 · Haus 1 · 15517 Fürstenwalde Tel: +49 (0)5063 277440 · Fax: +49 (0)5063 2774450 E-Mail: [email protected] · De-Mail: [email protected] Web: www.mentana-claimsoft.de · Blog: fp-francotyp.com/blog Service Center De-Mail: 01806/ Mentana (6368262) (0,20 € pro Anruf aus dem deutschen Festnetz, max. 0,60 € pro Anruf aus dem deutschen Mobilfunknetz) Service Center Signaturprodukte: 01806/ Signatur (74462887) (0,20 € pro Anruf aus dem deutschen Festnetz, max. 0,60 € pro Anruf aus dem deutschen Mobilfunknetz) Geschäftsführung Stephan Vanberg, Patricius de Gruyter HRB: 13886 - Amtsgericht Frankfurt/Oder
diff --git parent/pom.xml parent/pom.xml
index 1d84cad91..afd54687b 100644
--- parent/pom.xml
+++ parent/pom.xml
@@ -54,7 +54,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<bouncycastle.version>1.64</bouncycastle.version>
-
+
<!-- PDFBOX-4479 to build on jdk6 on newer Jenkins -->
<jdk.path>${env.JAVA_HOME}</jdk.path>
</properties>
@@ -155,7 +155,7 @@
</profile>
<profile>
<!-- from jdk11 onwards activation and bind are no longer part of the jdk -->
- <!-- must be set to "test" or "provided" in subprojects -->
+ <!-- must be set to "test" or "provided" in subprojects -->
<id>jdk11</id>
<activation>
<jdk>[11,)</jdk>
@@ -225,14 +225,18 @@
<encoding>UTF-8</encoding>
<!-- https://maven.apache.org/plugins/maven-compiler-plugin/examples/compile-using-different-jdk.html -->
<executable>${jdk.path}/bin/javac</executable>
- <fork>true</fork>
- <!-- enable these when getting CompilationFailureException without explanation: -->
- <compilerArgs>
- <arg>-verbose</arg>
- <arg>-J-Xmx1g</arg>
- <arg>-J-XX:PermSize=256m</arg>
- <arg>-J-XX:MaxPermSize=512m</arg>
- </compilerArgs>
+ <!-- <fork>true</fork> -->
+ <!-- <!-\- enable these when getting CompilationFailureException without explanation: -\-> -->
+ <!-- <compilerArgs> -->
+ <!-- <arg>-verbose</arg> -->
+ <!-- <arg>-J-Xmx1g</arg> -->
+ <!-- <arg>-J-XX:PermSize=256m</arg> -->
+ <!-- <arg>-J-XX:MaxPermSize=512m</arg> -->
+ <!-- </compilerArgs> -->
+ <compilerArguments>
+ <verbose/>
+ </compilerArguments>
+ <verbose>true</verbose>
</configuration>
</plugin>
<plugin>
@@ -257,51 +261,51 @@
</configuration>
</plugin>
<plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <configuration>
- <archive>
- <manifest>
- <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
- <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
- </manifest>
- </archive>
- </configuration>
- </plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+ <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
- <execution>
- <id>attach-sources</id>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
</plugin>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>animal-sniffer-maven-plugin</artifactId>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-maven-plugin</artifactId>
<!-- don't update, or it won't work with jdk1.7 -->
- <version>1.17</version>
- <executions>
- <execution>
- <id>check-java-version</id>
- <phase>test</phase>
- <goals>
- <goal>check</goal>
- </goals>
- <configuration>
+ <version>1.17</version>
+ <executions>
+ <execution>
+ <id>check-java-version</id>
+ <phase>test</phase>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ <configuration>
<skip>${skipTests}</skip>
- <signature>
- <groupId>org.codehaus.mojo.signature</groupId>
- <artifactId>java16</artifactId>
- <version>1.0</version>
- </signature>
- </configuration>
- </execution>
- </executions>
+ <signature>
+ <groupId>org.codehaus.mojo.signature</groupId>
+ <artifactId>java16</artifactId>
+ <version>1.0</version>
+ </signature>
+ </configuration>
+ </execution>
+ </executions>
</plugin>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
diff --git pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
index 29160d83f..1d6957055 100644
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
@@ -798,7 +798,7 @@ class AppearanceGeneratorHelper
{
float width = contentRect.getWidth() - contentRect.getLowerLeftX();
float fs = MINIMUM_FONT_SIZE;
- while (fs <= MAXIMUM_FONT_SIZE)
+ while (fs <= DEFAULT_FONT_SIZE)
{
// determine the number of lines needed for this font and contentRect
int numLines = 0;
@@ -818,7 +818,7 @@ class AppearanceGeneratorHelper
}
fs++;
}
- return Math.min(fs, MAXIMUM_FONT_SIZE);
+ return Math.min(fs, DEFAULT_FONT_SIZE);
}
// Acrobat defaults to 12 for multiline text with size 0
diff --git pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java
index af66d6282..2bf706ada 100644
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java
@@ -32,21 +32,21 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
* A block of text can contain multiple paragraphs which will
* be treated individually within the block placement.
* </p>
- *
+ *
*/
class PlainText
{
private static final float FONTSCALE = 1000f;
-
+
private final List<Paragraph> paragraphs;
-
+
/**
* Construct the text block from a single value.
- *
+ *
* Constructs the text block from a single value splitting
- * into individual {@link Paragraph} when a new line character is
+ * into individual {@link Paragraph} when a new line character is
* encountered.
- *
+ *
* @param textValue the text block string.
*/
PlainText(String textValue)
@@ -55,21 +55,21 @@ class PlainText
paragraphs = new ArrayList<Paragraph>();
for (String part : parts)
{
- // Acrobat prints a space for an empty paragraph
- if (part.length() == 0)
- {
- part = " ";
- }
+ // Acrobat prints a space for an empty paragraph
+ if (part.length() == 0)
+ {
+ part = " ";
+ }
paragraphs.add(new Paragraph(part));
}
}
-
+
/**
* Construct the text block from a list of values.
- *
+ *
* Constructs the text block from a list of values treating each
* entry as an individual {@link Paragraph}.
- *
+ *
* @param listValue the text block string.
*/
PlainText(List<String> listValue)
@@ -80,23 +80,23 @@ class PlainText
paragraphs.add(new Paragraph(part));
}
}
-
+
/**
* Get the list of paragraphs.
- *
+ *
* @return the paragraphs.
*/
List<Paragraph> getParagraphs()
{
return paragraphs;
}
-
+
/**
* Attribute keys and attribute values used for text handling.
- *
+ *
* This is similar to {@link java.awt.font.TextAttribute} but
* handled individually as to avoid a dependency on awt.
- *
+ *
*/
static class TextAttribute extends Attribute
{
@@ -109,12 +109,12 @@ class PlainText
* Attribute width of the text.
*/
public static final Attribute WIDTH = new TextAttribute("width");
-
+
protected TextAttribute(String name)
{
super(name);
}
-
+
}
@@ -124,30 +124,30 @@ class PlainText
* A block of text can contain multiple paragraphs which will
* be treated individually within the block placement.
* </p>
- *
+ *
*/
static class Paragraph
{
private final String textContent;
-
+
Paragraph(String text)
{
textContent = text;
}
-
+
/**
* Get the paragraph text.
- *
+ *
* @return the text.
*/
String getText()
{
return textContent;
}
-
+
/**
* Break the paragraph into individual lines.
- *
+ *
* @param font the font used for rendering the text.
* @param fontSize the fontSize used for rendering the text.
* @param width the width of the box holding the content.
@@ -158,13 +158,13 @@ class PlainText
{
BreakIterator iterator = BreakIterator.getLineInstance();
iterator.setText(textContent);
-
+
final float scale = fontSize/FONTSCALE;
-
+
int start = iterator.first();
int end = iterator.next();
float lineWidth = 0;
-
+
List<Line> textLines = new ArrayList<Line>();
Line textLine = new Line();
@@ -172,7 +172,9 @@ class PlainText
{
String word = textContent.substring(start,end);
float wordWidth = font.getStringWidth(word) * scale;
-
+ boolean wordNeedsSplit = false;
+ int splitOffset = end - start;
+
lineWidth = lineWidth + wordWidth;
// check if the last word would fit without the whitespace ending it
@@ -181,22 +183,45 @@ class PlainText
float whitespaceWidth = font.getStringWidth(word.substring(word.length()-1)) * scale;
lineWidth = lineWidth - whitespaceWidth;
}
-
- if (lineWidth >= width)
+
+ if (lineWidth >= width && textLine.getWords().size() > 0)
{
textLine.setWidth(textLine.calculateWidth(font, fontSize));
textLines.add(textLine);
textLine = new Line();
lineWidth = font.getStringWidth(word) * scale;
}
-
+
+ if (wordWidth > width && textLine.getWords().size() == 0) {
+ // single word does not fit into width
+ wordNeedsSplit = true;
+ while (true)
+ {
+ splitOffset--;
+ String substring = word.trim().substring(0, splitOffset);
+ float substringWidth = font.getStringWidth(substring) * scale;
+ if (substringWidth < width) {
+ word = substring;
+ wordWidth = font.getStringWidth(word) * scale;
+ lineWidth = wordWidth;
+ break;
+ }
+ }
+ }
+
+
AttributedString as = new AttributedString(word);
as.addAttribute(TextAttribute.WIDTH, wordWidth);
Word wordInstance = new Word(word);
wordInstance.setAttributes(as);
textLine.addWord(wordInstance);
- start = end;
- end = iterator.next();
+
+ if (wordNeedsSplit) {
+ start = start + splitOffset;
+ } else {
+ start = end;
+ end = iterator.next();
+ }
}
textLine.setWidth(textLine.calculateWidth(font, fontSize));
textLines.add(textLine);
@@ -216,19 +241,19 @@ class PlainText
{
return lineWidth;
}
-
+
void setWidth(float width)
{
lineWidth = width;
}
-
+
float calculateWidth(PDFont font, float fontSize) throws IOException
{
final float scale = fontSize/FONTSCALE;
float calculatedWidth = 0f;
for (Word word : words)
{
- calculatedWidth = calculatedWidth +
+ calculatedWidth = calculatedWidth +
(Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH);
String text = word.getText();
if (words.indexOf(word) == words.size() -1 && Character.isWhitespace(text.charAt(text.length()-1)))
@@ -244,7 +269,7 @@ class PlainText
{
return words;
}
-
+
float getInterWordSpacing(float width)
{
return (width - lineWidth)/(words.size()-1);
@@ -255,10 +280,10 @@ class PlainText
words.add(word);
}
}
-
+
/**
* An individual word.
- *
+ *
* A word is defined as a string which must be kept
* on the same line.
*/
@@ -266,22 +291,22 @@ class PlainText
{
private AttributedString attributedString;
private final String textContent;
-
+
Word(String text)
{
textContent = text;
}
-
+
String getText()
{
return textContent;
}
-
+
AttributedString getAttributes()
{
return attributedString;
}
-
+
void setAttributes(AttributedString as)
{
this.attributedString = as;
smime.p7s
Description: S/MIME cryptographic signature
