Last active
February 21, 2020 10:05
-
-
Save andreyvit/6061704 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once 'pdflib/fpdf.php'; | |
require_once 'pdflib/fpdi.php'; | |
// FPDI extension that preserves hyperlinks when copying PDF pages. | |
// | |
// (c) 2012, Andrey Tarantsov <[email protected]>, provided under the MIT license. | |
// | |
// Published at: https://gist.github.com/2020422 | |
// | |
// Note: the free version of FPDI requires unprotected PDFs conforming to spec version 1.4. | |
// I use qpdf (http://qpdf.sourceforge.net/) to preprocess PDFs before running through this | |
// code, invoking it like this: | |
// | |
// qpdf --decrypt --stream-data=uncompress --force-version=1.4 src.pdf temp.pdf | |
// | |
// then, after processing temp.pdf into out.pdf with FPDI, I run the following to re-establish | |
// protection: | |
// | |
// qpdf --encrypt "" "" 40 --extract=n -- out.pdf final.pdf | |
// | |
// Here's an example class that uses FDPI to append another PDF file, adding a message to each page: | |
// | |
// class MyPdfProcessor extends FPDI_with_annots { | |
// | |
// function concat($file, $message) { | |
// $pagecount = $this->setSourceFile($file); | |
// for ($i = 1; $i <= $pagecount; $i++) { | |
// $tplidx = $this->ImportPage($i); | |
// $s = $this->getTemplatesize($tplidx); | |
// $this->AddPage('P', array($s['w'], $s['h'])); | |
// $this->useTemplate($tplidx); | |
// | |
// // header | |
// $this->SetAutoPageBreak(FALSE); | |
// $this->SetXY(6, -28); | |
// $this->Rotate(90); | |
// $this->SetTextColor(102, 102, 102); | |
// $this->SetFont('Arial', '', 8); | |
// $this->Cell(0, 5, utf8_decode($message),'',1,'L'); | |
// $this->Rotate(0); // outputs Q to balance "q" added by the previous call to Rotate | |
// } | |
// } | |
// | |
// } | |
class FPDI_with_annots extends FPDI { | |
// default maxdepth prevents an infinite recursion on malformed PDFs (not theoretical, actually found in the wild) | |
function resolve(&$parser, $smt, $maxdepth=10) { | |
if ($maxdepth == 0) | |
return $smt; | |
if ($smt[0] == PDF_TYPE_OBJREF) { | |
$result = $parser->pdf_resolve_object($parser->c, $smt); | |
return $this->resolve($parser, $result, $maxdepth-1); | |
} else if ($smt[0] == PDF_TYPE_OBJECT) { | |
return $this->resolve($parser, $smt[1], $maxdepth-1); | |
} else if ($smt[0] == PDF_TYPE_ARRAY) { | |
$result = array(); | |
foreach ($smt[1] as $item) { | |
$result[] = $this->resolve($parser, $item, $maxdepth-1); | |
} | |
$smt[1] = $result; | |
return $smt; | |
} else if ($smt[0] == PDF_TYPE_DICTIONARY) { | |
$result = array(); | |
foreach ($smt[1] as $key => $item) { | |
$result[$key] = $this->resolve($parser, $item, $maxdepth-1); | |
} | |
$smt[1] = $result; | |
return $smt; | |
} else { | |
return $smt; | |
} | |
} | |
function findPageNoForRef(&$parser, $pageRef) { | |
$ref_obj = $pageRef[1]; $ref_gen = $pageRef[2]; | |
foreach ($parser->pages as $index => $page) { | |
$page_obj = $page['obj']; $page_gen = $page['gen']; | |
if ($page_obj == $ref_obj && $page_gen == $ref_gen) { | |
return $index + 1; | |
} | |
} | |
return -1; | |
} | |
function importPage($pageno, $boxName = '/CropBox') { | |
$tplidx = parent::importPage($pageno, $boxName); | |
$tpl =& $this->tpls[$tplidx]; | |
$parser =& $tpl['parser']; | |
// look for hyperlink annotations and store them in the template | |
if (isset($parser->pages[$pageno - 1][1][1]['/Annots'])) { | |
$annots = $parser->pages[$pageno - 1][1][1]['/Annots']; | |
$annots = $this->resolve($parser, $annots); | |
$links = array(); | |
foreach ($annots[1] as $annot) if ($annot[0] == PDF_TYPE_DICTIONARY) { | |
// all links look like: << /Type /Annot /Subtype /Link /Rect [...] ... >> | |
if ($annot[1]['/Type'][1] == '/Annot' && $annot[1]['/Subtype'][1] == '/Link') { | |
$rect = $annot[1]['/Rect']; | |
if ($rect[0] == PDF_TYPE_ARRAY && count($rect[1]) == 4) { | |
$x = $rect[1][0][1]; $y = $rect[1][1][1]; | |
$x2 = $rect[1][2][1]; $y2 = $rect[1][3][1]; | |
$w = $x2 - $x; $h = $y2 - $y; | |
$h = -$h; | |
} | |
if (isset($annot[1]['/A'])) { | |
$A = $annot[1]['/A']; | |
if ($A[0] == PDF_TYPE_DICTIONARY && isset($A[1]['/S'])) { | |
$S = $A[1]['/S']; | |
// << /Type /Annot ... /A << /S /URI /URI ... >> >> | |
if ($S[1] == '/URI' && isset($A[1]['/URI'])) { | |
$URI = $A[1]['/URI']; | |
if (is_string($URI[1])) { | |
$uri = str_replace("\\000", '', trim($URI[1])); | |
if (!empty($uri)) { | |
$links[] = array($x, $y, $w, $h, $uri); | |
} | |
} | |
// << /Type /Annot ... /A << /S /GoTo /D [%d 0 R /Fit] >> >> | |
} else if ($S[1] == '/GoTo' && isset($A[1]['/D'])) { | |
$D = $A[1]['/D']; | |
if ($D[0] == PDF_TYPE_ARRAY && count($D[1]) > 0 && $D[1][0][0] == PDF_TYPE_OBJREF) { | |
$target_pageno = $this->findPageNoForRef($parser, $D[1][0]); | |
if ($target_pageno >= 0) { | |
$links[] = array($x, $y, $w, $h, $target_pageno); | |
} | |
} | |
} | |
} | |
} else if (isset($annot[1]['/Dest'])) { | |
$Dest = $annot[1]['/Dest']; | |
// << /Type /Annot ... /Dest [42 0 R ...] >> | |
if ($Dest[0] == PDF_TYPE_ARRAY && $Dest[0][1][0] == PDF_TYPE_OBJREF) { | |
$target_pageno = $this->findPageNoForRef($parser, $Dest[0][1][0]); | |
if ($target_pageno >= 0) { | |
$links[] = array($x, $y, $w, $h, $target_pageno); | |
} | |
} | |
} | |
} | |
} | |
$tpl['links'] = $links; | |
} | |
// echo "Links on page $pageno:\n"; | |
// print_r($links); | |
return $tplidx; | |
} | |
function useTemplate($tplidx, $_x = null, $_y = null, $_w = 0, $_h = 0, $adjustPageSize = false) { | |
$result = parent::useTemplate($tplidx, $_x, $_y, $_w, $_h, $adjustPageSize); | |
// apply links from the template | |
$tpl =& $this->tpls[$tplidx]; | |
if (isset($tpl['links'])) { | |
foreach ($tpl['links'] as $link) { | |
// $link[4] is either a string (external URL) or an integer (page number) | |
if (is_int($link[4])) { | |
$l = $this->AddLink(); | |
$this->SetLink($l, 0, $link[4]); | |
$link[4] = $l; | |
} | |
$this->PageLinks[$this->page][] = $link; | |
} | |
} | |
return $result; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class PM_PDF extends FPDI_with_annots { | |
function concat($file, $message) { | |
$pagecount = $this->setSourceFile($file); | |
for ($i = 1; $i <= $pagecount; $i++) { | |
$tplidx = $this->ImportPage($i); | |
$s = $this->getTemplatesize($tplidx); | |
$this->AddPage('P', array($s['w'], $s['h'])); | |
$this->useTemplate($tplidx); | |
// header | |
$this->SetAutoPageBreak(FALSE); | |
// $this->SetXY(8, -13); | |
$this->SetXY(6, -28); | |
$this->Rotate(90); | |
$this->SetTextColor(102, 102, 102); | |
$this->SetFont('Arial', '', 8); | |
$this->Cell(0, 5, utf8_decode($message),'',1,'L'); | |
// $this->Ln(20); | |
$this->Rotate(0); // outputs Q to balance "q" added by the previous call to Rotate | |
// footer | |
// $this->SetY(-20); | |
// $this->SetFont('Arial','I',8); | |
// $this->Cell(0,10,'Page ' . $this->PageNo() . '/{nb}',0,0,'C'); | |
} | |
} | |
// http://www.phpbuilder.com/board/showthread.php?t=10318839 | |
function Rotate($angle,$x=-1,$y=-1) { | |
if($x==-1) | |
$x=$this->x; | |
if($y==-1) | |
$y=$this->y; | |
if($this->angle!=0) | |
$this->_out('Q'); | |
$this->angle=$angle; | |
if($angle!=0) { | |
$angle*=M_PI/180; | |
$c=cos($angle); | |
$s=sin($angle); | |
$cx=$x*$this->k; | |
$cy=($this->h-$y)*$this->k; | |
$this->_out(sprintf('q %.5f %.5f %.5f %.5f %.2f %.2f cm 1 0 0 1 %.2f %.2f cm',$c,$s,-$s,$c,$cx,$cy,-$cx,-$cy)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
define('XXXX_DEBUG_PDF_GENERATION_NO_DOWNLOAD_FILE', FALSE); | |
define('XXXX_DEBUG_PDF_GENERATION_NO_COMPRESSION_OR_ENCRYPTION', FALSE); | |
function xxxx_personalize_pdf($source_pdf_file, $temp_file, $output_file, $orig_filename_for_error_messages, $spotlight_restrictions) { | |
setlocale(LC_CTYPE, "en_US.UTF-8"); // otherwise escapeshellarg() strips non-ASCII characters | |
$encryption_options = ''; | |
if ($spotlight_restrictions) | |
$encryption_options = '--extract=n'; | |
$qpdf_command = sprintf('/xxxx/qpdf-2.3.1/qpdf/build/qpdf --decrypt --stream-data=uncompress --force-version=1.4 %s %s', escapeshellarg($source_pdf_file), escapeshellarg($temp_file)); | |
$output = shell_exec($qpdf_command); | |
if (!file_exists($temp_file) || filesize($temp_file) == 0) { | |
watchdog('pm_files_download_control', "Cannot download file '{$orig_filename_for_error_messages}' because an error occurred while running qpdf (!command): !error.", array('!command' => $qpdf_command, '!error' => $output), WATCHDOG_ERROR); | |
drupal_set_message("Cannot download file '{$orig_filename_for_error_messages}' because an error occurred while processing PDF.", 'error'); | |
drupal_goto(''); | |
} | |
$user_name = "John Doe"; // get user name from the database | |
$pdf = new PM_PDF(); | |
if (XXXX_DEBUG_PDF_GENERATION_NO_COMPRESSION_OR_ENCRYPTION) | |
$pdf->SetCompression(FALSE); | |
$pdf->concat($temp_file, "Persönliches Exemplar für {$user_name}. Bitte beachten Sie: Eine Weitergabe an Dritte ist nicht gestattet."); | |
$pdf->Output($temp_file, 'F'); | |
if (XXXX_DEBUG_PDF_GENERATION_NO_COMPRESSION_OR_ENCRYPTION) { | |
copy($temp_file, $output_file); | |
} else { | |
$qpdf_command = sprintf('/xxxx/qpdf-2.3.1/qpdf/build/qpdf --encrypt "" "" 40 ' . $encryption_options . ' -- %s %s', escapeshellarg($temp_file), escapeshellarg($output_file)); | |
$output = shell_exec($qpdf_command); | |
if (!file_exists($output_file) || filesize($output_file) == 0) { | |
watchdog('pm_files_download_control', "Cannot download file '{$orig_filename_for_error_messages}' because an error occurred while running qpdf (!command): !error.", array('!command' => $qpdf_command, '!error' => $output), WATCHDOG_ERROR); | |
drupal_set_message("Cannot download file '{$orig_filename_for_error_messages}' because an error occurred while processing PDF.", 'error'); | |
drupal_goto(''); | |
} | |
} | |
} | |
function xxxx_send_file($filename, $filepath) { | |
header('Content-Description: File Transfer'); | |
header('Content-Type: application/octet-stream'); | |
if (!XXXX_DEBUG_PDF_GENERATION_NO_DOWNLOAD_FILE) | |
header('Content-Disposition: attachment; filename=' . $filename); | |
header('Content-Transfer-Encoding: binary'); | |
header('Expires: 0'); | |
header('Cache-Control: must-revalidate'); | |
header('Pragma: public'); | |
header('Content-Length: ' . filesize($filepath)); | |
ob_clean(); | |
flush(); | |
readfile($filepath); | |
} | |
// ... | |
if ($file->type == 'pdf') { | |
$temp_file_mid = tempnam($secret_folder, $file->filename); | |
xxxx_personalize_pdf($file->filepath, $temp_file_mid, $output_file, $file->filename, $spotlight_restrictions); | |
if (file_exists($temp_file_mid)) | |
unlink($temp_file_mid); | |
xxxx_send_file($file->filename, $output_file); | |
unlink($output_file); | |
exit; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment