2

I've been trying for days to get a CheckBox or Radio Button to render using PDF::API2 and haven't been able to.

I've poured over the PDFMark reference, PDF specification, and any examples I've been able to find. I can get simple Widget annotations to render, but haven't been able to get anything that requires an appearance stream or appearance dictionary to work correctly. Below is a selection of test code attempting to set up a checkbox:

#!/usr/bin/perl

use PDF::API2;
use PDF::API2::Basic::PDF::Utils;

# set up pdf
my $pdfOptions = {};
my $pdf = PDF::API2->new( \$pdfOptions );
my $page = $pdf->page();

$page->mediabox( 'Letter' );

my $AcroForm = PDFDict();
$AcroForm->{NeedAppearances} = PDFBool( 'true' );
$AcroForm->realise;

my @Annots;
my @Fields;

my $resourceObj = PDFDict();
$resourceObj->{Type}     = PDFName( 'Font' );
$resourceObj->{Subtype}  = PDFName( 'Type1' );
$resourceObj->{Name}     = PDFName( 'ZaDb' );
$resourceObj->{BaseFont} = PDFName( 'ZapfDingbats' );
$resourceObj->realise();


$AcroForm->{DR} = PDFDict();
$AcroForm->{DR}->{Font} = PDFDict();
$AcroForm->{DR}->{ZaDb} = $resourceObj;
$AcroForm->realise();

my $item = PDFDict();

$item->{P}   = $page;
$item->{Type}    = PDFName( 'Annot' );
$item->{Subtype} = PDFName( 'Widget' );
$item->{FT}  = PDFName( 'Btn' );

my $yes = PDFName( 'Yes' );
my $off = PDFName( 'Off' );

$item->{P}   = $page;
$item->{Type}    = PDFName( 'Annot' );
$item->{Subtype} = PDFName( 'Widget' );
$item->{Rect}    = PDF::API2::Basic::PDF::Literal->new( "[100 300 200 400]" );
$item->{FT}  = PDFName( 'Btn' );
$item->{T}   = PDFStr( 'Urgent' );
$item->{V}   = PDFName( 'Yes' );
$item->{AS}  = PDFName( 'Yes' );
$item->{AP}  = PDFDict();
$item->{AP}->{N} = PDFDict();

# My understanding is that these would be nulled to be used with NeedAppearances
$item->{AP}->{N}->{$yes} = PDFNull(); 
$item->{AP}->{N}->{$off} = PDFNull();

$item->realise();

push @Annots, $item;
push @Fields, $item if( $AcroForm );

$page->{Annots} = PDFArray( @Annots );
$AcroForm->{Fields} = PDFArray(@Fields) if( $AcroForm );
$pdf->{Root}->{AcroForm} = $AcroForm if( $AcroForm );

print $pdf->stringify();
exit;

I would expect to see a checkbox rendered towards the middle of this page, instead I get an empty, unusable annotation. I'm trying to get the NeedAppearances flag to work, as I'd given up attempting a proper appearance stream/appearance dictionary, but I would be grateful for solutions that use either method.

E-Rock
  • 180
  • 7
  • I am not familar with PDF specificaton, but using a `PDFName()` as a hash key seems wrong. For example `$item->{AP}->{N}->{$off} = PDFNull()` where `$off` is a `PDFName()`. – Håkon Hægland Jun 25 '19 at 20:58
  • Looking at [the source](https://metacpan.org/release/PDF-API2/source/lib/PDF/API2.pm#L1541) it seems `$pdf->{Root}->{AcroForm} = ...` should be `$pdf->{pdf}{Root}{AcroForm} = ...` – Håkon Hægland Jun 25 '19 at 22:29
  • I think you may be correct about the placement of the AcroForm. I'll try that soon. As far as using the PDFName as the hash key, I'm at a point where I've tried both ways and was unsuccessful with both. The resulting PDFMarkup appeared correct but I'll look at this as a possible point of failure as well. – E-Rock Jun 25 '19 at 22:42
  • Here's where I am now: when opened in Adobe Acrobat, the annotation functions but is invisible when selected. This is with my original placement of the AcroForm, and with all entries in $item->{AP} commented out. The annotation still doesn't function when opened with the browser, whereas my /Annot /Widget /Tx annotations work perfectly in both the browser and in the PDF reader. I had also removed the NeedAppearances flag, so I'm at a loss as to where the appearance stream is being generated from. I've also commented out the default resources in DR. – E-Rock Jun 25 '19 at 22:48
  • 1
    The solution: indeed I needed to move the AcroForm, I also needed to populate a couple of entries in the item properties so that readers would have enough information to build the appearances itself. In particular, the button I wanted to render needed a {' stream'} entry that was an empty string. I'm assuming this forced the module to include the stream / endstream tags, which allowed the PDF reader to insert its own appearance streams. I'm still not sure how to set up a proper appearance stream, but this works for now. – E-Rock Jun 27 '19 at 15:21

2 Answers2

2

This is the code I finally got to render correctly, both in the browser and in Adobe Reader. Posting this because working examples of using this module for widget annotations are sparse.

The trick was: using {pdf}->new_obj to define objects and capture their references, as well as proper placement of the AcroForm, and finally, setting the {' stream'} property to an empty string, which I suppose forced the rendering of the stream / endstream tags, allowing the PDF readers an anchor in which to insert their appearance streams.

I used qpdf to analyze my output, which allowed me to see how the module's various methods were affecting the final PDF output.

    #!/usr/bin/perl

    # set up pdf
    my $pdfOptions = {};
    my $pdf = PDF::API2->new( \$pdfOptions );
    my $page = $pdf->page();

    $page->mediabox( 'Letter' );

    my @Annots;
    my @Fields;

    my $fontObj = PDFDict();
    $fontObj->realise();
    $fontObj->{Type} = PDFName( 'Font' );
    $fontObj->{Subtype} = PDFName( 'Type1' );
    $fontObj->{BaseFont} = PDFName( 'Times-Roman' );
    $fontObj = $pdf->{pdf}->new_obj( $fontObj );

    my $resourceObj = PDFDict();
    $resourceObj->realise();

    $resourceObj->{Font} = PDFDict();
    $resourceObj->{Font}->realise();

    $resourceObj->{Font}->{F1} = $fontObj;
    $resourceObj = $pdf->{pdf}->new_obj( $resourceObj );

    my $AcroForm = PDFDict();
    $AcroForm->realise();
    $AcroForm->{DR} = $resourceObj;
    $AcroForm->{NeedAppearances} = PDFBool( 'true' );

    my $yesObj = PDF::API2::Resource::XObject::Form->new( $pdf );
    $yesObj->{Resources} = $resourceObj;
    $yesObj->{BBox} = PDF::API2::Basic::PDF::Literal->new( "[100 300 200 400]" );
    $yesObj->realise();
    $yesObj->{' stream'} = '';
    $yesObj = $pdf->{pdf}->new_obj( $yesObj );

    my $noObj = PDF::API2::Resource::XObject::Form->new( $pdf );
    $noObj->{Resources} = $resourceObj;
    $noObj->{Subtype} = PDFName( 'Form' );
    $noObj->{BBox} = PDF::API2::Basic::PDF::Literal->new( "[100 300 200 400]" );
    $noObj->realise();
    $noObj->{' stream'} = '';
    $noObj = $pdf->{pdf}->new_obj( $noObj );

    my $item = PDFDict();
    $item->{Type}    = PDFName( 'Annot' );
    $item->{Subtype} = PDFName( 'Widget' );
    $item->{FT}      = PDFName( 'Btn' );
    $item->{T}       = PDFStr( 'checkbox1' );
    $item->{V}       = PDFName( 'Yes' );
    $item->{P}       = $page;
    $item->{Rect}    = PDF::API2::Basic::PDF::Literal->new( "[100 300 200 400]" );
    $item->{H}       = PDFName( 'N' );
    $item->{AS} = PDFName('Yes');

    $item->{AP}      = PDFDict();
    $item->{AP}->realise();

    $item->{AP}->{N} = PDFDict();
    $item->{AP}->{N}->realise();
    $item->{AP}->{N}->{'Yes'} = $yesObj;
    $item->{AP}->{N}->{'Off'} = $noObj;

    $item = $pdf->{pdf}->new_obj( $item );

    $item->realise();

    push @Annots, $item;
    push @Fields, $item if( $AcroForm );

    $page->{Annots} = PDFArray( @Annots );
    $AcroForm->{Fields} = PDFArray(@Fields) if( $AcroForm );
    $pdf->{catalog}->{'AcroForm'} = $AcroForm;
    $pdf->{pdf}->out_obj($pdf->{catalog});

    print $pdf->stringify();
    exit;
E-Rock
  • 180
  • 7
  • 1
    Thanks for taking the time to figure this out! I tried your code and unfortunately it did not work here. Specifically, `PDF::API2::Resource::XObject::Form->new( $pdf )` is failing. I think you need to replace `$pdf` with `$pdf->{pdf}` ? At least if I do this, it compiles and the resulting PDF shows a "yes" checkbox. – Håkon Hægland Jun 27 '19 at 19:09
  • You are correct, after more fiddling that did appear to be an issue. The normal text annotations rendered without the AcroForm entry so I had assumed that was all correct. – E-Rock Jul 08 '19 at 15:56
0

Following code will mark PDF checkbox

#!/usr/bin/perl -w
use strict;
use CAM::PDF;
my $pdf = CAM::PDF->new('tenant.pdf') or die "Could not open PDF ($!)!";
my @fields = $pdf->getFormFieldList();
foreach my $field ( @fields ) {
 if ($field =~ /Female/) {
  my $ff_obj    = $pdf->getFormField($field);

  $ff_obj->{value}->{value}->{AS}->{value} = "On";
  }
  else {
    $pdf->fillFormFields($field => $field);
  }
}
$pdf->cleanoutput('afilled.pdf');
brian d foy
  • 129,424
  • 31
  • 207
  • 592
nelson
  • 1