mirror of
https://github.com/qpdf/qpdf.git
synced 2024-11-05 04:47:53 +00:00
1013 lines
27 KiB
C++
1013 lines
27 KiB
C++
#include <qpdf/QPDFFormFieldObjectHelper.hh>
|
|
#include <qpdf/QTC.hh>
|
|
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
|
|
#include <qpdf/QPDFAnnotationObjectHelper.hh>
|
|
#include <qpdf/QUtil.hh>
|
|
#include <qpdf/Pl_QPDFTokenizer.hh>
|
|
#include <qpdf/QIntC.hh>
|
|
#include <stdlib.h>
|
|
|
|
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
|
|
}
|
|
|
|
QPDFFormFieldObjectHelper
|
|
QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different)
|
|
{
|
|
auto top_field = this->oh;
|
|
std::set<QPDFObjGen> seen;
|
|
while (top_field.isDictionary() &&
|
|
(! top_field.getKey("/Parent").isNull()))
|
|
{
|
|
top_field = top_field.getKey("/Parent");
|
|
if (is_different)
|
|
{
|
|
*is_different = true;
|
|
}
|
|
auto og = top_field.getObjGen();
|
|
if (seen.count(og))
|
|
{
|
|
break;
|
|
}
|
|
seen.insert(og);
|
|
}
|
|
return QPDFFormFieldObjectHelper(top_field);
|
|
}
|
|
|
|
QPDFObjectHandle
|
|
QPDFFormFieldObjectHelper::getFieldFromAcroForm(std::string const& name)
|
|
{
|
|
QPDFObjectHandle result = QPDFObjectHandle::newNull();
|
|
// Fields are supposed to be indirect, so this should work.
|
|
QPDF* q = this->oh.getOwningQPDF();
|
|
if (! q)
|
|
{
|
|
return result;
|
|
}
|
|
auto acroform = q->getRoot().getKey("/AcroForm");
|
|
if (! acroform.isDictionary())
|
|
{
|
|
return result;
|
|
}
|
|
return acroform.getKey(name);
|
|
}
|
|
|
|
QPDFObjectHandle
|
|
QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name)
|
|
{
|
|
QPDFObjectHandle node = this->oh;
|
|
if (! node.isDictionary())
|
|
{
|
|
return QPDFObjectHandle::newNull();
|
|
}
|
|
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");
|
|
}
|
|
|
|
QPDFObjectHandle
|
|
QPDFFormFieldObjectHelper::getDefaultResources()
|
|
{
|
|
return getFieldFromAcroForm("/DR");
|
|
}
|
|
|
|
std::string
|
|
QPDFFormFieldObjectHelper::getDefaultAppearance()
|
|
{
|
|
auto value = getInheritableFieldValue("/DA");
|
|
bool looked_in_acroform = false;
|
|
if (! value.isString())
|
|
{
|
|
value = getFieldFromAcroForm("/DA");
|
|
looked_in_acroform = true;
|
|
}
|
|
std::string result;
|
|
if (value.isString())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper DA present",
|
|
looked_in_acroform ? 0 : 1);
|
|
result = value.getUTF8Value();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int
|
|
QPDFFormFieldObjectHelper::getQuadding()
|
|
{
|
|
QPDFObjectHandle fv = getInheritableFieldValue("/Q");
|
|
bool looked_in_acroform = false;
|
|
if (! fv.isInteger())
|
|
{
|
|
fv = getFieldFromAcroForm("/Q");
|
|
looked_in_acroform = true;
|
|
}
|
|
int result = 0;
|
|
if (fv.isInteger())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present",
|
|
looked_in_acroform ? 0 : 1);
|
|
result = QIntC::to_int(fv.getIntValue());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int
|
|
QPDFFormFieldObjectHelper::getFlags()
|
|
{
|
|
QPDFObjectHandle f = getInheritableFieldValue("/Ff");
|
|
return f.isInteger() ? f.getIntValueAsInt() : 0;
|
|
}
|
|
|
|
bool
|
|
QPDFFormFieldObjectHelper::isText()
|
|
{
|
|
return (getFieldType() == "/Tx");
|
|
}
|
|
|
|
bool
|
|
QPDFFormFieldObjectHelper::isCheckbox()
|
|
{
|
|
return ((getFieldType() == "/Btn") &&
|
|
((getFlags() & (ff_btn_radio | ff_btn_pushbutton)) == 0));
|
|
}
|
|
|
|
bool
|
|
QPDFFormFieldObjectHelper::isRadioButton()
|
|
{
|
|
return ((getFieldType() == "/Btn") &&
|
|
((getFlags() & ff_btn_radio) == ff_btn_radio));
|
|
}
|
|
|
|
bool
|
|
QPDFFormFieldObjectHelper::isPushbutton()
|
|
{
|
|
return ((getFieldType() == "/Btn") &&
|
|
((getFlags() & ff_btn_pushbutton) == ff_btn_pushbutton));
|
|
}
|
|
|
|
bool
|
|
QPDFFormFieldObjectHelper::isChoice()
|
|
{
|
|
return (getFieldType() == "/Ch");
|
|
}
|
|
|
|
std::vector<std::string>
|
|
QPDFFormFieldObjectHelper::getChoices()
|
|
{
|
|
std::vector<std::string> result;
|
|
if (! isChoice())
|
|
{
|
|
return result;
|
|
}
|
|
QPDFObjectHandle opt = getInheritableFieldValue("/Opt");
|
|
if (opt.isArray())
|
|
{
|
|
int n = opt.getArrayNItems();
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
QPDFObjectHandle item = opt.getArrayItem(i);
|
|
if (item.isString())
|
|
{
|
|
result.push_back(item.getUTF8Value());
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::setFieldAttribute(
|
|
std::string const& key, QPDFObjectHandle value)
|
|
{
|
|
this->oh.replaceKey(key, value);
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::setFieldAttribute(
|
|
std::string const& key, std::string const& utf8_value)
|
|
{
|
|
this->oh.replaceKey(key, QPDFObjectHandle::newUnicodeString(utf8_value));
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::setV(
|
|
QPDFObjectHandle value, bool need_appearances)
|
|
{
|
|
if (getFieldType() == "/Btn")
|
|
{
|
|
if (isCheckbox())
|
|
{
|
|
bool okay = false;
|
|
if (value.isName())
|
|
{
|
|
std::string name = value.getName();
|
|
if ((name == "/Yes") || (name == "/Off"))
|
|
{
|
|
okay = true;
|
|
setCheckBoxValue((name == "/Yes"));
|
|
}
|
|
}
|
|
if (! okay)
|
|
{
|
|
this->oh.warnIfPossible(
|
|
"ignoring attempt to set a checkbox field to a"
|
|
" value of other than /Yes or /Off");
|
|
}
|
|
}
|
|
else if (isRadioButton())
|
|
{
|
|
if (value.isName())
|
|
{
|
|
setRadioButtonValue(value);
|
|
}
|
|
else
|
|
{
|
|
this->oh.warnIfPossible(
|
|
"ignoring attempt to set a radio button field to"
|
|
" an object that is not a name");
|
|
}
|
|
}
|
|
else if (isPushbutton())
|
|
{
|
|
this->oh.warnIfPossible(
|
|
"ignoring attempt set the value of a pushbutton field");
|
|
}
|
|
return;
|
|
}
|
|
if (value.isString())
|
|
{
|
|
setFieldAttribute(
|
|
"/V", QPDFObjectHandle::newUnicodeString(value.getUTF8Value()));
|
|
}
|
|
else
|
|
{
|
|
setFieldAttribute("/V", value);
|
|
}
|
|
if (need_appearances)
|
|
{
|
|
QPDF* qpdf = this->oh.getOwningQPDF();
|
|
if (! qpdf)
|
|
{
|
|
throw std::logic_error(
|
|
"QPDFFormFieldObjectHelper::setV called with"
|
|
" need_appearances = true on an object that is"
|
|
" not associated with an owning QPDF");
|
|
}
|
|
QPDFAcroFormDocumentHelper(*qpdf).setNeedAppearances(true);
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::setV(
|
|
std::string const& utf8_value, bool need_appearances)
|
|
{
|
|
setV(QPDFObjectHandle::newUnicodeString(utf8_value),
|
|
need_appearances);
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::setRadioButtonValue(QPDFObjectHandle name)
|
|
{
|
|
// Set the value of a radio button field. This has the following
|
|
// specific behavior:
|
|
// * If this is a radio button field that has a parent that is
|
|
// also a radio button field and has no explicit /V, call itself
|
|
// on the parent
|
|
// * If this is a radio button field with children, set /V to the
|
|
// given value. Then, for each child, if the child has the
|
|
// specified value as one of its keys in the /N subdictionary of
|
|
// its /AP (i.e. its normal appearance stream dictionary), set
|
|
// /AS to name; otherwise, if /Off is a member, set /AS to /Off.
|
|
// Note that we never turn on /NeedAppearances when setting a
|
|
// radio button field.
|
|
QPDFObjectHandle parent = this->oh.getKey("/Parent");
|
|
if (parent.isDictionary() && parent.getKey("/Parent").isNull())
|
|
{
|
|
QPDFFormFieldObjectHelper ph(parent);
|
|
if (ph.isRadioButton())
|
|
{
|
|
// This is most likely one of the individual buttons. Try
|
|
// calling on the parent.
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper set parent radio button");
|
|
ph.setRadioButtonValue(name);
|
|
return;
|
|
}
|
|
}
|
|
|
|
QPDFObjectHandle kids = this->oh.getKey("/Kids");
|
|
if (! (isRadioButton() && parent.isNull() && kids.isArray()))
|
|
{
|
|
this->oh.warnIfPossible("don't know how to set the value"
|
|
" of this field as a radio button");
|
|
return;
|
|
}
|
|
setFieldAttribute("/V", name);
|
|
int nkids = kids.getArrayNItems();
|
|
for (int i = 0; i < nkids; ++i)
|
|
{
|
|
QPDFObjectHandle kid = kids.getArrayItem(i);
|
|
QPDFObjectHandle AP = kid.getKey("/AP");
|
|
QPDFObjectHandle annot;
|
|
if (AP.isNull())
|
|
{
|
|
// The widget may be below. If there is more than one,
|
|
// just find the first one.
|
|
QPDFObjectHandle grandkids = kid.getKey("/Kids");
|
|
if (grandkids.isArray())
|
|
{
|
|
int ngrandkids = grandkids.getArrayNItems();
|
|
for (int j = 0; j < ngrandkids; ++j)
|
|
{
|
|
QPDFObjectHandle grandkid = grandkids.getArrayItem(j);
|
|
AP = grandkid.getKey("/AP");
|
|
if (! AP.isNull())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper radio button grandkid widget");
|
|
annot = grandkid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
annot = kid;
|
|
}
|
|
if (! annot.isInitialized())
|
|
{
|
|
QTC::TC("qpdf", "QPDFObjectHandle broken radio button");
|
|
this->oh.warnIfPossible(
|
|
"unable to set the value of this radio button");
|
|
continue;
|
|
}
|
|
if (AP.isDictionary() &&
|
|
AP.getKey("/N").isDictionary() &&
|
|
AP.getKey("/N").hasKey(name.getName()))
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn on radio button");
|
|
annot.replaceKey("/AS", name);
|
|
}
|
|
else
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn off radio button");
|
|
annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off"));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::setCheckBoxValue(bool value)
|
|
{
|
|
// Set /AS to /Yes or /Off in addition to setting /V.
|
|
QPDFObjectHandle name =
|
|
QPDFObjectHandle::newName(value ? "/Yes" : "/Off");
|
|
setFieldAttribute("/V", name);
|
|
QPDFObjectHandle AP = this->oh.getKey("/AP");
|
|
QPDFObjectHandle annot;
|
|
if (AP.isNull())
|
|
{
|
|
// The widget may be below. If there is more than one, just
|
|
// find the first one.
|
|
QPDFObjectHandle kids = this->oh.getKey("/Kids");
|
|
if (kids.isArray())
|
|
{
|
|
int nkids = kids.getArrayNItems();
|
|
for (int i = 0; i < nkids; ++i)
|
|
{
|
|
QPDFObjectHandle kid = kids.getArrayItem(i);
|
|
AP = kid.getKey("/AP");
|
|
if (! AP.isNull())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget");
|
|
annot = kid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
annot = this->oh;
|
|
}
|
|
if (! annot.isInitialized())
|
|
{
|
|
QTC::TC("qpdf", "QPDFObjectHandle broken checkbox");
|
|
this->oh.warnIfPossible(
|
|
"unable to set the value of this checkbox");
|
|
return;
|
|
}
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS");
|
|
annot.replaceKey("/AS", name);
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper& aoh)
|
|
{
|
|
std::string ft = getFieldType();
|
|
// Ignore field types we don't know how to generate appearances
|
|
// for. Button fields don't really need them -- see code in
|
|
// QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded.
|
|
if ((ft == "/Tx") || (ft == "/Ch"))
|
|
{
|
|
generateTextAppearance(aoh);
|
|
}
|
|
}
|
|
|
|
class ValueSetter: public QPDFObjectHandle::TokenFilter
|
|
{
|
|
public:
|
|
ValueSetter(std::string const& DA, std::string const& V,
|
|
std::vector<std::string> const& opt, double tf,
|
|
QPDFObjectHandle::Rectangle const& bbox);
|
|
virtual ~ValueSetter()
|
|
{
|
|
}
|
|
virtual void handleToken(QPDFTokenizer::Token const&);
|
|
virtual void handleEOF();
|
|
void writeAppearance();
|
|
|
|
private:
|
|
std::string DA;
|
|
std::string V;
|
|
std::vector<std::string> opt;
|
|
double tf;
|
|
QPDFObjectHandle::Rectangle bbox;
|
|
enum { st_top, st_bmc, st_emc, st_end } state;
|
|
bool replaced;
|
|
};
|
|
|
|
ValueSetter::ValueSetter(std::string const& DA, std::string const& V,
|
|
std::vector<std::string> const& opt, double tf,
|
|
QPDFObjectHandle::Rectangle const& bbox) :
|
|
DA(DA),
|
|
V(V),
|
|
opt(opt),
|
|
tf(tf),
|
|
bbox(bbox),
|
|
state(st_top),
|
|
replaced(false)
|
|
{
|
|
}
|
|
|
|
void
|
|
ValueSetter::handleToken(QPDFTokenizer::Token const& token)
|
|
{
|
|
QPDFTokenizer::token_type_e ttype = token.getType();
|
|
std::string value = token.getValue();
|
|
bool do_replace = false;
|
|
switch (state)
|
|
{
|
|
case st_top:
|
|
writeToken(token);
|
|
if ((ttype == QPDFTokenizer::tt_word) && (value == "BMC"))
|
|
{
|
|
state = st_bmc;
|
|
}
|
|
break;
|
|
|
|
case st_bmc:
|
|
if ((ttype == QPDFTokenizer::tt_space) ||
|
|
(ttype == QPDFTokenizer::tt_comment))
|
|
{
|
|
writeToken(token);
|
|
}
|
|
else
|
|
{
|
|
state = st_emc;
|
|
}
|
|
// fall through to emc
|
|
|
|
case st_emc:
|
|
if ((ttype == QPDFTokenizer::tt_word) && (value == "EMC"))
|
|
{
|
|
do_replace = true;
|
|
state = st_end;
|
|
}
|
|
break;
|
|
|
|
case st_end:
|
|
writeToken(token);
|
|
break;
|
|
}
|
|
if (do_replace)
|
|
{
|
|
writeAppearance();
|
|
}
|
|
}
|
|
|
|
void
|
|
ValueSetter::handleEOF()
|
|
{
|
|
if (! this->replaced)
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper replaced BMC at EOF");
|
|
write("/Tx BMC\n");
|
|
writeAppearance();
|
|
}
|
|
}
|
|
|
|
void
|
|
ValueSetter::writeAppearance()
|
|
{
|
|
this->replaced = true;
|
|
|
|
// This code does not take quadding into consideration because
|
|
// doing so requires font metric information, which we don't
|
|
// have in many cases.
|
|
|
|
double tfh = 1.2 * tf;
|
|
int dx = 1;
|
|
|
|
// Write one or more lines, centered vertically, possibly with
|
|
// one row highlighted.
|
|
|
|
size_t max_rows = static_cast<size_t>((bbox.ury - bbox.lly) / tfh);
|
|
bool highlight = false;
|
|
size_t highlight_idx = 0;
|
|
|
|
std::vector<std::string> lines;
|
|
if (opt.empty() || (max_rows < 2))
|
|
{
|
|
lines.push_back(V);
|
|
}
|
|
else
|
|
{
|
|
// Figure out what rows to write
|
|
size_t nopt = opt.size();
|
|
size_t found_idx = 0;
|
|
bool found = false;
|
|
for (found_idx = 0; found_idx < nopt; ++found_idx)
|
|
{
|
|
if (opt.at(found_idx) == V)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
{
|
|
// Try to make the found item the second one, but
|
|
// adjust for under/overflow.
|
|
int wanted_first = QIntC::to_int(found_idx) - 1;
|
|
int wanted_last = QIntC::to_int(found_idx + max_rows) - 2;
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper list found");
|
|
while (wanted_first < 0)
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper list first too low");
|
|
++wanted_first;
|
|
++wanted_last;
|
|
}
|
|
while (wanted_last >= QIntC::to_int(nopt))
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper list last too high");
|
|
if (wanted_first > 0)
|
|
{
|
|
--wanted_first;
|
|
}
|
|
--wanted_last;
|
|
}
|
|
highlight = true;
|
|
highlight_idx = found_idx - QIntC::to_size(wanted_first);
|
|
for (size_t i = QIntC::to_size(wanted_first);
|
|
i <= QIntC::to_size(wanted_last); ++i)
|
|
{
|
|
lines.push_back(opt.at(i));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper list not found");
|
|
// include our value and the first n-1 rows
|
|
highlight_idx = 0;
|
|
highlight = true;
|
|
lines.push_back(V);
|
|
for (size_t i = 0; ((i < nopt) && (i < (max_rows - 1))); ++i)
|
|
{
|
|
lines.push_back(opt.at(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write the lines centered vertically, highlighting if needed
|
|
size_t nlines = lines.size();
|
|
double dy = bbox.ury - ((bbox.ury - bbox.lly -
|
|
(static_cast<double>(nlines) * tfh)) / 2.0);
|
|
if (highlight)
|
|
{
|
|
write("q\n0.85 0.85 0.85 rg\n" +
|
|
QUtil::double_to_string(bbox.llx) + " " +
|
|
QUtil::double_to_string(
|
|
bbox.lly + dy -
|
|
(tfh * (static_cast<double>(highlight_idx + 1)))) + " " +
|
|
QUtil::double_to_string(bbox.urx - bbox.llx) + " " +
|
|
QUtil::double_to_string(tfh) +
|
|
" re f\nQ\n");
|
|
}
|
|
dy -= tf;
|
|
write("q\nBT\n" + DA + "\n");
|
|
for (size_t i = 0; i < nlines; ++i)
|
|
{
|
|
// We could adjust Tm to translate to the beginning the first
|
|
// line, set TL to tfh, and use T* for each subsequent line,
|
|
// but doing this would require extracting any Tm from DA,
|
|
// which doesn't seem really worth the effort.
|
|
if (i == 0)
|
|
{
|
|
write(QUtil::double_to_string(bbox.llx + static_cast<double>(dx)) +
|
|
" " +
|
|
QUtil::double_to_string(bbox.lly + static_cast<double>(dy)) +
|
|
" Td\n");
|
|
}
|
|
else
|
|
{
|
|
write("0 " + QUtil::double_to_string(-tfh) + " Td\n");
|
|
}
|
|
write(QPDFObjectHandle::newString(lines.at(i)).unparse() + " Tj\n");
|
|
}
|
|
write("ET\nQ\nEMC");
|
|
}
|
|
|
|
class TfFinder: public QPDFObjectHandle::TokenFilter
|
|
{
|
|
public:
|
|
TfFinder();
|
|
virtual ~TfFinder()
|
|
{
|
|
}
|
|
virtual void handleToken(QPDFTokenizer::Token const&);
|
|
double getTf();
|
|
std::string getFontName();
|
|
std::string getDA();
|
|
|
|
private:
|
|
double tf;
|
|
size_t tf_idx;
|
|
std::string font_name;
|
|
double last_num;
|
|
size_t last_num_idx;
|
|
std::string last_name;
|
|
std::vector<std::string> DA;
|
|
};
|
|
|
|
TfFinder::TfFinder() :
|
|
tf(11.0),
|
|
tf_idx(0),
|
|
last_num(0.0),
|
|
last_num_idx(0)
|
|
{
|
|
}
|
|
|
|
void
|
|
TfFinder::handleToken(QPDFTokenizer::Token const& token)
|
|
{
|
|
QPDFTokenizer::token_type_e ttype = token.getType();
|
|
std::string value = token.getValue();
|
|
DA.push_back(token.getRawValue());
|
|
switch (ttype)
|
|
{
|
|
case QPDFTokenizer::tt_integer:
|
|
case QPDFTokenizer::tt_real:
|
|
last_num = strtod(value.c_str(), 0);
|
|
last_num_idx = DA.size() - 1;
|
|
break;
|
|
|
|
case QPDFTokenizer::tt_name:
|
|
last_name = value;
|
|
break;
|
|
|
|
case QPDFTokenizer::tt_word:
|
|
if ((value == "Tf") &&
|
|
(last_num > 1.0) &&
|
|
(last_num < 1000.0))
|
|
{
|
|
// These ranges are arbitrary but keep us from doing
|
|
// insane things or suffering from over/underflow
|
|
tf = last_num;
|
|
}
|
|
tf_idx = last_num_idx;
|
|
font_name = last_name;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
double
|
|
TfFinder::getTf()
|
|
{
|
|
return this->tf;
|
|
}
|
|
|
|
std::string
|
|
TfFinder::getDA()
|
|
{
|
|
std::string result;
|
|
size_t n = this->DA.size();
|
|
for (size_t i = 0; i < n; ++i)
|
|
{
|
|
std::string cur = this->DA.at(i);
|
|
if (i == tf_idx)
|
|
{
|
|
double delta = strtod(cur.c_str(), 0) - this->tf;
|
|
if ((delta > 0.001) || (delta < -0.001))
|
|
{
|
|
// tf doesn't match the font size passed to Tf, so
|
|
// substitute.
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper fallback Tf");
|
|
cur = QUtil::double_to_string(tf);
|
|
}
|
|
}
|
|
result += cur;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string
|
|
TfFinder::getFontName()
|
|
{
|
|
return this->font_name;
|
|
}
|
|
|
|
QPDFObjectHandle
|
|
QPDFFormFieldObjectHelper::getFontFromResource(
|
|
QPDFObjectHandle resources, std::string const& name)
|
|
{
|
|
QPDFObjectHandle result;
|
|
if (resources.isDictionary() &&
|
|
resources.getKey("/Font").isDictionary() &&
|
|
resources.getKey("/Font").hasKey(name))
|
|
{
|
|
result = resources.getKey("/Font").getKey(name);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void
|
|
QPDFFormFieldObjectHelper::generateTextAppearance(
|
|
QPDFAnnotationObjectHelper& aoh)
|
|
{
|
|
QPDFObjectHandle AS = aoh.getAppearanceStream("/N");
|
|
if (AS.isNull())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper create AS from scratch");
|
|
QPDFObjectHandle::Rectangle rect = aoh.getRect();
|
|
QPDFObjectHandle::Rectangle bbox(
|
|
0, 0, rect.urx - rect.llx, rect.ury - rect.lly);
|
|
QPDFObjectHandle dict = QPDFObjectHandle::parse(
|
|
"<< /Resources << /ProcSet [ /PDF /Text ] >>"
|
|
" /Type /XObject /Subtype /Form >>");
|
|
dict.replaceKey("/BBox", QPDFObjectHandle::newFromRectangle(bbox));
|
|
AS = QPDFObjectHandle::newStream(
|
|
this->oh.getOwningQPDF(), "/Tx BMC\nEMC\n");
|
|
AS.replaceDict(dict);
|
|
QPDFObjectHandle AP = aoh.getAppearanceDictionary();
|
|
if (AP.isNull())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper create AP from scratch");
|
|
aoh.getObjectHandle().replaceKey(
|
|
"/AP", QPDFObjectHandle::newDictionary());
|
|
AP = aoh.getAppearanceDictionary();
|
|
}
|
|
AP.replaceKey("/N", AS);
|
|
}
|
|
if (! AS.isStream())
|
|
{
|
|
aoh.getObjectHandle().warnIfPossible(
|
|
"unable to get normal appearance stream for update");
|
|
return;
|
|
}
|
|
QPDFObjectHandle bbox_obj = AS.getDict().getKey("/BBox");
|
|
if (! bbox_obj.isRectangle())
|
|
{
|
|
aoh.getObjectHandle().warnIfPossible(
|
|
"unable to get appearance stream bounding box");
|
|
return;
|
|
}
|
|
QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
|
|
std::string DA = getDefaultAppearance();
|
|
std::string V = getValueAsString();
|
|
std::vector<std::string> opt;
|
|
if (isChoice() && ((getFlags() & ff_ch_combo) == 0))
|
|
{
|
|
opt = getChoices();
|
|
}
|
|
|
|
TfFinder tff;
|
|
Pl_QPDFTokenizer tok("tf", &tff);
|
|
tok.write(QUtil::unsigned_char_pointer(DA.c_str()), DA.length());
|
|
tok.finish();
|
|
double tf = tff.getTf();
|
|
DA = tff.getDA();
|
|
|
|
std::string (*encoder)(std::string const&, char) = &QUtil::utf8_to_ascii;
|
|
std::string font_name = tff.getFontName();
|
|
if (! font_name.empty())
|
|
{
|
|
// See if the font is encoded with something we know about.
|
|
QPDFObjectHandle resources = AS.getDict().getKey("/Resources");
|
|
QPDFObjectHandle font = getFontFromResource(resources, font_name);
|
|
bool found_font_in_dr = false;
|
|
if (! font.isInitialized())
|
|
{
|
|
QPDFObjectHandle dr = getDefaultResources();
|
|
font = getFontFromResource(dr, font_name);
|
|
found_font_in_dr = (font.isInitialized() && font.isDictionary());
|
|
}
|
|
if (found_font_in_dr && resources.isDictionary())
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper get font from /DR");
|
|
if (resources.isIndirect())
|
|
{
|
|
resources = resources.getOwningQPDF()->makeIndirectObject(
|
|
resources.shallowCopy());
|
|
AS.getDict().replaceKey("/Resources", resources);
|
|
}
|
|
// Use mergeResources to force /Font to be local
|
|
resources.mergeResources(
|
|
QPDFObjectHandle::parse("<< /Font << >> >>"));
|
|
resources.getKey("/Font").replaceKey(font_name, font);
|
|
}
|
|
|
|
if (font.isInitialized() &&
|
|
font.isDictionary() &&
|
|
font.getKey("/Encoding").isName())
|
|
{
|
|
std::string encoding = font.getKey("/Encoding").getName();
|
|
if (encoding == "/WinAnsiEncoding")
|
|
{
|
|
QTC::TC("qpdf", "QPDFFormFieldObjectHelper WinAnsi");
|
|
encoder = &QUtil::utf8_to_win_ansi;
|
|
}
|
|
else if (encoding == "/MacRomanEncoding")
|
|
{
|
|
encoder = &QUtil::utf8_to_mac_roman;
|
|
}
|
|
}
|
|
}
|
|
|
|
V = (*encoder)(V, '?');
|
|
for (size_t i = 0; i < opt.size(); ++i)
|
|
{
|
|
opt.at(i) = (*encoder)(opt.at(i), '?');
|
|
}
|
|
|
|
AS.addTokenFilter(new ValueSetter(DA, V, opt, tf, bbox));
|
|
}
|