Add special case setV code for button fields

This commit is contained in:
Jay Berkenbilt 2019-01-03 17:00:56 -05:00
parent 1342612308
commit b55567a0fa
11 changed files with 15458 additions and 5 deletions

View File

@ -163,10 +163,13 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
void setFieldAttribute(std::string const& key,
std::string const& utf8_value);
// Set /V (field value) to the given value. Optionally set
// /NeedAppearances to true. You can explicitly tell this method
// not to set /NeedAppearances if you are going to explicitly
// generate an appearance stream yourself.
// Set /V (field value) to the given value. If need_appearances is
// true and the field type is either /Tx (text) or /Ch (choice),
// set /NeedAppearances to true. You can explicitly tell this
// method not to set /NeedAppearances if you are going to generate
// an appearance stream yourself. Starting with qpdf 8.3.0, this
// method handles fields of type /Btn (checkboxes, radio buttons,
// pushbuttons) specially.
QPDF_DLL
void setV(QPDFObjectHandle value, bool need_appearances = true);
@ -177,6 +180,9 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
void setV(std::string const& utf8_value, bool need_appearances = true);
private:
void setRadioButtonValue(QPDFObjectHandle name);
void setCheckBoxValue(bool value);
class Members
{
friend class QPDFFormFieldObjectHelper;

View File

@ -272,6 +272,47 @@ 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;
}
setFieldAttribute("/V", value);
if (need_appearances)
{
@ -294,3 +335,138 @@ QPDFFormFieldObjectHelper::setV(
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 childen, 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);
}

View File

@ -390,3 +390,11 @@ QPDFObjectHandle replace with copy 0
QPDFPageDocumentHelper indirect as resources 0
QPDFAnnotationObjectHelper forbidden flags 0
QPDFAnnotationObjectHelper missing required flags 0
QPDFFormFieldObjectHelper set parent radio button 0
QPDFFormFieldObjectHelper radio button grandkid widget 0
QPDFFormFieldObjectHelper turn on radio button 0
QPDFFormFieldObjectHelper turn off radio button 0
QPDFFormFieldObjectHelper checkbox kid widget 0
QPDFObjectHandle broken radio button 0
QPDFFormFieldObjectHelper set checkbox AS 0
QPDFObjectHandle broken checkbox 0

View File

@ -188,7 +188,7 @@ my @form_tests = (
'form-errors',
);
$n_tests += scalar(@form_tests) + 2;
$n_tests += scalar(@form_tests) + 6;
# Many of the form*.pdf files were created by converting the
# LibreOffice document storage/form.odt to PDF and then manually
@ -216,6 +216,22 @@ $td->runtest("compare files",
{$td->FILE => "a.pdf"},
{$td->FILE => "form-no-need-appearances-filled.pdf"});
$td->runtest("button fields",
{$td->COMMAND => "test_driver 51 button-set.pdf"},
{$td->FILE => "button-set.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("compare files",
{$td->FILE => "a.pdf"},
{$td->FILE => "button-set-out.pdf"});
$td->runtest("broken button fields",
{$td->COMMAND => "test_driver 51 button-set-broken.pdf"},
{$td->FILE => "button-set-broken.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("compare files",
{$td->FILE => "a.pdf"},
{$td->FILE => "button-set-broken-out.pdf"});
show_ntests();
# ----------
$td->notify("--- Stream Replacement Tests ---");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
setting r1 via parent
WARNING: button-set-broken.pdf, object 5 0 at offset 995: unable to set the value of this radio button
turning checkbox1 on
turning checkbox2 off
WARNING: button-set-broken.pdf, object 7 0 at offset 1354: unable to set the value of this checkbox
setting r2 via child
test 51 done

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
setting r1 via parent
turning checkbox1 on
turning checkbox2 off
setting r2 via child
test 51 done

File diff suppressed because it is too large Load Diff

View File

@ -1771,6 +1771,55 @@ void runtest(int n, char const* filename1, char const* arg2)
std::cout << *iter << std::endl;
}
}
else if (n == 51)
{
// Test radio button and checkbox field setting. The input
// files must have radios button called r1 and r2 and
// checkboxes called checkbox1 and checkbox2. The files
// button-set*.pdf are designed for this test case.
QPDFObjectHandle acroform = pdf.getRoot().getKey("/AcroForm");
QPDFObjectHandle fields = acroform.getKey("/Fields");
int n = fields.getArrayNItems();
for (int i = 0; i < n; ++i)
{
QPDFObjectHandle field = fields.getArrayItem(i);
QPDFObjectHandle T = field.getKey("/T");
if (! T.isString())
{
continue;
}
std::string Tval = T.getUTF8Value();
if (Tval == "r1")
{
std::cout << "setting r1 via parent\n";
QPDFFormFieldObjectHelper foh(field);
foh.setV(QPDFObjectHandle::newName("/2"));
}
else if (Tval == "r2")
{
std::cout << "setting r2 via child\n";
field = field.getKey("/Kids").getArrayItem(1);
QPDFFormFieldObjectHelper foh(field);
foh.setV(QPDFObjectHandle::newName("/3"));
}
else if (Tval == "checkbox1")
{
std::cout << "turning checkbox1 on\n";
QPDFFormFieldObjectHelper foh(field);
foh.setV(QPDFObjectHandle::newName("/Yes"));
}
else if (Tval == "checkbox2")
{
std::cout << "turning checkbox2 off\n";
QPDFFormFieldObjectHelper foh(field);
foh.setV(QPDFObjectHandle::newName("/Off"));
}
}
QPDFWriter w(pdf, "a.pdf");
w.setQDFMode(true);
w.setStaticID(true);
w.write();
}
else
{
throw std::runtime_error(std::string("invalid test ") +