-
Notifications
You must be signed in to change notification settings - Fork 610
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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<>(); | ||
|
@@ -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(); | ||
|
@@ -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 | ||
|
@@ -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(); | ||
|
@@ -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 | ||
} | ||
} | ||
} | ||
} | ||
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec says something like this: Algorithm: appearance streams
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like this?
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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... ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Essentially yes. |
||
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) { | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right this makes it more clear:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To test There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 "); | ||
} | ||
} | ||
|
@@ -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; | ||
|
@@ -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) | ||
|
@@ -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)); | ||
} | ||
|
||
|
There was a problem hiding this comment.
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.)There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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,