Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed several form flattening issues #992

Merged
merged 3 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openpdf/src/main/java/com/lowagie/text/Document.java
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ public boolean isGlyphSubstitutionEnabled() {
*
* @param textRenderingOptions the text rendering options
* @see #setDocumentLanguage(String)
* @see Document#setGlyphSubstitutionsEnabled(boolean)
* @see Document#setGlyphSubstitutionEnabled(boolean)
*/
public void setTextRenderingOptions(TextRenderingOptions textRenderingOptions) {
this.textRenderingOptions = textRenderingOptions == null ? new TextRenderingOptions() : textRenderingOptions;
Expand Down
29 changes: 29 additions & 0 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfContentByte.java
Original file line number Diff line number Diff line change
Expand Up @@ -2254,6 +2254,35 @@ void addTemplateReference(PdfIndirectReference template, PdfName name, float a,
public void addTemplate(PdfTemplate template, float x, float y) {
addTemplate(template, 1, 0, 0, 1, x, y);
}

/**
* Adds a template to this content using double matrices.
*
* @param template the template
* @param a an element of the transformation matrix
* @param b an element of the transformation matrix
* @param c an element of the transformation matrix
* @param d an element of the transformation matrix
* @param e an element of the transformation matrix
* @param f an element of the transformation matrix
*/
public void addTemplate(final PdfTemplate template, final double a, final double b, final double c, final double d, final double e, final double f) {
checkWriter();
checkNoPattern(template);

PdfName name = writer.addDirectTemplateSimple(template, null);
PageResources prs = getPageResources();
name = prs.addXObject(name, template.getIndirectReference());

content.append("q ");
content.append(a).append(' ');
content.append(b).append(' ');
content.append(c).append(' ');
content.append(d).append(' ');
content.append(e).append(' ');
content.append(f).append(" cm ");
content.append(name.getBytes()).append(" Do Q").append_i(separator);
}

/**
* Changes the current color for filling paths (device dependent colors!).
Expand Down
4 changes: 4 additions & 0 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -2685,6 +2685,10 @@ public int getPermissions() {
return pValue;
}

public void setPermissions(int permissionValue) {
this.pValue=permissionValue;
}

/**
* Returns <CODE>true</CODE> if the PDF has a 128 bit key encryption.
*
Expand Down
15 changes: 15 additions & 0 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfRectangle.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ public PdfRectangle(Rectangle rectangle, int rotation) {
public PdfRectangle(Rectangle rectangle) {
this(rectangle.getLeft(), rectangle.getBottom(), rectangle.getRight(), rectangle.getTop(), 0);
}

/**
* To be used when the array contains 4 float numbers as pdf coordinates like RECT / BBox
* @param rectangle as a PdfArray
*/
public PdfRectangle(PdfArray rectangle) {
this(convertToFloat(rectangle.getPdfObject(0)),convertToFloat(rectangle.getPdfObject(1)),convertToFloat(rectangle.getPdfObject(2)),convertToFloat(rectangle.getPdfObject(3)));
}

private static float convertToFloat(PdfObject object) {
if(!(object instanceof PdfNumber)) {
throw new IllegalArgumentException("Invalid argument. Float value (coordinate) expected! But was "+object);
}
return ((PdfNumber)object).floatValue();
}

// methods
/**
Expand Down
201 changes: 176 additions & 25 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@
*/
package com.lowagie.text.pdf;

import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.xml.sax.SAXException;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.ExceptionConverter;
Expand All @@ -58,17 +70,8 @@
import com.lowagie.text.pdf.interfaces.PdfViewerPreferences;
import com.lowagie.text.pdf.internal.PdfViewerPreferencesImp;
import com.lowagie.text.xml.xmp.XmpReader;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;


class PdfStamperImp extends PdfWriter {
HashMap<PdfReader, IntHashtable> readers2intrefs = new HashMap<>();
Expand Down Expand Up @@ -178,11 +181,11 @@ void close(Map<String, String> moreInfo) throws IOException {
acroFields.getXfa().setXfa(this);
}
if (sigFlags != 0) {
acroForm.put(PdfName.SIGFLAGS, new PdfNumber(sigFlags));
markUsed(acroForm);
markUsed(catalog);
acroForm.put(PdfName.SIGFLAGS, new PdfNumber(sigFlags));
markUsed(acroForm);
markUsed(catalog);
}
}
}
closed = true;
addSharedObjectsToBody();
setOutlines();
Expand Down Expand Up @@ -539,6 +542,15 @@ RandomAccessFileOrArray getReaderFile(PdfReader reader) {
return currentPdfReaderInstance.getReaderFile();
}

/**
* Removes the encryption from the document (and also inherently the permissions)
* @throws DocumentException
*/
public void removeEncryption() throws DocumentException {
super.setEncryption(null,null,0,ENCRYPTION_NONE);
this.reader.setPermissions(0);
}

/**
* @param reader
* @param openFile
Expand Down Expand Up @@ -852,8 +864,10 @@ void flatFields() {
}
PdfDictionary acroForm = reader.getCatalog().getAsDict(PdfName.ACROFORM);
PdfArray acroFds = null;
PdfBoolean needAppearance=null;
if (acroForm != null) {
acroFds = (PdfArray)PdfReader.getPdfObject(acroForm.get(PdfName.FIELDS), acroForm);
needAppearance = (PdfBoolean)acroForm.get(PdfName.NEEDAPPEARANCES);
}
for (Map.Entry<String, Item> entry : fields.entrySet()) {
String name = entry.getKey();
Expand All @@ -870,17 +884,104 @@ void flatFields() {
if (page == -1)
continue;
PdfDictionary appDic = merged.getAsDict(PdfName.AP);
PdfStream appStream=null;

if (appDic != null) {
appStream = appDic.getAsStream(PdfName.N);
}

//Lonzak (fix) if NeedAppearances flag is true then regenerate appearance before flattening
if (needAppearance!=null && needAppearance.booleanValue()) {

boolean regenerate = false;

//not existing AP
if((appDic == null || appDic.get(PdfName.N) == null)) {
regenerate=true;
}
else if(appDic.getDirectObject(PdfName.N) instanceof PdfIndirectReference) {
//since newly added
regenerate=false;
}
else {
int type = acroFields.getFieldType(name);
String value = acroFields.getField(name);

//workaround for libre/open office which creates nearly empty streams: /TX BMC\nEMC
//Currently only for Textfields - for Radios/Checkboxes the appearance stream has to be determined (by looking at /AS or /V)
if(type==AcroFields.FIELD_TYPE_TEXT && appStream instanceof PRStream) {
if(value!=null && !value.isEmpty()) {
try {
byte[] bytes= PdfReader.getStreamBytes((PRStream)appStream);
((PRStream)appStream).setData(bytes);
if(new String(bytes).equals("/Tx BMC\nEMC\n")) {
regenerate=true;
}
}
catch (IOException e) {
//ignore
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you try to find out whether creation of the appearance stream is necessary; you only want to call regenerateField if there was no appearance stream yet (except probably an empty one).
Strictly speaking, though, NeedAppearances set to true requires you to recreate all appearances.
For example some PDF processors set the form values but don't update the appearances. They then set NeedAppearances to delegate that task to the next processor, leaving the PDF in a state with appearances showing the incorrect former value.
(Of course one does not need to re-create appearances that have been created in the current PdfStamperImp, and trying to re-create signature appearances makes no sense, either.)

Copy link
Contributor Author

@Lonzak Lonzak Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to be conservative/careful to not regenerate appearances unnecessarily... But you are right - all appearances need to be regenerated. Lets hope the PDF processor can do this without any errors :-) (And luckily this mechanism is deprecated in 2.0) So the following would suffice?

if (needAppearance!=null && needAppearance.booleanValue()) {
  	boolean regenerate = false;
   	int type = this.acroFields.getFieldType(name);

   	if(type!=AcroFields.FIELD_TYPE_SIGNATURE) {
          	if(appDic != null && appDic.getDirectObject(PdfName.N) instanceof PdfIndirectReference) {
        		//since newly added
          		regenerate=false;
          	}
           	else {
           	    regenerate=true;
           	}
   	}
 ....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. But please do test,


if(regenerate) {
try {
this.acroFields.regenerateField(name);
appDic = this.acroFields.getFieldItem(name).getMerged(k).getAsDict(PdfName.AP);
}
catch (Exception e) {
//ignore any exception
}
}
}

boolean transformNeeded=false;
double rotation = 0;
if(merged.getAsDict(PdfName.MK) != null && merged.getAsDict(PdfName.MK).get(PdfName.R) != null){
rotation = merged.getAsDict(PdfName.MK).getAsNumber(PdfName.R).floatValue();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You use this rotation value to determine how the existing appearance stream needs to be rotated and, consequentially, scaled when put into the field Rect. Strictly speaking, though, the MK dictionary only contains instructions for a PDF processor how to create the appearance for a field. For fitting the appearance into the field Rect, one should use the Matrix of the appearance as described in ISO 32000-2:2020 section 12.5.5.

Usually using the MK value will render the correct result because the annotation Matrix usually is built according to the MK/R value. But in some cases, in particular for signature appearances, PDF processors may have used a different actual rotation (visible in the appearance Matrix), assuming no-one would touch a signature appearance...

Copy link
Contributor Author

@Lonzak Lonzak Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok interesting - so should I leave the rotation code in there in case there is no matrix defined? But if it is it will overwrite the value?

            double rotation = 0;
            if(merged.getAsDict(PdfName.MK) != null && merged.getAsDict(PdfName.MK).get(PdfName.R) != null){
                rotation = merged.getAsDict(PdfName.MK).getAsNumber(PdfName.R).floatValue();
            }
            if (appDic != null && appDic.getAsArray(PdfName.MATRIX) != null) {

                PdfArray matrixArray = appDic.getAsArray(PdfName.MATRIX);
                
                if(matrixArray.size() == 6) {
                    float a = matrixArray.getAsNumber(0).floatValue();
                    float b = matrixArray.getAsNumber(1).floatValue();

                    double rotationRadians = Math.atan2(b, a);
                    //in degrees
                    rotation = Math.toDegrees(rotationRadians);
                }
            }

Copy link
Contributor

@mkl-public mkl-public Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working with the matrix should be done strictly according to spec, i.e. using the algorithm in 12.5.5. If that looks too complicated, continue using the MK/R value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec says something like this:

Algorithm: appearance streams

  1. The appearance’s bounding box (specified by its BBox entry) shall be transformed, using Matrix, to produce a quadrilateral with arbitrary orientation. The transformed appearance box is the smallest upright rectangle that encompasses this quadrilateral.
  2. A matrix A shall be computed that scales and translates the transformed appearance box to align with the edges of the annotation’s rectangle (specified by the Rect entry). A maps the lower-left corner (the corner with the smallest x and y coordinates) and the upper-right corner (the corner with the greatest x and y coordinates) of the transformed appearance box to the corresponding corners of the annotation’s rectangle.
  3. Matrix shall be concatenated with A to form a matrix AA that maps from the appearance’s coordinate system to the annotation’s rectangle in default user space:
    AA = Matrix × 𝐴

Ok I think we'll stick with /R for now....

}

if (this.acroFields.isGenerateAppearances() && appStream!=null) {

PdfArray bboxRaw = appStream.getAsArray(PdfName.BBOX);
PdfArray rectRaw = merged.getAsArray(PdfName.RECT);

if (bboxRaw != null && rectRaw != null) {
transformNeeded = true;
PdfRectangle bbox = new PdfRectangle(bboxRaw);
PdfRectangle rect = new PdfRectangle(rectRaw);

float rectWidth = rect.width();
float rectHeight = rect.height();

//Switches width and height if the rotation is an odd multiple of 90 degrees
if (Math.abs(rotation)>0 && rotation % 180 != 0 && rotation % 90 == 0) {
rectWidth = rect.height();
rectHeight = rect.width();
}

float scaleFactorWidth = Math.abs(bbox.width() != 0 ? rectWidth / bbox.width() : 1.0f);
float scaleFactorHeight = Math.abs(bbox.height() != 0 ? rectHeight / bbox.height() : 1.0f);

PdfArray array = new PdfArray(new float[]{scaleFactorWidth, 0, 0, scaleFactorHeight, 0, 0});
appStream.put(PdfName.MATRIX, array);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you might overwrite a previously existing Matrix with its own ideas in respect to the transformation of the appearance, in particular to its rotation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course the Matrix does not need to remain as it was (as the meaning of the Matrix in form Xobjects used as annotation appearances and in form Xobjects used content stream resources differs somewhat), but the original value should be taken into account and not be overwritten without consideration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

if (scaleFactorWidth != 1 || scaleFactorHeight != 1) {
                        PdfArray array = new PdfArray(new float[]{scaleFactorWidth, 0, 0, scaleFactorHeight, 0, 0});
                        appStream.put(PdfName.MATRIX, array);
                        markUsed(appStream);
                    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if you do as said above, i.e. implement the 12.5.5 algorithm, you would construct the new matrix with that algorithm, not like this here... ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we would stick with the above /R - then this part stays as well (like it is)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially yes.
Usually the rotation of the Matrix is constructed based on the MK/R value, but you apply the rotation separately later, so you usually are ok with overwriting the Matrix with a pure scaling one to make dimensions match.

markUsed(appStream);
}
}

if (appDic != null && (flags & PdfFormField.FLAGS_PRINT) != 0 && (flags & PdfFormField.FLAGS_HIDDEN) == 0) {
PdfObject obj = appDic.get(PdfName.N);
PdfObject normalAppearanceObj = appDic.get(PdfName.N);
PdfAppearance app = null;
if (obj != null) {
PdfObject objReal = PdfReader.getPdfObject(obj);
if (obj instanceof PdfIndirectReference && !obj.isIndirect())
app = new PdfAppearance((PdfIndirectReference) obj);
PdfObject objReal = PdfReader.getPdfObject(normalAppearanceObj);
if (normalAppearanceObj != null) {
if (normalAppearanceObj instanceof PdfIndirectReference && !normalAppearanceObj.isIndirect())
app = new PdfAppearance((PdfIndirectReference)normalAppearanceObj);
else if (objReal instanceof PdfStream) {
((PdfDictionary) objReal).put(PdfName.SUBTYPE, PdfName.FORM);
app = new PdfAppearance((PdfIndirectReference) obj);
} else {
app = new PdfAppearance((PdfIndirectReference)normalAppearanceObj);
}
else {
if (objReal != null && objReal.isDictionary()) {
PdfName as = merged.getAsName(PdfName.AS);
if (as != null) {
Expand All @@ -896,11 +997,52 @@ else if (objReal instanceof PdfStream) {
}
}
}
if (app != null) {
if (app != null && objReal!=null) {
Rectangle box = PdfReader.getNormalizedRectangle(merged.getAsArray(PdfName.RECT));
PdfContentByte cb = getOverContent(page);
cb.setLiteral("Q ");

if(transformNeeded) {
AffineTransform transform = new AffineTransform();
double x = box.getLeft();
double y = box.getBottom();

if (rotation != 0 && rotation % 90 == 0 && rotation % 270 != 0) {
Copy link
Contributor

@mkl-public mkl-public Dec 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test rotation % 270 != 0 makes no sense. I assume you mean something like rotation % 360 != 270.
And for the whole term you probably actually mean rotation % 360 == 90 || rotation % 360 == 180.

Copy link
Contributor Author

@Lonzak Lonzak Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right this makes it more clear:

rotation = rotation % 360; 
if (rotation == 90 || rotation == 270) {
    x += box.getWidth();
}
if (rotation == 180 || rotation == 270) {
    y += box.getHeight();
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, much clearer.

x += box.getWidth();
}
if (rotation != 0 && (rotation % 180 == 0 || rotation % 270 == 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test rotation % 270 == 0 makes no sense, it is true for 270°, 540° (equivalent to 180°), 810° (equivalent to 90°), 1080° (equivalent to 0°), ...
I assume you mean something like rotation % 360 == 180 || rotation % 360 == 270.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cp. above

y += box.getHeight();
}
transform.translate(x, y);

//before applying the rotation convert from degree to radiant
transform.rotate(Math.toRadians(rotation));

// rotation matrix
double[] matrix = new double[6];
transform.getMatrix(matrix);
cb.addTemplate(app, matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
}
else {
//when objReal is an PdfIndirectReference then it was just created (thus it doesn't need to be corrected
if(!(objReal instanceof PdfIndirectReference)) {

// Lonzak: npe bugfix
PdfRectangle bBoxCoordinates = new PdfRectangle(((PdfDictionary)objReal).getAsArray(PdfName.BBOX));
if(bBoxCoordinates!=null && bBoxCoordinates.size()>=4) {
// DEVSIX-1741 - Bugfix backported as Jonthan of iText suggested
Rectangle bBox = PdfReader.getNormalizedRectangle(bBoxCoordinates);
cb.addTemplate(app, (box.getWidth() / bBox.getWidth()), 0, 0, (box.getHeight() / bBox.getHeight()), box.getLeft(), box.getBottom());
}
else {
throw new DocumentException("The required BBox attribute of the field "+ name +" is missing. The PDF is not PDF spec compliant!");
}
}
else {
cb.addTemplate(app, box.getLeft(), box.getBottom());
}
}

cb.setLiteral("q ");
}
}
Expand Down Expand Up @@ -1029,6 +1171,11 @@ private void flatFreeTextFields()
if ((annoto instanceof PdfIndirectReference) && !annoto.isIndirect())
continue;

//Lonzak Fix: java.lang.ClassCastException: com.lowagie.text.pdf.PdfNull cannot be cast to com.lowagie.text.pdf.PdfDictionary
if(!(annoto instanceof PdfDictionary)) {
continue;
}

PdfDictionary annDic = (PdfDictionary)annoto;
if (!annDic.get(PdfName.SUBTYPE).equals(PdfName.FREETEXT))
continue;
Expand All @@ -1055,7 +1202,8 @@ else if (objReal instanceof PdfStream)
}
else
{
if (objReal.isDictionary())
//Lonzak: NPE Fix since objReal or obj can be null
if (objReal!=null && objReal.isDictionary())
{
PdfName as_p = appDic.getAsName(PdfName.AS);
if (as_p != null)
Expand Down Expand Up @@ -1373,8 +1521,11 @@ private void addAnnotationToDocument(PdfAnnotation annot) {
}
}

void addAnnotation(PdfAnnotation annot, int page) {
annot.setPage(page);
public void addAnnotation(PdfAnnotation annot, int page) {
//Bugfix to prevent that for autofill parents the /P page reference is added [^Lonzak]
if(annot.isAnnotation()){
annot.setPage(page);
}
addAnnotation(annot, reader.getPageN(page));
}

Expand Down
2 changes: 2 additions & 0 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,8 @@ private static String getNameString(PdfDictionary dic, PdfName key) {

// types of encryption

/** No encryption */
public static final int ENCRYPTION_NONE = -1;
/** Type of encryption */
public static final int STANDARD_ENCRYPTION_40 = 0;
/** Type of encryption */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import org.junit.jupiter.api.Test;

import java.io.StringReader;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
Expand All @@ -22,8 +24,10 @@ class EmbeddedImageTest
@Test
void processHtmlWithEmbeddedImage() throws Exception
{
String html = Files.readAllLines(Paths.get(ClassLoader.getSystemResource("base64-image.html").getPath())).stream()
.collect(Collectors.joining());
URI resourceUri = ClassLoader.getSystemResource("base64-image.html").toURI();
Path resourcePath = Paths.get(resourceUri);
String html = Files.readAllLines(resourcePath).stream().collect(Collectors.joining());

final StringReader reader = new StringReader(html);
final Map<String, Object> interfaceProps = new HashMap<>();
final List<Element> elements = HTMLWorker.parseToList(reader, new StyleSheet(), interfaceProps);
Expand Down
Loading