blob: dc71daf1c96cd7d47e5f57b58ebd47b54848af9f [file] [log] [blame]
/*
* 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 java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.fontbox.util.BoundingBox;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdfwriter.ContentStreamWriter;
import org.apache.pdfbox.pdmodel.PDResources;
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.AppearanceStyle;
import org.apache.pdfbox.pdmodel.interactive.PlainText;
import org.apache.pdfbox.pdmodel.interactive.PlainTextFormatter;
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary;
import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.util.Matrix;
/**
* Create the AcroForms field appearance helper.
*
* @author Stephan Gerhard
* @author Ben Litchfield
*/
class AppearanceGeneratorHelper
{
private static final Logger LOG = LogManager.getLogger(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.
*
* 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};
/**
* The scaling factor for font units to PDF units
*/
private static final int FONTSCALE = 1000;
/**
* The default font size used for multiline text
*/
private static final float DEFAULT_FONT_SIZE = 12;
/**
* The minimum/maximum font sizes used for multiline text auto sizing
*/
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
*/
AppearanceGeneratorHelper(PDVariableText field) throws IOException
{
this.field = field;
validateAndEnsureAcroFormResources();
try
{
this.defaultAppearance = field.getDefaultAppearanceString();
}
catch (IOException ex)
{
throw new IOException("Could not process default appearance string '" +
field.getDefaultAppearance() + "' for field '" +
field.getFullyQualifiedName() + "': " + ex.getMessage(), ex);
}
}
/*
* Adobe Reader/Acrobat are adding resources which are at the field/widget level
* to the AcroForm level.
*/
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
PDResources acroFormResources = field.getAcroForm().getDefaultResources();
if (acroFormResources == null)
{
return;
}
for (PDAnnotationWidget widget : field.getWidgets())
{
PDAppearanceStream stream = widget.getNormalAppearanceStream();
if (stream == null)
{
continue;
}
PDResources widgetResources = stream.getResources();
if (widgetResources == null)
{
continue;
}
COSDictionary widgetFontDict = widgetResources.getCOSObject()
.getCOSDictionary(COSName.FONT);
COSDictionary acroFormFontDict = acroFormResources.getCOSObject()
.getCOSDictionary(COSName.FONT);
for (COSName fontResourceName : widgetResources.getFontNames())
{
try
{
if (acroFormResources.getFont(fontResourceName) == null)
{
LOG.debug("Adding font resource {} from widget to AcroForm",
fontResourceName);
// use the COS-object to preserve a possible indirect object reference
acroFormFontDict.setItem(fontResourceName,
widgetFontDict.getItem(fontResourceName));
}
}
catch (IOException e)
{
LOG.warn("Unable to match field level font with AcroForm font", e);
}
}
}
}
/**
* 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
{
value = getFormattedValue(apValue);
// Treat multiline field values in single lines as single lime values.
// This is in line with how Adobe Reader behaves when entering 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())
{
value = value.replaceAll("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]", " ");
}
for (PDAnnotationWidget widget : field.getWidgets())
{
if (widget.getCOSObject().containsKey("PMD"))
{
LOG.warn(
"widget of field {} is a PaperMetaData widget, no appearance stream created",
field.getFullyQualifiedName());
continue;
}
// 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)
{
defaultAppearance = getWidgetDefaultAppearanceString(widget);
}
PDRectangle rect = widget.getRectangle();
if (rect == null)
{
widget.getCOSObject().removeItem(COSName.AP);
LOG.warn("widget of field {} has no rectangle, no appearance stream created",
field.getFullyQualifiedName());
continue;
}
PDAppearanceDictionary appearanceDict = widget.getAppearance();
if (appearanceDict == null)
{
appearanceDict = new PDAppearanceDictionary();
widget.setAppearance(appearanceDict);
}
PDAppearanceEntry appearance = appearanceDict.getNormalAppearance();
// TODO support appearances other than "normal"
PDAppearanceStream appearanceStream;
if (isValidAppearanceStream(appearance))
{
appearanceStream = appearance.getAppearanceStream();
}
else
{
appearanceStream = prepareNormalAppearanceStream(widget);
appearanceDict.setNormalAppearance(appearanceStream);
// TODO support appearances other than "normal"
}
PDAppearanceCharacteristicsDictionary appearanceCharacteristics =
widget.getAppearanceCharacteristics();
/*
* 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 (appearanceCharacteristics != null || appearanceStream.getContentStream().getLength() == 0)
{
initializeAppearanceContent(widget, appearanceCharacteristics, appearanceStream);
}
setAppearanceContent(widget, appearanceStream);
// restore the field level appearance
defaultAppearance = acroFormAppearance;
}
}
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)
{
return apValue;
}
PDAction actionF = actions.getF();
if (actionF != null)
{
if (field.getAcroForm().getScriptingHandler() != null)
{
ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler();
return scriptingHandler.format((PDActionJavaScript) actionF, apValue);
}
LOG.info("Field contains a formatting action but no ScriptingHandler " +
"has been supplied - formatted value might be incorrect");
}
return apValue;
}
private static boolean isValidAppearanceStream(PDAppearanceEntry appearance)
{
if (appearance == null)
{
return false;
}
if (!appearance.isStream())
{
return false;
}
PDRectangle bbox = appearance.getAppearanceStream().getBBox();
if (bbox == null)
{
return false;
}
return Math.abs(bbox.getWidth()) > 0 && Math.abs(bbox.getHeight()) > 0;
}
private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widget)
{
PDAppearanceStream appearanceStream = new PDAppearanceStream(field.getAcroForm().getDocument());
// Calculate the entries for the bounding box and the transformation matrix
// settings for the appearance stream
int rotation = resolveRotation(widget);
PDRectangle rect = widget.getRectangle();
Matrix matrix = Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0);
Point2D.Float point2D = matrix.transformPoint(rect.getWidth(), rect.getHeight());
PDRectangle bbox = new PDRectangle(Math.abs((float) point2D.getX()), Math.abs((float) point2D.getY()));
appearanceStream.setBBox(bbox);
AffineTransform at = calculateMatrix(bbox, rotation);
if (!at.isIdentity())
{
appearanceStream.setMatrix(at);
}
appearanceStream.setFormType(1);
appearanceStream.setResources(new PDResources());
return appearanceStream;
}
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)
{
// 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
*
* @param widget the field widget
* @param appearanceCharacteristics the appearance characteristics dictionary from the widget or
* null
* @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,
PDAppearanceCharacteristicsDictionary appearanceCharacteristics,
PDAppearanceStream appearanceStream) throws IOException
{
try (ByteArrayOutputStream output = new ByteArrayOutputStream();
PDAppearanceContentStream contents = new PDAppearanceContentStream(appearanceStream, output))
{
// TODO: support more entries like patterns, etc.
if (appearanceCharacteristics != null)
{
PDColor backgroundColour = appearanceCharacteristics.getBackground();
if (backgroundColour != null)
{
contents.setNonStrokingColor(backgroundColour);
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
contents.addRect(bbox.getLowerLeftX(),bbox.getLowerLeftY(),bbox.getWidth(), bbox.getHeight());
contents.fill();
}
float lineWidth = 0f;
PDColor borderColour = appearanceCharacteristics.getBorderColour();
if (borderColour != null)
{
contents.setStrokingColor(borderColour);
lineWidth = 1f;
}
PDBorderStyleDictionary borderStyle = widget.getBorderStyle();
if (borderStyle != null && borderStyle.getWidth() > 0)
{
lineWidth = borderStyle.getWidth();
}
if (lineWidth > 0 && borderColour != null)
{
if (Float.compare(lineWidth, 1) != 0)
{
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());
contents.closeAndStroke();
}
}
writeToStream(output.toByteArray(), appearanceStream);
}
}
/**
* Constructs and sets new contents for given appearance stream.
*/
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
try (ByteArrayOutputStream output = new ByteArrayOutputStream())
{
ContentStreamWriter writer = new ContentStreamWriter(output);
List<Object> tokens = new PDFStreamParser(appearanceStream).parse();
int bmcIndex = tokens.indexOf(BMC);
if (bmcIndex == -1)
{
// append to existing stream
writer.writeTokens(tokens);
writer.writeTokens(COSName.TX, BMC);
}
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)
{
// append EMC
writer.writeTokens(EMC);
}
else
{
// append contents after EMC
writer.writeTokens(tokens.subList(emcIndex, tokens.size()));
}
writeToStream(output.toByteArray(), appearanceStream);
}
}
/**
* Generate and insert text content and clipping around it.
*/
private void insertGeneratedAppearance(PDAnnotationWidget widget,
PDAppearanceStream appearanceStream,
OutputStream output) throws IOException
{
try (PDAppearanceContentStream contents = new PDAppearanceContentStream(appearanceStream, output))
{
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
// 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.
float borderWidth = 0;
if (widget.getBorderStyle() != null)
{
borderWidth = widget.getBorderStyle().getWidth();
}
float padding = Math.max(1f, borderWidth);
PDRectangle clipRect = applyPadding(bbox, padding);
PDRectangle contentRect = applyPadding(clipRect, padding);
contents.saveGraphicsState();
// Acrobat always adds a clipping path
contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(),
clipRect.getWidth(), clipRect.getHeight());
contents.clip();
// get the font
PDFont font = defaultAppearance.getFont();
if (font == null)
{
throw new IllegalArgumentException("font is null, check whether /DA entry is incomplete or incorrect");
}
if (font.getName().contains("+"))
{
LOG.warn("Font '{}' of field '{}' contains subsetted font '{}'",
defaultAppearance.getFontName().getName(), field.getFullyQualifiedName(),
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(\"{}\", font);",
defaultAppearance.getFontName().getName());
}
// calculate the fontSize (because 0 = autosize)
float fontSize = defaultAppearance.getFontSize();
if (Float.compare(fontSize, 0) == 0)
{
fontSize = calculateFontSize(font, contentRect);
}
// for a listbox generate the highlight rectangle for the selected
// options
if (field instanceof PDListBox)
{
insertGeneratedListboxSelectionHighlight(contents, appearanceStream, font, fontSize);
}
// start the text output
contents.beginText();
// write font and color from the /DA string, with the calculated font size
defaultAppearance.writeTo(contents, fontSize);
// calculate the y-position of the baseline
float y;
// calculate font metrics at font size
float fontScaleY = fontSize / FONTSCALE;
float fontBoundingBoxAtSize = font.getBoundingBox().getHeight() * fontScaleY;
float fontCapAtSize;
float fontDescentAtSize;
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;
}
if (field instanceof PDTextField && ((PDTextField) field).isMultiline())
{
y = contentRect.getUpperRightY() - fontBoundingBoxAtSize;
}
else
{
// Adobe shows the text 'shifted up' in case the caps don't fit into the clipping area
if (fontCapAtSize > clipRect.getHeight())
{
y = clipRect.getLowerLeftY() + -fontDescentAtSize;
}
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())
{
insertGeneratedCombAppearance(contents, appearanceStream, font, fontSize);
}
else if (field instanceof PDListBox)
{
insertGeneratedListboxAppearance(contents, appearanceStream, contentRect, font, fontSize);
}
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();
formatter.format();
}
contents.endText();
contents.restoreGraphicsState();
}
}
/*
* 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)
{
// 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)
{
return new AffineTransform();
}
float tx = 0, ty = 0;
switch (rotation)
{
case 90:
tx = bbox.getUpperRightY();
break;
case 180:
tx = bbox.getUpperRightY();
ty = bbox.getUpperRightX();
break;
case 270:
ty = bbox.getUpperRightX();
break;
default:
break;
}
Matrix matrix = Matrix.getRotateInstance(Math.toRadians(rotation), tx, ty);
return matrix.createAffineTransform();
}
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,
* 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).getMaxLen() != -1 &&
!((PDTextField) field).isMultiline() &&
!((PDTextField) field).isPassword() &&
!((PDTextField) field).isFileSelect();
}
/**
* Generate the appearance for comb fields.
*
* @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
* @throws IOException
*/
private void insertGeneratedCombAppearance(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream,
PDFont font, float fontSize) throws IOException
{
int maxLen = ((PDTextField) field).getMaxLen();
int quadding = field.getQ();
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 prevCharWidth = 0f;
float xOffset = combWidth / 2;
// add to initial offset if right aligned or centered
if (quadding == 2)
{
xOffset = xOffset + (maxLen - numChars) * combWidth;
}
else if (quadding == 1)
{
xOffset = xOffset + Math.floorDiv(maxLen - numChars, 2) * combWidth;
}
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(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream,
PDFont font, float fontSize) throws IOException
{
PDListBox listBox = (PDListBox) field;
List<Integer> indexEntries = listBox.getSelectedOptionsIndex();
List<String> values = listBox.getValue();
List<String> options = listBox.getOptionsExportValues();
if (!values.isEmpty() && !options.isEmpty() && indexEntries.isEmpty())
{
// create indexEntries from options
indexEntries = new ArrayList<>(values.size());
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 display starts with the first entry in Opt.
int topIndex = listBox.getTopIndex();
float highlightBoxHeight = font.getBoundingBox().getHeight() * fontSize / FONTSCALE;
// the padding area
PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1);
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);
contents.fill();
}
contents.setNonStrokingColor(0f);
}
private void insertGeneratedListboxAppearance(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream,
PDRectangle contentRect, PDFont font, float fontSize) throws IOException
{
contents.setNonStrokingColor(0f);
int q = field.getQ();
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)
{
adjustAmount = adjustAmount / 2.0f;
}
contents.newLineAtOffset(adjustAmount, 0);
}
else if (q != PDVariableText.QUADDING_LEFT)
{
throw new IOException("Error: Unknown justification value:" + q);
}
List<String> options = ((PDListBox) field).getOptionsDisplayValues();
int numOptions = options.size();
float yTextPos = contentRect.getUpperRightY();
int topIndex = ((PDListBox) field).getTopIndex();
float ascent = font.getFontDescriptor().getAscent();
float height = font.getBoundingBox().getHeight();
for (int i = topIndex; i < numOptions; i++)
{
if (i == topIndex)
{
yTextPos = yTextPos - ascent / FONTSCALE * fontSize;
}
else
{
yTextPos = yTextPos - height / FONTSCALE * fontSize;
contents.beginText();
}
contents.newLineAtOffset(contentRect.getLowerLeftX(), yTextPos);
contents.showText(options.get(i));
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
{
try (OutputStream out = appearanceStream.getCOSObject().createOutputStream())
{
out.write(data);
}
}
/**
* 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
{
float fontSize = defaultAppearance.getFontSize();
// zero is special, it means the text is auto-sized
if (Float.compare(fontSize, 0) == 0)
{
if (isMultiLine())
{
PlainText textContent = new PlainText(value);
if (textContent.getParagraphs() != null)
{
float width = contentRect.getWidth() - contentRect.getLowerLeftX();
float fs = MINIMUM_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())
{
numLines += paragraph.getLines(font, fs, width).size();
}
// calculate the height required for this font size
float fontScaleY = fs / FONTSCALE;
float leading = font.getBoundingBox().getHeight() * fontScaleY;
float height = leading * numLines;
// if this font size didn't fit, use the prior size that did fit
if (height > contentRect.getHeight())
{
return Math.max(fs - 1, MINIMUM_FONT_SIZE);
}
fs++;
}
return Math.min(fs, DEFAULT_FONT_SIZE);
}
// Acrobat defaults to 12 for multiline text with size 0
return DEFAULT_FONT_SIZE;
}
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)
{
height = font.getBoundingBox().getHeight() * font.getFontMatrix().getScaleY();
}
float heightBasedFontSize = contentRect.getHeight() / height * yScalingFactor;
if (Float.isInfinite(widthBasedFontSize))
{
// PDFBOX-5763: avoids -Infinity if empty value and tiny rectangle
return heightBasedFontSize;
}
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 appearanceStream the annotations appearance stream.
* @return the resolved boundingBox.
*/
private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget,
PDAppearanceStream appearanceStream)
{
PDRectangle boundingBox = appearanceStream.getBBox();
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);
}
}