You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ms...@apache.org on 2020/12/11 18:20:16 UTC
svn commit: r1884334 - in /pdfbox/branches/2.0/pdfbox/src:
main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormGenerateAppearancesTest.java
Author: msahyoun
Date: Fri Dec 11 18:20:16 2020
New Revision: 1884334
URL: http://svn.apache.org/viewvc?rev=1884334&view=rev
Log:
PDFBOX-5041: calculate Capheight/Descent for missing font descriptor; avoid NPE
Added:
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormGenerateAppearancesTest.java
Modified:
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
Modified: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java?rev=1884334&r1=1884333&r2=1884334&view=diff
==============================================================================
--- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java (original)
+++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java Fri Dec 11 18:20:16 2020
@@ -17,6 +17,7 @@
package org.apache.pdfbox.pdmodel.interactive.form;
import java.awt.geom.AffineTransform;
+import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -25,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.apache.fontbox.util.BoundingBox;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
@@ -34,6 +36,10 @@ import org.apache.pdfbox.pdmodel.PDResou
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
+import org.apache.pdfbox.pdmodel.font.PDType3CharProc;
+import org.apache.pdfbox.pdmodel.font.PDType3Font;
+import org.apache.pdfbox.pdmodel.font.PDVectorFont;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
@@ -51,32 +57,33 @@ import org.apache.pdfbox.util.Matrix;
* @author Stephan Gerhard
* @author Ben Litchfield
*/
-class AppearanceGeneratorHelper
-{
+class AppearanceGeneratorHelper {
private static final Log LOG = LogFactory.getLog(AppearanceGeneratorHelper.class);
private static final Operator BMC = Operator.getOperator("BMC");
private static final Operator EMC = Operator.getOperator("EMC");
-
+
private final PDVariableText field;
-
+
private PDDefaultAppearanceString defaultAppearance;
private String value;
-
+
/**
* The highlight color
*
- * The color setting is used by Adobe to display the highlight box for selected entries in a list box.
+ * The color setting is used by Adobe to display the highlight box for selected
+ * entries in a list box.
*
- * Regardless of other settings in an existing appearance stream Adobe will always use this value.
+ * Regardless of other settings in an existing appearance stream Adobe will
+ * always use this value.
*/
- private static final float[] HIGHLIGHT_COLOR = {153/255f, 193/255f, 215/255f};
-
+ private static final float[] HIGHLIGHT_COLOR = { 153 / 255f, 193 / 255f, 215 / 255f };
+
/**
* The scaling factor for font units to PDF units
*/
private static final int FONTSCALE = 1000;
-
+
/**
* The default font size used for multiline text
*/
@@ -87,197 +94,168 @@ class AppearanceGeneratorHelper
*/
private static final float MINIMUM_FONT_SIZE = 4;
private static final float MAXIMUM_FONT_SIZE = 300;
-
+
/**
* The default padding applied by Acrobat to the fields bbox.
*/
private static final float DEFAULT_PADDING = 0.5f;
-
+
/**
* Constructs a COSAppearance from the given field.
*
* @param field the field which you wish to control the appearance of
- * @throws IOException
+ * @throws IOException
*/
- AppearanceGeneratorHelper(PDVariableText field) throws IOException
- {
+ AppearanceGeneratorHelper(PDVariableText field) throws IOException {
this.field = field;
validateAndEnsureAcroFormResources();
-
- try
- {
+
+ try {
this.defaultAppearance = field.getDefaultAppearanceString();
- }
- catch (IOException ex)
- {
- throw new IOException("Could not process default appearance string '" +
- field.getDefaultAppearance() + "' for field '" +
- field.getFullyQualifiedName() + "'", ex);
+ } catch (IOException ex) {
+ throw new IOException("Could not process default appearance string '" + field.getDefaultAppearance()
+ + "' for field '" + field.getFullyQualifiedName() + "'", ex);
}
}
-
+
/*
* Adobe Reader/Acrobat are adding resources which are at the field/widget level
- * to the AcroForm level.
+ * to the AcroForm level.
*/
- private void validateAndEnsureAcroFormResources()
- {
- // add font resources which might be available at the field
+ private void validateAndEnsureAcroFormResources() {
+ // add font resources which might be available at the field
// level but are not at the AcroForm level to the AcroForm
- // to match Adobe Reader/Acrobat behavior
- if (field.getAcroForm().getDefaultResources() == null)
- {
+ // to match Adobe Reader/Acrobat behavior
+ if (field.getAcroForm().getDefaultResources() == null) {
return;
}
-
+
PDResources acroFormResources = field.getAcroForm().getDefaultResources();
-
- for (PDAnnotationWidget widget : field.getWidgets())
- {
- if (widget.getNormalAppearanceStream() != null && widget.getNormalAppearanceStream().getResources() != null)
- {
+
+ for (PDAnnotationWidget widget : field.getWidgets()) {
+ if (widget.getNormalAppearanceStream() != null
+ && widget.getNormalAppearanceStream().getResources() != null) {
PDResources widgetResources = widget.getNormalAppearanceStream().getResources();
- for (COSName fontResourceName : widgetResources.getFontNames())
- {
- try
- {
- if (acroFormResources.getFont(fontResourceName) == null)
- {
+ for (COSName fontResourceName : widgetResources.getFontNames()) {
+ try {
+ if (acroFormResources.getFont(fontResourceName) == null) {
LOG.debug("Adding font resource " + fontResourceName + " from widget to AcroForm");
acroFormResources.put(fontResourceName, widgetResources.getFont(fontResourceName));
}
- }
- catch (IOException e)
- {
+ } catch (IOException e) {
LOG.warn("Unable to match field level font with AcroForm font");
}
}
}
}
}
-
+
/**
* This is the public method for setting the appearance stream.
*
* @param apValue the String value which the appearance should represent
* @throws IOException If there is an error creating the stream.
*/
- public void setAppearanceValue(String apValue) throws IOException
- {
+ public void setAppearanceValue(String apValue) throws IOException {
value = getFormattedValue(apValue);
-
+
// Treat multiline field values in single lines as single lime values.
// This is in line with how Adobe Reader behaves when enetring text
// interactively but NOT how it behaves when the field value has been
// set programmatically and Reader is forced to generate the appearance
// using PDAcroForm.setNeedAppearances
// see PDFBOX-3911
- if (field instanceof PDTextField && !((PDTextField) field).isMultiline())
- {
+ if (field instanceof PDTextField && !((PDTextField) field).isMultiline()) {
value = apValue.replaceAll("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]", " ");
}
- for (PDAnnotationWidget widget : field.getWidgets())
- {
- // some fields have the /Da at the widget level if the
+ for (PDAnnotationWidget widget : field.getWidgets()) {
+ // some fields have the /Da at the widget level if the
// widgets differ in layout.
PDDefaultAppearanceString acroFormAppearance = defaultAppearance;
-
- if (widget.getCOSObject().getDictionaryObject(COSName.DA) != null)
- {
+
+ if (widget.getCOSObject().getDictionaryObject(COSName.DA) != null) {
defaultAppearance = getWidgetDefaultAppearanceString(widget);
}
PDRectangle rect = widget.getRectangle();
- if (rect == null)
- {
+ if (rect == null) {
widget.getCOSObject().removeItem(COSName.AP);
- LOG.warn("widget of field " + field.getFullyQualifiedName() + " has no rectangle, no appearance stream created");
+ LOG.warn("widget of field " + field.getFullyQualifiedName()
+ + " has no rectangle, no appearance stream created");
continue;
}
PDAppearanceDictionary appearanceDict = widget.getAppearance();
- if (appearanceDict == null)
- {
+ if (appearanceDict == null) {
appearanceDict = new PDAppearanceDictionary();
widget.setAppearance(appearanceDict);
}
PDAppearanceEntry appearance = appearanceDict.getNormalAppearance();
// TODO support appearances other than "normal"
-
+
PDAppearanceStream appearanceStream;
- if (isValidAppearanceStream(appearance))
- {
+ if (isValidAppearanceStream(appearance)) {
appearanceStream = appearance.getAppearanceStream();
- }
- else
- {
+ } else {
appearanceStream = prepareNormalAppearanceStream(widget);
appearanceDict.setNormalAppearance(appearanceStream);
// TODO support appearances other than "normal"
}
-
+
/*
- * Adobe Acrobat always recreates the complete appearance stream if there is an appearance characteristics
- * entry (the widget dictionaries MK entry). In addition if there is no content yet also create the appearance
- * stream from the entries.
+ * Adobe Acrobat always recreates the complete appearance stream if there is an
+ * appearance characteristics entry (the widget dictionaries MK entry). In
+ * addition if there is no content yet also create the appearance stream from
+ * the entries.
*
*/
- if (widget.getAppearanceCharacteristics() != null || appearanceStream.getContentStream().getLength() == 0)
- {
+ if (widget.getAppearanceCharacteristics() != null || appearanceStream.getContentStream().getLength() == 0) {
initializeAppearanceContent(widget, appearanceStream);
}
-
+
setAppearanceContent(widget, appearanceStream);
-
+
// restore the field level appearance
- defaultAppearance = acroFormAppearance;
+ defaultAppearance = acroFormAppearance;
}
}
- private String getFormattedValue(String apValue)
- {
- // format the field value for the appearance if there is scripting support and the field
+ private String getFormattedValue(String apValue) {
+ // format the field value for the appearance if there is scripting support and
+ // the field
// has a format event
PDFormFieldAdditionalActions actions = field.getActions();
- if (actions != null && actions.getF() != null)
- {
- if (field.getAcroForm().getScriptingHandler() != null)
- {
+ if (actions != null && actions.getF() != null) {
+ if (field.getAcroForm().getScriptingHandler() != null) {
ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler();
return scriptingHandler.format((PDActionJavaScript) field.getActions().getF(), apValue);
- }
- else
- {
- LOG.info("Field contains a formatting action but no SriptingHandler has been supplied - formatted value might be incorrect");
+ } else {
+ LOG.info(
+ "Field contains a formatting action but no SriptingHandler has been supplied - formatted value might be incorrect");
return apValue;
}
}
return apValue;
}
- private static boolean isValidAppearanceStream(PDAppearanceEntry appearance)
- {
- if (appearance == null)
- {
+ private static boolean isValidAppearanceStream(PDAppearanceEntry appearance) {
+ if (appearance == null) {
return false;
}
- if (!appearance.isStream())
- {
+ if (!appearance.isStream()) {
return false;
}
PDRectangle bbox = appearance.getAppearanceStream().getBBox();
- if (bbox == null)
- {
+ if (bbox == null) {
return false;
}
return Math.abs(bbox.getWidth()) > 0 && Math.abs(bbox.getHeight()) > 0;
}
- private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widget)
- {
+ private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widget) {
PDAppearanceStream appearanceStream = new PDAppearanceStream(field.getAcroForm().getDocument());
// Calculate the entries for the bounding box and the transformation matrix
@@ -291,8 +269,7 @@ class AppearanceGeneratorHelper
appearanceStream.setBBox(bbox);
AffineTransform at = calculateMatrix(bbox, rotation);
- if (!at.isIdentity())
- {
+ if (!at.isIdentity()) {
appearanceStream.setMatrix(at);
}
appearanceStream.setFormType(1);
@@ -300,90 +277,80 @@ class AppearanceGeneratorHelper
return appearanceStream;
}
- private PDDefaultAppearanceString getWidgetDefaultAppearanceString(PDAnnotationWidget widget) throws IOException
- {
+ private PDDefaultAppearanceString getWidgetDefaultAppearanceString(PDAnnotationWidget widget) throws IOException {
COSString da = (COSString) widget.getCOSObject().getDictionaryObject(COSName.DA);
PDResources dr = field.getAcroForm().getDefaultResources();
return new PDDefaultAppearanceString(da, dr);
}
- private int resolveRotation(PDAnnotationWidget widget)
- {
- PDAppearanceCharacteristicsDictionary characteristicsDictionary = widget.getAppearanceCharacteristics();
- if (characteristicsDictionary != null)
- {
+ private int resolveRotation(PDAnnotationWidget widget) {
+ PDAppearanceCharacteristicsDictionary characteristicsDictionary = widget.getAppearanceCharacteristics();
+ if (characteristicsDictionary != null) {
// 0 is the default value if the R key doesn't exist
return characteristicsDictionary.getRotation();
}
return 0;
}
-
/**
* Initialize the content of the appearance stream.
*
- * Get settings like border style, border width and colors to be used to draw a rectangle and background color
- * around the widget
+ * Get settings like border style, border width and colors to be used to draw a
+ * rectangle and background color around the widget
*
- * @param widget the field widget
+ * @param widget the field widget
* @param appearanceStream the appearance stream to be used
* @throws IOException in case we can't write to the appearance stream
*/
- private void initializeAppearanceContent(PDAnnotationWidget widget, PDAppearanceStream appearanceStream) throws IOException
- {
+ private void initializeAppearanceContent(PDAnnotationWidget widget, PDAppearanceStream appearanceStream)
+ throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
- PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(),
- appearanceStream, output);
+ PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(), appearanceStream,
+ output);
PDAppearanceCharacteristicsDictionary appearanceCharacteristics = widget.getAppearanceCharacteristics();
-
+
// TODO: support more entries like patterns, etc.
- if (appearanceCharacteristics != null)
- {
+ if (appearanceCharacteristics != null) {
PDColor backgroundColour = appearanceCharacteristics.getBackground();
- if (backgroundColour != null)
- {
+ if (backgroundColour != null) {
contents.setNonStrokingColor(backgroundColour);
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
- contents.addRect(bbox.getLowerLeftX(),bbox.getLowerLeftY(),bbox.getWidth(), bbox.getHeight());
+ contents.addRect(bbox.getLowerLeftX(), bbox.getLowerLeftY(), bbox.getWidth(), bbox.getHeight());
contents.fill();
}
float lineWidth = 0f;
PDColor borderColour = appearanceCharacteristics.getBorderColour();
- if (borderColour != null)
- {
+ if (borderColour != null) {
contents.setStrokingColor(borderColour);
lineWidth = 1f;
}
PDBorderStyleDictionary borderStyle = widget.getBorderStyle();
- if (borderStyle != null && borderStyle.getWidth() > 0)
- {
+ if (borderStyle != null && borderStyle.getWidth() > 0) {
lineWidth = borderStyle.getWidth();
}
- if (lineWidth > 0 && borderColour != null)
- {
- if (lineWidth != 1)
- {
+ if (lineWidth > 0 && borderColour != null) {
+ if (lineWidth != 1) {
contents.setLineWidth(lineWidth);
}
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
- PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth/2));
- contents.addRect(clipRect.getLowerLeftX(),clipRect.getLowerLeftY(),clipRect.getWidth(), clipRect.getHeight());
+ PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth / 2));
+ contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), clipRect.getWidth(),
+ clipRect.getHeight());
contents.closeAndStroke();
}
}
-
+
contents.close();
output.close();
writeToStream(output.toByteArray(), appearanceStream);
}
-
+
/**
* Parses an appearance stream into tokens.
*/
- private List<Object> tokenize(PDAppearanceStream appearanceStream) throws IOException
- {
+ private List<Object> tokenize(PDAppearanceStream appearanceStream) throws IOException {
PDFStreamParser parser = new PDFStreamParser(appearanceStream);
parser.parse();
return parser.getTokens();
@@ -392,43 +359,36 @@ class AppearanceGeneratorHelper
/**
* Constructs and sets new contents for given appearance stream.
*/
- private void setAppearanceContent(PDAnnotationWidget widget,
- PDAppearanceStream appearanceStream) throws IOException
- {
+ private void setAppearanceContent(PDAnnotationWidget widget, PDAppearanceStream appearanceStream)
+ throws IOException {
// first copy any needed resources from the documentâs DR dictionary into
// the streamâs Resources dictionary
defaultAppearance.copyNeededResourcesTo(appearanceStream);
-
+
// then replace the existing contents of the appearance stream from /Tx BMC
// to the matching EMC
ByteArrayOutputStream output = new ByteArrayOutputStream();
ContentStreamWriter writer = new ContentStreamWriter(output);
-
+
List<Object> tokens = tokenize(appearanceStream);
int bmcIndex = tokens.indexOf(BMC);
- if (bmcIndex == -1)
- {
+ if (bmcIndex == -1) {
// append to existing stream
writer.writeTokens(tokens);
writer.writeTokens(COSName.TX, BMC);
- }
- else
- {
+ } else {
// prepend content before BMC
writer.writeTokens(tokens.subList(0, bmcIndex + 1));
}
-
+
// insert field contents
insertGeneratedAppearance(widget, appearanceStream, output);
-
+
int emcIndex = tokens.indexOf(EMC);
- if (emcIndex == -1)
- {
+ if (emcIndex == -1) {
// append EMC
writer.writeTokens(EMC);
- }
- else
- {
+ } else {
// append contents after EMC
writer.writeTokens(tokens.subList(emcIndex, tokens.size()));
}
@@ -436,68 +396,61 @@ class AppearanceGeneratorHelper
output.close();
writeToStream(output.toByteArray(), appearanceStream);
}
-
+
/**
- * Generate and insert text content and clipping around it.
+ * Generate and insert text content and clipping around it.
*/
- private void insertGeneratedAppearance(PDAnnotationWidget widget,
- PDAppearanceStream appearanceStream,
- OutputStream output) throws IOException
- {
- PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(),
- appearanceStream, output);
+ private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceStream appearanceStream,
+ OutputStream output) throws IOException {
+ PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(), appearanceStream,
+ output);
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
- // Acrobat calculates the left and right padding dependent on the offset of the border edge
+ // Acrobat calculates the left and right padding dependent on the offset of the
+ // border edge
// This calculation works for forms having been generated by Acrobat.
- // The minimum distance is always 1f even if there is no rectangle being drawn around.
+ // The minimum distance is always 1f even if there is no rectangle being drawn
+ // around.
float borderWidth = 0;
- if (widget.getBorderStyle() != null)
- {
+ if (widget.getBorderStyle() != null) {
borderWidth = widget.getBorderStyle().getWidth();
}
PDRectangle clipRect = applyPadding(bbox, Math.max(1f, borderWidth));
PDRectangle contentRect = applyPadding(clipRect, Math.max(1f, borderWidth));
-
+
contents.saveGraphicsState();
-
+
// Acrobat always adds a clipping path
- contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(),
- clipRect.getWidth(), clipRect.getHeight());
+ contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), clipRect.getWidth(), clipRect.getHeight());
contents.clip();
-
+
// get the font
PDFont font = defaultAppearance.getFont();
- if (font == null)
- {
+ if (font == null) {
throw new IllegalArgumentException("font is null, check whether /DA entry is incomplete or incorrect");
}
- if (font.getName().contains("+"))
- {
- LOG.warn("Font '" + defaultAppearance.getFontName().getName() +
- "' of field '" + field.getFullyQualifiedName() +
- "' contains subsetted font '" + font.getName() + "'");
- LOG.warn("This may bring trouble with PDField.setValue(), PDAcroForm.flatten() or " +
- "PDAcroForm.refreshAppearances()");
+ if (font.getName().contains("+")) {
+ LOG.warn("Font '" + defaultAppearance.getFontName().getName() + "' of field '"
+ + field.getFullyQualifiedName() + "' contains subsetted font '" + font.getName() + "'");
+ LOG.warn("This may bring trouble with PDField.setValue(), PDAcroForm.flatten() or "
+ + "PDAcroForm.refreshAppearances()");
LOG.warn("You should replace this font with a non-subsetted font:");
LOG.warn("PDFont font = PDType0Font.load(doc, new FileInputStream(fontfile), false);");
- LOG.warn("acroForm.getDefaultResources().put(COSName.getPDFName(\"" +
- defaultAppearance.getFontName().getName() + "\", font);");
+ LOG.warn("acroForm.getDefaultResources().put(COSName.getPDFName(\""
+ + defaultAppearance.getFontName().getName() + "\", font);");
}
-
+
// calculate the fontSize (because 0 = autosize)
float fontSize = defaultAppearance.getFontSize();
-
- if (fontSize == 0)
- {
- fontSize = calculateFontSize(font, contentRect);
+
+ if (fontSize == 0) {
+ fontSize = calculateFontSize(font, contentRect);
}
-
+
// for a listbox generate the highlight rectangle for the selected
// options
- if (field instanceof PDListBox)
- {
+ if (field instanceof PDListBox) {
insertGeneratedListboxSelectionHighlight(contents, appearanceStream, font, fontSize);
}
@@ -513,97 +466,89 @@ class AppearanceGeneratorHelper
// calculate font metrics at font size
float fontScaleY = fontSize / FONTSCALE;
float fontBoundingBoxAtSize = font.getBoundingBox().getHeight() * fontScaleY;
- float fontCapAtSize = font.getFontDescriptor().getCapHeight() * fontScaleY;
- float fontDescentAtSize = font.getFontDescriptor().getDescent() * fontScaleY;
-
- if (field instanceof PDTextField && ((PDTextField) field).isMultiline())
- {
- y = contentRect.getUpperRightY() - fontBoundingBoxAtSize;
+
+ float fontCapAtSize = 0;
+ float fontDescentAtSize = 0;
+
+ if (font.getFontDescriptor() != null) {
+ fontCapAtSize = font.getFontDescriptor().getCapHeight() * fontScaleY;
+ fontDescentAtSize = font.getFontDescriptor().getDescent() * fontScaleY;
+ } else {
+ float fontCapHeight = resolveCapHeight(font);
+ float fontDescent = resolveDescent(font);
+ LOG.debug("missing font descriptor - resolved Cap/Descent to " + fontCapHeight + "/" + fontDescent);
+ fontCapAtSize = fontCapHeight * fontScaleY;
+ fontDescentAtSize = fontDescent * fontScaleY;
}
- else
- {
- // Adobe shows the text 'shiftet up' in case the caps don't fit into the clipping area
- if (fontCapAtSize > clipRect.getHeight())
- {
+
+ if (field instanceof PDTextField && ((PDTextField) field).isMultiline()) {
+ y = contentRect.getUpperRightY() - fontBoundingBoxAtSize;
+ } else {
+ // Adobe shows the text 'shiftet up' in case the caps don't fit into the
+ // clipping area
+ if (fontCapAtSize > clipRect.getHeight()) {
y = clipRect.getLowerLeftY() + -fontDescentAtSize;
- }
- else
- {
+ } else {
// calculate the position based on the content rectangle
y = clipRect.getLowerLeftY() + (clipRect.getHeight() - fontCapAtSize) / 2;
-
+
// check to ensure that ascents and descents fit
if (y - clipRect.getLowerLeftY() < -fontDescentAtSize) {
-
+
float fontDescentBased = -fontDescentAtSize + contentRect.getLowerLeftY();
float fontCapBased = contentRect.getHeight() - contentRect.getLowerLeftY() - fontCapAtSize;
-
+
y = Math.min(fontDescentBased, Math.max(y, fontCapBased));
}
}
}
-
+
// show the text
float x = contentRect.getLowerLeftX();
-
+
// special handling for comb boxes as these are like table cells with individual
// chars
- if (shallComb())
- {
+ if (shallComb()) {
insertGeneratedCombAppearance(contents, appearanceStream, font, fontSize);
- }
- else if (field instanceof PDListBox)
- {
+ } else if (field instanceof PDListBox) {
insertGeneratedListboxAppearance(contents, appearanceStream, contentRect, font, fontSize);
- }
- else
- {
+ } else {
PlainText textContent = new PlainText(value);
AppearanceStyle appearanceStyle = new AppearanceStyle();
appearanceStyle.setFont(font);
appearanceStyle.setFontSize(fontSize);
-
+
// Adobe Acrobat uses the font's bounding box for the leading between the lines
appearanceStyle.setLeading(font.getBoundingBox().getHeight() * fontScaleY);
-
- PlainTextFormatter formatter = new PlainTextFormatter
- .Builder(contents)
- .style(appearanceStyle)
- .text(textContent)
- .width(contentRect.getWidth())
- .wrapLines(isMultiLine())
- .initialOffset(x, y)
- .textAlign(getTextAlign(widget))
- .build();
+
+ PlainTextFormatter formatter = new PlainTextFormatter.Builder(contents).style(appearanceStyle)
+ .text(textContent).width(contentRect.getWidth()).wrapLines(isMultiLine()).initialOffset(x, y)
+ .textAlign(getTextAlign(widget)).build();
formatter.format();
}
-
+
contents.endText();
contents.restoreGraphicsState();
contents.close();
}
/*
- * PDFBox handles a widget with a joined in field dictionary and without
- * an individual name as a widget only. As a result - as a widget can't have a
+ * PDFBox handles a widget with a joined in field dictionary and without an
+ * individual name as a widget only. As a result - as a widget can't have a
* quadding /Q entry we need to do a low level access to the dictionary and
* otherwise get the quadding from the field.
*/
- private int getTextAlign(PDAnnotationWidget widget)
- {
+ private int getTextAlign(PDAnnotationWidget widget) {
// Use quadding value from joined field/widget if set, else use from field.
return widget.getCOSObject().getInt(COSName.Q, field.getQ());
}
- private AffineTransform calculateMatrix(PDRectangle bbox, int rotation)
- {
- if (rotation == 0)
- {
+ private AffineTransform calculateMatrix(PDRectangle bbox, int rotation) {
+ if (rotation == 0) {
return new AffineTransform();
}
float tx = 0, ty = 0;
- switch (rotation)
- {
+ switch (rotation) {
case 90:
tx = bbox.getUpperRightY();
break;
@@ -622,142 +567,125 @@ class AppearanceGeneratorHelper
}
-
- private boolean isMultiLine()
- {
+ private boolean isMultiLine() {
return field instanceof PDTextField && ((PDTextField) field).isMultiline();
}
-
+
/**
* Determine if the appearance shall provide a comb output.
*
* <p>
* May be set only if the MaxLen entry is present in the text field dictionary
- * and if the Multiline, Password, and FileSelect flags are clear.
- * If set, the field shall be automatically divided into as many equally spaced positions,
+ * and if the Multiline, Password, and FileSelect flags are clear. If set, the
+ * field shall be automatically divided into as many equally spaced positions,
* or combs, as the value of MaxLen, and the text is laid out into those combs.
* </p>
*
* @return the comb state
*/
- private boolean shallComb()
- {
- return field instanceof PDTextField &&
- ((PDTextField) field).isComb() &&
- !((PDTextField) field).isMultiline() &&
- !((PDTextField) field).isPassword() &&
- !((PDTextField) field).isFileSelect();
+ private boolean shallComb() {
+ return field instanceof PDTextField && ((PDTextField) field).isComb() && !((PDTextField) field).isMultiline()
+ && !((PDTextField) field).isPassword() && !((PDTextField) field).isFileSelect();
}
-
+
/**
* Generate the appearance for comb fields.
*
- * @param contents the content stream to write to
+ * @param contents the content stream to write to
* @param appearanceStream the appearance stream used
- * @param font the font to be used
- * @param fontSize the font size to be used
+ * @param font the font to be used
+ * @param fontSize the font size to be used
* @throws IOException
*/
private void insertGeneratedCombAppearance(PDPageContentStream contents, PDAppearanceStream appearanceStream,
- PDFont font, float fontSize) throws IOException
- {
-
- // TODO: Currently the quadding is not taken into account
- // so the comb is always filled from left to right.
-
+ PDFont font, float fontSize) throws IOException {
+
+ // TODO: Currently the quadding is not taken into account
+ // so the comb is always filled from left to right.
+
int maxLen = ((PDTextField) field).getMaxLen();
int numChars = Math.min(value.length(), maxLen);
-
+
PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1);
-
+
float combWidth = appearanceStream.getBBox().getWidth() / maxLen;
float ascentAtFontSize = font.getFontDescriptor().getAscent() / FONTSCALE * fontSize;
- float baselineOffset = paddingEdge.getLowerLeftY() +
- (appearanceStream.getBBox().getHeight() - ascentAtFontSize)/2;
-
+ float baselineOffset = paddingEdge.getLowerLeftY()
+ + (appearanceStream.getBBox().getHeight() - ascentAtFontSize) / 2;
+
float prevCharWidth = 0f;
-
+
float xOffset = combWidth / 2;
- for (int i = 0; i < numChars; i++)
- {
- String combString = value.substring(i, i+1);
- float currCharWidth = font.getStringWidth(combString) / FONTSCALE * fontSize/2;
-
- xOffset = xOffset + prevCharWidth/2 - currCharWidth/2;
-
+ for (int i = 0; i < numChars; i++) {
+ String combString = value.substring(i, i + 1);
+ float currCharWidth = font.getStringWidth(combString) / FONTSCALE * fontSize / 2;
+
+ xOffset = xOffset + prevCharWidth / 2 - currCharWidth / 2;
+
contents.newLineAtOffset(xOffset, baselineOffset);
contents.showText(combString);
-
+
baselineOffset = 0;
prevCharWidth = currCharWidth;
xOffset = combWidth;
}
}
-
- private void insertGeneratedListboxSelectionHighlight(PDPageContentStream contents, PDAppearanceStream appearanceStream,
- PDFont font, float fontSize) throws IOException
- {
+
+ private void insertGeneratedListboxSelectionHighlight(PDPageContentStream contents,
+ PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException {
List<Integer> indexEntries = ((PDListBox) field).getSelectedOptionsIndex();
List<String> values = ((PDListBox) field).getValue();
List<String> options = ((PDListBox) field).getOptionsExportValues();
-
- if (!values.isEmpty() && !options.isEmpty() && indexEntries.isEmpty())
- {
+
+ if (!values.isEmpty() && !options.isEmpty() && indexEntries.isEmpty()) {
// create indexEntries from options
indexEntries = new ArrayList<Integer>();
- for (String v : values)
- {
+ for (String v : values) {
indexEntries.add(options.indexOf(v));
}
}
- // The first entry which shall be presented might be adjusted by the optional TI key
- // If this entry is present the first entry to be displayed is the keys value otherwise
+ // The first entry which shall be presented might be adjusted by the optional TI
+ // key
+ // If this entry is present the first entry to be displayed is the keys value
+ // otherwise
// display starts with the first entry in Opt.
int topIndex = ((PDListBox) field).getTopIndex();
-
- float highlightBoxHeight = font.getBoundingBox().getHeight() * fontSize / FONTSCALE;
- // the padding area
+ float highlightBoxHeight = font.getBoundingBox().getHeight() * fontSize / FONTSCALE;
+
+ // the padding area
PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1);
- for (int selectedIndex : indexEntries)
- {
+ for (int selectedIndex : indexEntries) {
contents.setNonStrokingColor(HIGHLIGHT_COLOR[0], HIGHLIGHT_COLOR[1], HIGHLIGHT_COLOR[2]);
contents.addRect(paddingEdge.getLowerLeftX(),
paddingEdge.getUpperRightY() - highlightBoxHeight * (selectedIndex - topIndex + 1) + 2,
- paddingEdge.getWidth(),
- highlightBoxHeight);
+ paddingEdge.getWidth(), highlightBoxHeight);
contents.fill();
}
contents.setNonStrokingColor(0f);
}
-
-
+
private void insertGeneratedListboxAppearance(PDPageContentStream contents, PDAppearanceStream appearanceStream,
- PDRectangle contentRect, PDFont font, float fontSize) throws IOException
- {
+ PDRectangle contentRect, PDFont font, float fontSize) throws IOException {
contents.setNonStrokingColor(0f);
-
+
int q = field.getQ();
- if (q == PDVariableText.QUADDING_CENTERED || q == PDVariableText.QUADDING_RIGHT)
- {
+ if (q == PDVariableText.QUADDING_CENTERED || q == PDVariableText.QUADDING_RIGHT) {
float fieldWidth = appearanceStream.getBBox().getWidth();
float stringWidth = (font.getStringWidth(value) / FONTSCALE) * fontSize;
float adjustAmount = fieldWidth - stringWidth - 4;
- if (q == PDVariableText.QUADDING_CENTERED)
- {
+ if (q == PDVariableText.QUADDING_CENTERED) {
adjustAmount = adjustAmount / 2.0f;
}
contents.newLineAtOffset(adjustAmount, 0);
- }
- else if (q != PDVariableText.QUADDING_LEFT)
- {
+ } else if (q != PDVariableText.QUADDING_LEFT) {
throw new IOException("Error: Unknown justification value:" + q);
}
@@ -767,16 +695,12 @@ class AppearanceGeneratorHelper
float yTextPos = contentRect.getUpperRightY();
int topIndex = ((PDListBox) field).getTopIndex();
-
- for (int i = topIndex; i < numOptions; i++)
- {
-
- if (i == topIndex)
- {
+
+ for (int i = topIndex; i < numOptions; i++) {
+
+ if (i == topIndex) {
yTextPos = yTextPos - font.getFontDescriptor().getAscent() / FONTSCALE * fontSize;
- }
- else
- {
+ } else {
yTextPos = yTextPos - font.getBoundingBox().getHeight() / FONTSCALE * fontSize;
contents.beginText();
}
@@ -784,52 +708,44 @@ class AppearanceGeneratorHelper
contents.newLineAtOffset(contentRect.getLowerLeftX(), yTextPos);
contents.showText(options.get(i));
- if (i != (numOptions - 1))
- {
+ if (i != (numOptions - 1)) {
contents.endText();
}
}
}
-
+
/**
* Writes the stream to the actual stream in the COSStream.
*
* @throws IOException If there is an error writing to the stream
*/
- private void writeToStream(byte[] data, PDAppearanceStream appearanceStream) throws IOException
- {
+ private void writeToStream(byte[] data, PDAppearanceStream appearanceStream) throws IOException {
OutputStream out = appearanceStream.getCOSObject().createOutputStream();
out.write(data);
out.close();
}
/**
- * My "not so great" method for calculating the fontsize. It does not work superb, but it
- * handles ok.
+ * My "not so great" method for calculating the fontsize. It does not work
+ * superb, but it handles ok.
*
* @return the calculated font-size
* @throws IOException If there is an error getting the font information.
*/
- private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException
- {
+ private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException {
float fontSize = defaultAppearance.getFontSize();
-
+
// zero is special, it means the text is auto-sized
- if (fontSize == 0)
- {
- if (isMultiLine())
- {
+ if (fontSize == 0) {
+ if (isMultiLine()) {
PlainText textContent = new PlainText(value);
- if (textContent.getParagraphs() != null)
- {
+ if (textContent.getParagraphs() != null) {
float width = contentRect.getWidth() - contentRect.getLowerLeftX();
float fs = MINIMUM_FONT_SIZE;
- while (fs <= DEFAULT_FONT_SIZE)
- {
+ while (fs <= DEFAULT_FONT_SIZE) {
// determine the number of lines needed for this font and contentRect
int numLines = 0;
- for (PlainText.Paragraph paragraph : textContent.getParagraphs())
- {
+ for (PlainText.Paragraph paragraph : textContent.getParagraphs()) {
numLines += paragraph.getLines(font, fs, width).size();
}
// calculate the height required for this font size
@@ -838,8 +754,7 @@ class AppearanceGeneratorHelper
float height = leading * numLines;
// if this font size didn't fit, use the prior size that did fit
- if (height > contentRect.getHeight())
- {
+ if (height > contentRect.getHeight()) {
return Math.max(fs - 1, MINIMUM_FONT_SIZE);
}
fs++;
@@ -849,61 +764,113 @@ class AppearanceGeneratorHelper
// Acrobat defaults to 12 for multiline text with size 0
return DEFAULT_FONT_SIZE;
- }
- else
- {
+ } else {
float yScalingFactor = FONTSCALE * font.getFontMatrix().getScaleY();
float xScalingFactor = FONTSCALE * font.getFontMatrix().getScaleX();
-
+
// fit width
float width = font.getStringWidth(value) * font.getFontMatrix().getScaleX();
float widthBasedFontSize = contentRect.getWidth() / width * xScalingFactor;
// fit height
- float height = (font.getFontDescriptor().getCapHeight() +
- -font.getFontDescriptor().getDescent()) * font.getFontMatrix().getScaleY();
- if (height <= 0)
- {
+ float height = (font.getFontDescriptor().getCapHeight() + -font.getFontDescriptor().getDescent())
+ * font.getFontMatrix().getScaleY();
+ if (height <= 0) {
height = font.getBoundingBox().getHeight() * font.getFontMatrix().getScaleY();
}
float heightBasedFontSize = contentRect.getHeight() / height * yScalingFactor;
-
+
return Math.min(heightBasedFontSize, widthBasedFontSize);
}
}
return fontSize;
}
-
+
+ /*
+ * Resolve the cap height.
+ *
+ * This is a very basic implementation using the height of "H" as reference.
+ */
+ private float resolveCapHeight(PDFont font) throws IOException {
+ return resolveGlyphHeight(font, "H".codePointAt(0));
+ }
+
+ /*
+ * Resolve the descent.
+ *
+ * This is a very basic implementation using the height of "y" - "a" as reference.
+ */
+ private float resolveDescent(PDFont font) throws IOException {
+ return resolveGlyphHeight(font, "y".codePointAt(0)) - resolveGlyphHeight(font, "a".codePointAt(0));
+ }
+
+ // this calculates the real (except for type 3 fonts) individual glyph bounds
+ private float resolveGlyphHeight(PDFont font, int code) throws IOException {
+ GeneralPath path = null;
+ if (font instanceof PDType3Font) {
+ // It is difficult to calculate the real individual glyph bounds for type 3
+ // fonts
+ // because these are not vector fonts, the content stream could contain almost
+ // anything
+ // that is found in page content streams.
+ PDType3Font t3Font = (PDType3Font) font;
+ PDType3CharProc charProc = t3Font.getCharProc(code);
+ if (charProc != null) {
+ BoundingBox fontBBox = t3Font.getBoundingBox();
+ PDRectangle glyphBBox = charProc.getGlyphBBox();
+ if (glyphBBox != null) {
+ // PDFBOX-3850: glyph bbox could be larger than the font bbox
+ glyphBBox.setLowerLeftX(Math.max(fontBBox.getLowerLeftX(), glyphBBox.getLowerLeftX()));
+ glyphBBox.setLowerLeftY(Math.max(fontBBox.getLowerLeftY(), glyphBBox.getLowerLeftY()));
+ glyphBBox.setUpperRightX(Math.min(fontBBox.getUpperRightX(), glyphBBox.getUpperRightX()));
+ glyphBBox.setUpperRightY(Math.min(fontBBox.getUpperRightY(), glyphBBox.getUpperRightY()));
+ path = glyphBBox.toGeneralPath();
+ }
+ }
+ } else if (font instanceof PDVectorFont) {
+ PDVectorFont vectorFont = (PDVectorFont) font;
+ path = vectorFont.getPath(code);
+ } else if (font instanceof PDSimpleFont) {
+ PDSimpleFont simpleFont = (PDSimpleFont) font;
+
+ // these two lines do not always work, e.g. for the TT fonts in file 032431.pdf
+ // which is why PDVectorFont is tried first.
+ String name = simpleFont.getEncoding().getName(code);
+ path = simpleFont.getPath(name);
+ } else {
+ // shouldn't happen, please open issue in JIRA
+ LOG.warn("Unknown font class: " + font.getClass());
+ }
+ if (path == null) {
+ return -1;
+ }
+ return (float) path.getBounds2D().getHeight();
+ }
+
/**
* Resolve the bounding box.
*
- * @param fieldWidget the annotation widget.
+ * @param fieldWidget the annotation widget.
* @param appearanceStream the annotations appearance stream.
* @return the resolved boundingBox.
*/
- private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget,
- PDAppearanceStream appearanceStream)
- {
+ private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget, PDAppearanceStream appearanceStream) {
PDRectangle boundingBox = appearanceStream.getBBox();
- if (boundingBox == null)
- {
+ if (boundingBox == null) {
boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle();
}
return boundingBox;
}
-
+
/**
* Apply padding to a box.
*
* @param box box
* @return the padded box.
*/
- private PDRectangle applyPadding(PDRectangle box, float padding)
- {
- return new PDRectangle(box.getLowerLeftX() + padding,
- box.getLowerLeftY() + padding,
- box.getWidth() - 2 * padding,
- box.getHeight() - 2 * padding);
+ private PDRectangle applyPadding(PDRectangle box, float padding) {
+ return new PDRectangle(box.getLowerLeftX() + padding, box.getLowerLeftY() + padding,
+ box.getWidth() - 2 * padding, box.getHeight() - 2 * padding);
}
}
\ No newline at end of file
Added: pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormGenerateAppearancesTest.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormGenerateAppearancesTest.java?rev=1884334&view=auto
==============================================================================
--- pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormGenerateAppearancesTest.java (added)
+++ pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormGenerateAppearancesTest.java Fri Dec 11 18:20:16 2020
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.pdmodel.interactive.form;
+
+import static org.junit.Assert.assertFalse;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
+import org.junit.Test;
+
+public class PDAcroFormGenerateAppearancesTest {
+
+ /**
+ * PDFBOX-5041 Missing font descriptor
+ *
+ * @throws IOException
+ */
+ @Test
+ public void test5041MissingFontDescriptor() throws IOException
+ {
+
+ String sourceUrl = "https://issues.apache.org/jira/secure/attachment/13016941/REDHAT-1301016-0.pdf";
+
+ PDDocument testPdf = null;
+ try
+ {
+ testPdf = PDDocument.load(new URL(sourceUrl).openStream());
+ PDDocumentCatalog catalog = testPdf.getDocumentCatalog();
+ boolean thrown = false;
+ try
+ {
+ catalog.getAcroForm();
+ }
+ catch (Exception e)
+ {
+ thrown = true;
+ }
+ assertFalse("There shall be no exception when getting the AcroForm", thrown);
+ }
+ finally
+ {
+ IOUtils.closeQuietly(testPdf);
+ }
+ }
+}