Skip to content
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

Handle indirect references in annotation dictionaries #196

Merged
merged 3 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions src/FpdfTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfNull;
use setasign\Fpdi\PdfParser\Type\PdfType;

/**
* This trait is used for the implementation of FPDI in FPDF and tFPDF.
Expand Down Expand Up @@ -142,20 +143,6 @@ protected function _putlinks($n)
$this->_put('/A <</S /URI /URI ' . $this->_textstring($pl[4]) . '>>');
if (isset($pl['importedLink'])) {
$values = $pl['importedLink']['pdfObject']->value;
unset(
$values['P'],
$values['NM'],
$values['AP'],
$values['AS'],
$values['Type'],
$values['Subtype'],
$values['Rect'],
$values['A'],
$values['QuadPoints'],
$values['Rotate'],
$values['M'],
$values['StructParent']
);

foreach ($values as $name => $entry) {
$this->_put('/' . $name . ' ', false);
Expand Down
28 changes: 28 additions & 0 deletions src/PdfParser/Type/PdfType.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,34 @@ protected static function ensureType($type, $value, $errorMessage)
return $value;
}

/**
* Flatten indirect object references to direct objects.
*
* @param PdfType $value
* @param PdfParser $parser
* @return PdfType
* @throws CrossReferenceException
* @throws PdfParserException
*/
public static function flatten(PdfType $value, PdfParser $parser)
{
if ($value instanceof PdfIndirectObjectReference) {
return self::flatten(self::resolve($value, $parser), $parser);
}

if ($value instanceof PdfDictionary || $value instanceof PdfArray) {
foreach ($value->value as $key => $_value) {
$value->value[$key] = self::flatten($_value, $parser);
}
}

if ($value instanceof PdfStream) {
throw new PdfTypeException('There is a stream object found which cannot be flattened to a direct object.');
}

return $value;
}

/**
* The value of the PDF type.
*
Expand Down
23 changes: 23 additions & 0 deletions src/PdfReader/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,29 @@ public function getExternalLinks($box = PageBoundaries::CROP_BOX)
}
}

// we remove unsupported/unneeded values here
unset(
$annotation->value['P'],
$annotation->value['NM'],
$annotation->value['AP'],
$annotation->value['AS'],
$annotation->value['Type'],
$annotation->value['Subtype'],
$annotation->value['Rect'],
$annotation->value['A'],
$annotation->value['QuadPoints'],
$annotation->value['Rotate'],
$annotation->value['M'],
$annotation->value['StructParent'],
$annotation->value['OC']
);

// ...and flatten the PDF object to eliminate any indirect references.
// Indirect references are a problem when writing the output in FPDF
// because FPDF uses pre-calculated object numbers while FPDI creates
// them at runtime.
$annotation = PdfType::flatten($annotation, $this->parser);

$links[] = [
'rect' => $normalizedRect,
'quadPoints' => $normalizedQuadPoints,
Expand Down
37 changes: 12 additions & 25 deletions src/Tcpdf/Fpdi.php
Original file line number Diff line number Diff line change
Expand Up @@ -303,21 +303,8 @@ protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeight
// ensure we have a default value - otherwise TCPDF will set it to 4 throughout
$lastAnnotationOpt['f'] = 0;

// values in this dictonary are all direct objects and we don't need to resolve them here again.
$values = $externalLink['pdfObject']->value;
unset(
$values['P'],
$values['NM'],
$values['AP'],
$values['AS'],
$values['Type'],
$values['Subtype'],
$values['Rect'],
$values['A'],
$values['QuadPoints'],
$values['Rotate'],
$values['M'],
$values['StructParent']
);

foreach ($values as $key => $value) {
try {
Expand All @@ -326,17 +313,17 @@ protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeight
$value = PdfDictionary::ensure($value);
$bs = [];
if (isset($value->value['W'])) {
$bs['w'] = PdfNumeric::ensure(PdfType::resolve($value->value['W'], $parser))->value;
$bs['w'] = PdfNumeric::ensure($value->value['W'])->value;
}

if (isset($value->value['S'])) {
$bs['s'] = PdfName::ensure(PdfType::resolve($value->value['S'], $parser))->value;
$bs['s'] = PdfName::ensure($value->value['S'])->value;
}

if (isset($value->value['D'])) {
$d = [];
foreach (PdfArray::ensure(PdfType::resolve($value->value['D'], $parser))->value as $item) {
$d[] = PdfNumeric::ensure(PdfType::resolve($item, $parser))->value;
foreach (PdfArray::ensure($value->value['D'])->value as $item) {
$d[] = PdfNumeric::ensure($item)->value;
}
$bs['d'] = $d;
}
Expand All @@ -345,20 +332,20 @@ protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeight
break;

case 'Border':
$borderArray = PdfArray::ensure(PdfType::resolve($value, $parser))->value;
$borderArray = PdfArray::ensure($value)->value;
if (count($borderArray) < 3) {
continue 2;
}

$border = [
PdfNumeric::ensure(PdfType::resolve($borderArray[0], $parser))->value,
PdfNumeric::ensure(PdfType::resolve($borderArray[1], $parser))->value,
PdfNumeric::ensure(PdfType::resolve($borderArray[2], $parser))->value,
PdfNumeric::ensure($borderArray[0])->value,
PdfNumeric::ensure($borderArray[1])->value,
PdfNumeric::ensure($borderArray[2])->value,
];
if (isset($borderArray[3])) {
$dashArray = [];
foreach (PdfArray::ensure(PdfType::resolve($borderArray[3], $parser))->value as $item) {
$dashArray[] = PdfNumeric::ensure(PdfType::resolve($item, $parser))->value;
foreach (PdfArray::ensure($borderArray[3])->value as $item) {
$dashArray[] = PdfNumeric::ensure($item)->value;
}
$border[] = $dashArray;
}
Expand All @@ -371,7 +358,7 @@ protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeight
$colors = PdfArray::ensure(PdfType::resolve($value, $parser))->value;
$m = count($colors) === 4 ? 100 : 255;
foreach ($colors as $item) {
$c[] = PdfNumeric::ensure(PdfType::resolve($item, $parser))->value * $m;
$c[] = PdfNumeric::ensure($item)->value * $m;
}
$lastAnnotationOpt['c'] = $c;
break;
Expand Down
Binary file not shown.
42 changes: 42 additions & 0 deletions tests/_files/pdfs/links/update-links.pdf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/* A simple helper script to modify links.pdf in view to indirect references in annotation dictionaries.
* We used SetaPDF-Core for doing such low-level modifications.
*/

require_once __DIR__ . '/../../../../../SetaPDF/library/SetaPDF/Autoload.php';

$writer = new SetaPDF_Core_Writer_File('links-with-indirect-references.pdf');
$document = SetaPDF_Core_Document::loadByFilename('links.pdf', $writer);

$pages = $document->getCatalog()->getPages();
$page = $pages->getPage(1);

$annotations = $page->getAnnotations();
$allAnnots = $annotations->getAll();

$dict = $allAnnots[0]->getDictionary();
$bs = $dict->getValue('BS');
$d = $bs->getValue('D');
$bs->offsetSet('D', $document->createNewObject($d));
$dict->offsetSet('BS', $document->createNewObject($bs));

$dict = $allAnnots[1]->getDictionary();
$c = $dict->getValue('C');
$object = $document->createNewObject($c);
$ref = new SetaPDF_Core_Type_IndirectReference($object, 0, $document);
$dict->offsetSet('C', $document->createNewObject($ref));


$dict = $allAnnots[5]->getDictionary();
$border = $dict->getValue('Border');
$dict->offsetSet('Border', $document->createNewObject($border));

$page = $pages->getPage(2);
$annotations = $page->getAnnotations();
$allAnnots = $annotations->getAll();

$dict = $allAnnots[0]->getDictionary();
$rect = $dict->getValue('Rect');
$dict->offsetSet('Rect', $document->createNewObject($rect));

$document->save()->finish();
17 changes: 14 additions & 3 deletions tests/functional/LinkHandling/AbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ protected function compatAssertEqualsWithDelta($expected, $actual, $message = ''
}
}


protected function save($pdf)
{
return $pdf->Output('S');
Expand Down Expand Up @@ -131,11 +130,11 @@ public function testDoNotImportLinks()
* This test simply imports a page with several links including quad-points
* and place it resized and compressed onto a new page.
*/
public function testLinkHandling1()
public function testLinkHandling1($filename = __DIR__ . '/../../_files/pdfs/links/links.pdf')
{
$pdf = $this->getInstance();
$pdf->AddPage();
$pdf->setSourceFile(__DIR__ . '/../../_files/pdfs/links/links.pdf');
$pdf->setSourceFile($filename);
$tplId = $pdf->importPage(1, PageBoundaries::CROP_BOX, true, true);
$pdf->useTemplate($tplId, [
'x' => 20,
Expand Down Expand Up @@ -235,6 +234,18 @@ public function testLinkHandling1()
return $pdfString;
}

/**
* This test imports annotations with indirect references in their properties which should be flattened.
*
* The original file links.pdf was modified appropriately.
*
* @return void
*/
public function testLinkHandlingWithIndirectReferencesInAnnotation()
{
$this->testLinkHandling1(__DIR__ . '/../../_files/pdfs/links/links-with-indirect-references.pdf');
}

/**
* Take the result of testLinkHandling1 and re-place it with the same settings.
* @depends testLinkHandling1
Expand Down
Loading