Skip to content

Commit

Permalink
Summary: Fixed several form flattening issues.
Browse files Browse the repository at this point in the history
- Main change is in PdfStamper
- Added testdocuments and junit
- some smaller refactoring, jdoc and fixing other junits
  • Loading branch information
Lonzak committed Nov 15, 2023
1 parent 6998d7f commit f5ad660
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 33 deletions.
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
205 changes: 180 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,22 @@
*/
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.Iterator;

Check failure on line 56 in openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java

View workflow job for this annotation

GitHub Actions / Checkstyle job

[checkstyle] reported by reviewdog 🐶 Unused import - java.util.Iterator. Raw Output: /github/workspace/./openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java:56:8: error: Unused import - java.util.Iterator. (com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck)
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.NodeList;

Check failure on line 61 in openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java

View workflow job for this annotation

GitHub Actions / Checkstyle job

[checkstyle] reported by reviewdog 🐶 Unused import - org.w3c.dom.NodeList. Raw Output: /github/workspace/./openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java:61:8: error: Unused import - org.w3c.dom.NodeList. (com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck)
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

Check failure on line 63 in openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java

View workflow job for this annotation

GitHub Actions / Checkstyle job

[checkstyle] reported by reviewdog 🐶 Unused import - org.xml.sax.SAXParseException. Raw Output: /github/workspace/./openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java:63:8: error: Unused import - org.xml.sax.SAXParseException. (com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck)

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.ExceptionConverter;
Expand All @@ -58,17 +74,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 +185,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 +546,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 +868,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 +888,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
}
}
}
}

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();
}

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);
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 +1001,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) {
x += box.getWidth();
}
if (rotation != 0 && (rotation % 180 == 0 || rotation % 270 == 0)) {
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 +1175,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 +1206,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 +1525,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

0 comments on commit f5ad660

Please sign in to comment.