aboutsummaryrefslogtreecommitdiffstats
path: root/libqpdf
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2018-06-19 15:26:41 +0200
committerJay Berkenbilt <ejb@ql.org>2018-06-21 21:57:13 +0200
commit0b05111db80469d3f556209bfd856af1fda9b142 (patch)
tree46aca07e5984bfda3b284bb70383b11840cb3436 /libqpdf
parent0dadf17ab705fa2f96f0513278672978d73601ed (diff)
downloadqpdf-0b05111db80469d3f556209bfd856af1fda9b142.tar.zst
Implement helper class for interactive forms
Diffstat (limited to 'libqpdf')
-rw-r--r--libqpdf/QPDFAcroFormDocumentHelper.cc252
-rw-r--r--libqpdf/QPDFAnnotationObjectHelper.cc75
-rw-r--r--libqpdf/QPDFFormFieldObjectHelper.cc190
-rw-r--r--libqpdf/QPDFPageObjectHelper.cc23
-rw-r--r--libqpdf/build.mk3
5 files changed, 543 insertions, 0 deletions
diff --git a/libqpdf/QPDFAcroFormDocumentHelper.cc b/libqpdf/QPDFAcroFormDocumentHelper.cc
new file mode 100644
index 00000000..7e70fd92
--- /dev/null
+++ b/libqpdf/QPDFAcroFormDocumentHelper.cc
@@ -0,0 +1,252 @@
+#include <qpdf/QPDFAcroFormDocumentHelper.hh>
+
+#include <qpdf/QTC.hh>
+#include <qpdf/QPDFPageDocumentHelper.hh>
+
+QPDFAcroFormDocumentHelper::Members::~Members()
+{
+}
+
+QPDFAcroFormDocumentHelper::Members::Members() :
+ cache_valid(false)
+{
+}
+
+QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF& qpdf) :
+ QPDFDocumentHelper(qpdf),
+ m(new Members())
+{
+}
+
+void
+QPDFAcroFormDocumentHelper::invalidateCache()
+{
+ this->m->cache_valid = false;
+ this->m->field_to_annotations.clear();
+ this->m->annotation_to_field.clear();
+}
+
+bool
+QPDFAcroFormDocumentHelper::hasAcroForm()
+{
+ return this->qpdf.getRoot().hasKey("/AcroForm");
+}
+
+std::vector<QPDFFormFieldObjectHelper>
+QPDFAcroFormDocumentHelper::getFormFields()
+{
+ analyze();
+ std::vector<QPDFFormFieldObjectHelper> result;
+ for (std::map<QPDFObjGen,
+ std::vector<QPDFAnnotationObjectHelper> >::iterator iter =
+ this->m->field_to_annotations.begin();
+ iter != this->m->field_to_annotations.end(); ++iter)
+ {
+ result.push_back(this->qpdf.getObjectByObjGen((*iter).first));
+ }
+ return result;
+}
+
+std::vector<QPDFAnnotationObjectHelper>
+QPDFAcroFormDocumentHelper::getAnnotationsForField(QPDFFormFieldObjectHelper h)
+{
+ analyze();
+ std::vector<QPDFAnnotationObjectHelper> result;
+ QPDFObjGen og(h.getObjectHandle().getObjGen());
+ if (this->m->field_to_annotations.count(og))
+ {
+ result = this->m->field_to_annotations[og];
+ }
+ return result;
+}
+
+std::vector<QPDFAnnotationObjectHelper>
+QPDFAcroFormDocumentHelper::getWidgetAnnotationsForPage(QPDFPageObjectHelper h)
+{
+ return h.getAnnotations("/Widget");
+}
+
+QPDFFormFieldObjectHelper
+QPDFAcroFormDocumentHelper::getFieldForAnnotation(QPDFAnnotationObjectHelper h)
+{
+ QPDFObjectHandle oh = h.getObjectHandle();
+ if (! (oh.isDictionary() &&
+ oh.getKey("/Subtype").isName() &&
+ (oh.getKey("/Subtype").getName() == "/Widget")))
+ {
+ throw std::logic_error(
+ "QPDFAnnotationObjectHelper::getFieldForAnnotation called for"
+ " non-/Widget annotation");
+ }
+ analyze();
+ QPDFFormFieldObjectHelper result(QPDFObjectHandle::newNull());
+ QPDFObjGen og(oh.getObjGen());
+ if (this->m->annotation_to_field.count(og))
+ {
+ result = this->m->annotation_to_field[og];
+ }
+ return result;
+}
+
+void
+QPDFAcroFormDocumentHelper::analyze()
+{
+ if (this->m->cache_valid)
+ {
+ return;
+ }
+ this->m->cache_valid = true;
+ QPDFObjectHandle acroform = this->qpdf.getRoot().getKey("/AcroForm");
+ if (! (acroform.isDictionary() && acroform.hasKey("/Fields")))
+ {
+ return;
+ }
+ QPDFObjectHandle fields = acroform.getKey("/Fields");
+ if (! fields.isArray())
+ {
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper fields not array");
+ acroform.warnIfPossible(
+ "/Fields key of /AcroForm dictionary is not an array; ignoring");
+ fields = QPDFObjectHandle::newArray();
+ }
+
+ // Traverse /AcroForm to find annotations and map them
+ // bidirectionally to fields.
+
+ std::set<QPDFObjGen> visited;
+ size_t nfields = fields.getArrayNItems();
+ QPDFObjectHandle null(QPDFObjectHandle::newNull());
+ for (size_t i = 0; i < nfields; ++i)
+ {
+ traverseField(fields.getArrayItem(i), null, 0, visited);
+ }
+
+ // All Widget annotations should have been encountered by
+ // traversing /AcroForm, but in case any weren't, find them by
+ // walking through pages, and treat any widget annotation that is
+ // not associated with a field as its own field. This just ensures
+ // that requesting the field for any annotation we find through a
+ // page's /Annots list will have some associated field. Note that
+ // a file that contains this kind of error will probably not
+ // actually work with most viewers.
+
+ QPDFPageDocumentHelper dh(this->qpdf);
+ std::vector<QPDFPageObjectHelper> pages = dh.getAllPages();
+ for (std::vector<QPDFPageObjectHelper>::iterator iter = pages.begin();
+ iter != pages.end(); ++iter)
+ {
+ QPDFPageObjectHelper ph(*iter);
+ std::vector<QPDFAnnotationObjectHelper> annots =
+ getWidgetAnnotationsForPage(ph);
+ for (std::vector<QPDFAnnotationObjectHelper>::iterator i2 =
+ annots.begin();
+ i2 != annots.end(); ++i2)
+ {
+ QPDFObjectHandle annot((*i2).getObjectHandle());
+ QPDFObjGen og(annot.getObjGen());
+ if (this->m->annotation_to_field.count(og) == 0)
+ {
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper orphaned widget");
+ // This is not supposed to happen, but it's easy
+ // enough for us to handle this case. Treat the
+ // annotation as its own field. This could allow qpdf
+ // to sensibly handle a case such as a PDF creator
+ // adding a self-contained annotation (merged with the
+ // field dictionary) to the page's /Annots array and
+ // forgetting to also put it in /AcroForm.
+ annot.warnIfPossible(
+ "this widget annotation is not"
+ " reachable from /AcroForm in the document catalog");
+ this->m->annotation_to_field[og] =
+ QPDFFormFieldObjectHelper(annot);
+ this->m->field_to_annotations[og].push_back(
+ QPDFAnnotationObjectHelper(annot));
+ }
+ }
+ }
+}
+
+void
+QPDFAcroFormDocumentHelper::traverseField(
+ QPDFObjectHandle field, QPDFObjectHandle parent, int depth,
+ std::set<QPDFObjGen>& visited)
+{
+ if (depth > 100)
+ {
+ // Arbitrarily cut off recursion at a fixed depth to avoid
+ // specially crafted files that could cause stack overflow.
+ return;
+ }
+ if (! field.isIndirect())
+ {
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper direct field");
+ field.warnIfPossible(
+ "encountered a direct object as a field or annotation while"
+ " traversing /AcroForm; ignoring field or annotation");
+ return;
+ }
+ if (! field.isDictionary())
+ {
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper non-dictionary field");
+ field.warnIfPossible(
+ "encountered a non-dictionary as a field or annotation while"
+ " traversing /AcroForm; ignoring field or annotation");
+ return;
+ }
+ QPDFObjGen og(field.getObjGen());
+ if (visited.count(og) != 0)
+ {
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper loop");
+ field.warnIfPossible("loop detected while traversing /AcroForm");
+ return;
+ }
+ visited.insert(og);
+
+ // A dictionary encountered while traversing the /AcroForm field
+ // may be a form field, an annotation, or the merger of the two. A
+ // field that has no fields below it is a terminal. If a terminal
+ // field looks like an annotation, it is an annotation because
+ // annotation dictionary fields can be merged with terminal field
+ // dictionaries. Otherwise, the annotation fields might be there
+ // to be inherited by annotations below it.
+
+ bool is_annotation = false;
+ bool is_field = (0 == depth);
+ QPDFObjectHandle kids = field.getKey("/Kids");
+ if (kids.isArray())
+ {
+ is_field = true;
+ size_t nkids = kids.getArrayNItems();
+ for (size_t k = 0; k < nkids; ++k)
+ {
+ traverseField(kids.getArrayItem(k), field, 1 + depth, visited);
+ }
+ }
+ else
+ {
+ if (field.hasKey("/Parent"))
+ {
+ is_field = true;
+ }
+ if (field.hasKey("/Subtype") ||
+ field.hasKey("/Rect") ||
+ field.hasKey("/AP"))
+ {
+ is_annotation = true;
+ }
+ }
+
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper field found",
+ (depth == 0) ? 0 : 1);
+ QTC::TC("qpdf", "QPDFAcroFormDocumentHelper annotation found",
+ (is_field ? 0 : 1));
+
+ if (is_annotation)
+ {
+ QPDFObjectHandle our_field = (is_field ? field : parent);
+ this->m->field_to_annotations[our_field.getObjGen()].push_back(
+ QPDFAnnotationObjectHelper(field));
+ this->m->annotation_to_field[og] =
+ QPDFFormFieldObjectHelper(our_field);
+ }
+}
diff --git a/libqpdf/QPDFAnnotationObjectHelper.cc b/libqpdf/QPDFAnnotationObjectHelper.cc
new file mode 100644
index 00000000..ee1d8180
--- /dev/null
+++ b/libqpdf/QPDFAnnotationObjectHelper.cc
@@ -0,0 +1,75 @@
+#include <qpdf/QPDFAnnotationObjectHelper.hh>
+#include <qpdf/QTC.hh>
+
+QPDFAnnotationObjectHelper::Members::~Members()
+{
+}
+
+QPDFAnnotationObjectHelper::Members::Members()
+{
+}
+
+QPDFAnnotationObjectHelper::QPDFAnnotationObjectHelper(QPDFObjectHandle oh) :
+ QPDFObjectHelper(oh)
+{
+}
+
+std::string
+QPDFAnnotationObjectHelper::getSubtype()
+{
+ return this->oh.getKey("/Subtype").getName();
+}
+
+QPDFObjectHandle::Rectangle
+QPDFAnnotationObjectHelper::getRect()
+{
+ return this->oh.getKey("/Rect").getArrayAsRectangle();
+}
+
+QPDFObjectHandle
+QPDFAnnotationObjectHelper::getAppearanceDictionary()
+{
+ return this->oh.getKey("/AP");
+}
+
+std::string
+QPDFAnnotationObjectHelper::getAppearanceState()
+{
+ if (this->oh.getKey("/AS").isName())
+ {
+ QTC::TC("qpdf", "QPDFAnnotationObjectHelper AS present");
+ return this->oh.getKey("/AS").getName();
+ }
+ QTC::TC("qpdf", "QPDFAnnotationObjectHelper AS absent");
+ return "";
+}
+
+QPDFObjectHandle
+QPDFAnnotationObjectHelper::getAppearanceStream(
+ std::string const& which,
+ std::string const& state)
+{
+ QPDFObjectHandle ap = getAppearanceDictionary();
+ std::string desired_state = state.empty() ? getAppearanceState() : state;
+ if (ap.isDictionary())
+ {
+ QPDFObjectHandle ap_sub = ap.getKey(which);
+ if (ap_sub.isStream() && desired_state.empty())
+ {
+ QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP stream");
+ return ap_sub;
+ }
+ if (ap_sub.isDictionary() && (! desired_state.empty()))
+ {
+ QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP dictionary");
+ QPDFObjectHandle ap_sub_val = ap_sub.getKey(desired_state);
+ if (ap_sub_val.isStream())
+ {
+ QTC::TC("qpdf", "QPDFAnnotationObjectHelper AN sub stream");
+ return ap_sub_val;
+ }
+ }
+ }
+ QTC::TC("qpdf", "QPDFAnnotationObjectHelper AN null");
+ return QPDFObjectHandle::newNull();
+}
diff --git a/libqpdf/QPDFFormFieldObjectHelper.cc b/libqpdf/QPDFFormFieldObjectHelper.cc
new file mode 100644
index 00000000..e6445af3
--- /dev/null
+++ b/libqpdf/QPDFFormFieldObjectHelper.cc
@@ -0,0 +1,190 @@
+#include <qpdf/QPDFFormFieldObjectHelper.hh>
+#include <qpdf/QTC.hh>
+
+QPDFFormFieldObjectHelper::Members::~Members()
+{
+}
+
+QPDFFormFieldObjectHelper::Members::Members()
+{
+}
+
+QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper(QPDFObjectHandle oh) :
+ QPDFObjectHelper(oh),
+ m(new Members())
+{
+}
+
+QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper() :
+ QPDFObjectHelper(QPDFObjectHandle::newNull()),
+ m(new Members())
+{
+}
+
+bool
+QPDFFormFieldObjectHelper::isNull()
+{
+ return this->oh.isNull();
+}
+
+QPDFFormFieldObjectHelper
+QPDFFormFieldObjectHelper::getParent()
+{
+ return this->oh.getKey("/Parent"); // may be null
+}
+
+QPDFObjectHandle
+QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name)
+{
+ QPDFObjectHandle node = this->oh;
+ QPDFObjectHandle result(node.getKey(name));
+ std::set<QPDFObjGen> seen;
+ while (result.isNull() && node.hasKey("/Parent"))
+ {
+ seen.insert(node.getObjGen());
+ node = node.getKey("/Parent");
+ if (seen.count(node.getObjGen()))
+ {
+ break;
+ }
+ result = node.getKey(name);
+ if (! result.isNull())
+ {
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper non-trivial inheritance");
+ }
+ }
+ return result;
+}
+
+std::string
+QPDFFormFieldObjectHelper::getInheritableFieldValueAsString(
+ std::string const& name)
+{
+ QPDFObjectHandle fv = getInheritableFieldValue(name);
+ std::string result;
+ if (fv.isString())
+ {
+ result = fv.getUTF8Value();
+ }
+ return result;
+}
+
+std::string
+QPDFFormFieldObjectHelper::getInheritableFieldValueAsName(
+ std::string const& name)
+{
+ QPDFObjectHandle fv = getInheritableFieldValue(name);
+ std::string result;
+ if (fv.isName())
+ {
+ result = fv.getName();
+ }
+ return result;
+}
+
+std::string
+QPDFFormFieldObjectHelper::getFieldType()
+{
+ return getInheritableFieldValueAsName("/FT");
+}
+
+std::string
+QPDFFormFieldObjectHelper::getFullyQualifiedName()
+{
+ std::string result;
+ QPDFObjectHandle node = this->oh;
+ std::set<QPDFObjGen> seen;
+ while ((! node.isNull()) && (seen.count(node.getObjGen()) == 0))
+ {
+ if (node.getKey("/T").isString())
+ {
+ if (! result.empty())
+ {
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper non-trivial qualified name");
+ result = "." + result;
+ }
+ result = node.getKey("/T").getUTF8Value() + result;
+ }
+ seen.insert(node.getObjGen());
+ node = node.getKey("/Parent");
+ }
+ return result;
+}
+
+std::string
+QPDFFormFieldObjectHelper::getPartialName()
+{
+ std::string result;
+ if (this->oh.getKey("/T").isString())
+ {
+ result = this->oh.getKey("/T").getUTF8Value();
+ }
+ return result;
+}
+
+std::string
+QPDFFormFieldObjectHelper::getAlternativeName()
+{
+ if (this->oh.getKey("/TU").isString())
+ {
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper TU present");
+ return this->oh.getKey("/TU").getUTF8Value();
+ }
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper TU absent");
+ return getFullyQualifiedName();
+}
+
+std::string
+QPDFFormFieldObjectHelper::getMappingName()
+{
+ if (this->oh.getKey("/TM").isString())
+ {
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper TM present");
+ return this->oh.getKey("/TM").getUTF8Value();
+ }
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper TM absent");
+ return getAlternativeName();
+}
+
+QPDFObjectHandle
+QPDFFormFieldObjectHelper::getValue()
+{
+ return getInheritableFieldValue("/V");
+}
+
+std::string
+QPDFFormFieldObjectHelper::getValueAsString()
+{
+ return getInheritableFieldValueAsString("/V");
+}
+
+QPDFObjectHandle
+QPDFFormFieldObjectHelper::getDefaultValue()
+{
+ return getInheritableFieldValue("/DV");
+}
+
+std::string
+QPDFFormFieldObjectHelper::getDefaultValueAsString()
+{
+ return getInheritableFieldValueAsString("/DV");
+}
+
+std::string
+QPDFFormFieldObjectHelper::getDefaultAppearance()
+{
+ return getInheritableFieldValueAsString("/DA");
+}
+
+int
+QPDFFormFieldObjectHelper::getQuadding()
+{
+ int result = 0;
+ QPDFObjectHandle fv = getInheritableFieldValue("/Q");
+ if (fv.isInteger())
+ {
+ QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present");
+ result = static_cast<int>(fv.getIntValue());
+ }
+ return result;
+}
diff --git a/libqpdf/QPDFPageObjectHelper.cc b/libqpdf/QPDFPageObjectHelper.cc
index b16d8751..47bc2117 100644
--- a/libqpdf/QPDFPageObjectHelper.cc
+++ b/libqpdf/QPDFPageObjectHelper.cc
@@ -19,6 +19,29 @@ QPDFPageObjectHelper::getPageImages()
return this->oh.getPageImages();
}
+std::vector<QPDFAnnotationObjectHelper>
+QPDFPageObjectHelper::getAnnotations(std::string const& only_subtype)
+{
+ std::vector<QPDFAnnotationObjectHelper> result;
+ QPDFObjectHandle annots = this->oh.getKey("/Annots");
+ if (annots.isArray())
+ {
+ size_t nannots = annots.getArrayNItems();
+ for (size_t i = 0; i < nannots; ++i)
+ {
+ QPDFObjectHandle annot = annots.getArrayItem(i);
+ if (only_subtype.empty() ||
+ (annot.isDictionary() &&
+ annot.getKey("/Subtype").isName() &&
+ (only_subtype == annot.getKey("/Subtype").getName())))
+ {
+ result.push_back(QPDFAnnotationObjectHelper(annot));
+ }
+ }
+ }
+ return result;
+}
+
std::vector<QPDFObjectHandle>
QPDFPageObjectHelper::getPageContents()
{
diff --git a/libqpdf/build.mk b/libqpdf/build.mk
index 07cfc81e..437c683e 100644
--- a/libqpdf/build.mk
+++ b/libqpdf/build.mk
@@ -35,7 +35,10 @@ SRCS_libqpdf = \
libqpdf/Pl_StdioFile.cc \
libqpdf/Pl_TIFFPredictor.cc \
libqpdf/QPDF.cc \
+ libqpdf/QPDFAcroFormDocumentHelper.cc \
+ libqpdf/QPDFAnnotationObjectHelper.cc \
libqpdf/QPDFExc.cc \
+ libqpdf/QPDFFormFieldObjectHelper.cc \
libqpdf/QPDFObjGen.cc \
libqpdf/QPDFObject.cc \
libqpdf/QPDFObjectHandle.cc \