commit efd543fe593537a291d015b6414ee8fdddcaf4dd Author: basil <15813246678@163.com> Date: Tue Jun 3 01:21:14 2025 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3cb6a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.git +/.idea +/vendor +/Cache +/nbproject +/composer.lock +.DS_Store +cookie \ No newline at end of file diff --git a/2.pdf b/2.pdf new file mode 100644 index 0000000..d35dba3 Binary files /dev/null and b/2.pdf differ diff --git a/Fpdi/CFpdf.php b/Fpdi/CFpdf.php new file mode 100644 index 0000000..4c5d6a8 --- /dev/null +++ b/Fpdi/CFpdf.php @@ -0,0 +1,381 @@ + 250, '!' => 250, '"' => 408, '#' => 668, '$' => 490, '%' => 875, '&' => 698, '\'' => 250, + '(' => 240, ')' => 240, '*' => 417, '+' => 667, ',' => 250, '-' => 313, '.' => 250, '/' => 520, '0' => 500, '1' => 500, + '2' => 500, '3' => 500, '4' => 500, '5' => 500, '6' => 500, '7' => 500, '8' => 500, '9' => 500, ':' => 250, ';' => 250, + '<' => 667, '=' => 667, '>' => 667, '?' => 396, '@' => 921, 'A' => 677, 'B' => 615, 'C' => 719, 'D' => 760, 'E' => 625, + 'F' => 552, 'G' => 771, 'H' => 802, 'I' => 354, 'J' => 354, 'K' => 781, 'L' => 604, 'M' => 927, 'N' => 750, 'O' => 823, + 'P' => 563, 'Q' => 823, 'R' => 729, 'S' => 542, 'T' => 698, 'U' => 771, 'V' => 729, 'W' => 948, 'X' => 771, 'Y' => 677, + 'Z' => 635, '[' => 344, '\\' => 520, ']' => 344, '^' => 469, '_' => 500, '`' => 250, 'a' => 469, 'b' => 521, 'c' => 427, + 'd' => 521, 'e' => 438, 'f' => 271, 'g' => 469, 'h' => 531, 'i' => 250, 'j' => 250, 'k' => 458, 'l' => 240, 'm' => 802, + 'n' => 531, 'o' => 500, 'p' => 521, 'q' => 521, 'r' => 365, 's' => 333, 't' => 292, 'u' => 521, 'v' => 458, 'w' => 677, + 'x' => 479, 'y' => 458, 'z' => 427, '{' => 480, '|' => 496, '}' => 480, '~' => 667 + ); + + protected $GB_widths = array( + ' ' => 207, '!' => 270, '"' => 342, '#' => 467, '$' => 462, '%' => 797, '&' => 710, '\'' => 239, + '(' => 374, ')' => 374, '*' => 423, '+' => 605, ',' => 238, '-' => 375, '.' => 238, '/' => 334, '0' => 462, '1' => 462, + '2' => 462, '3' => 462, '4' => 462, '5' => 462, '6' => 462, '7' => 462, '8' => 462, '9' => 462, ':' => 238, ';' => 238, + '<' => 605, '=' => 605, '>' => 605, '?' => 344, '@' => 748, 'A' => 684, 'B' => 560, 'C' => 695, 'D' => 739, 'E' => 563, + 'F' => 511, 'G' => 729, 'H' => 793, 'I' => 318, 'J' => 312, 'K' => 666, 'L' => 526, 'M' => 896, 'N' => 758, 'O' => 772, + 'P' => 544, 'Q' => 772, 'R' => 628, 'S' => 465, 'T' => 607, 'U' => 753, 'V' => 711, 'W' => 972, 'X' => 647, 'Y' => 620, + 'Z' => 607, '[' => 374, '\\' => 333, ']' => 374, '^' => 606, '_' => 500, '`' => 239, 'a' => 417, 'b' => 503, 'c' => 427, + 'd' => 529, 'e' => 415, 'f' => 264, 'g' => 444, 'h' => 518, 'i' => 241, 'j' => 230, 'k' => 495, 'l' => 228, 'm' => 793, + 'n' => 527, 'o' => 524, 'p' => 524, 'q' => 504, 'r' => 338, 's' => 336, 't' => 277, 'u' => 517, 'v' => 450, 'w' => 652, + 'x' => 466, 'y' => 452, 'z' => 407, '{' => 370, '|' => 258, '}' => 370, '~' => 605 + ); + + public function AddCIDFont($family, $style, $name, $cw, $CMap, $registry) + { + $fontkey = strtolower($family) . strtoupper($style); + if (isset($this->fonts[$fontkey])) + $this->Error("Font already added: $family $style"); + $i = count($this->fonts) + 1; + $name = str_replace(' ', '', $name); + $this->fonts[$fontkey] = array('i' => $i, 'type' => 'Type0', 'name' => $name, 'up' => -130, 'ut' => 40, 'cw' => $cw, 'CMap' => $CMap, 'registry' => $registry); + } + + public function AddCIDFonts($family, $name, $cw, $CMap, $registry) + { + $this->AddCIDFont($family, '', $name, $cw, $CMap, $registry); + $this->AddCIDFont($family, 'B', $name . ',Bold', $cw, $CMap, $registry); + $this->AddCIDFont($family, 'I', $name . ',Italic', $cw, $CMap, $registry); + $this->AddCIDFont($family, 'BI', $name . ',BoldItalic', $cw, $CMap, $registry); + } + + public function AddBig5Font($family = 'Big5', $name = 'MSungStd-Light-Acro') + { + // Add Big5 font with proportional Latin + $cw = $this->Big5_widths; + $CMap = 'ETenms-B5-H'; + $registry = array('ordering' => 'CNS1', 'supplement' => 0); + $this->AddCIDFonts($family, $name, $cw, $CMap, $registry); + } + + public function AddBig5hwFont($family = 'Big5-hw', $name = 'MSungStd-Light-Acro') + { + // Add Big5 font with half-witdh Latin + for ($i = 32; $i <= 126; $i++) + $cw[chr($i)] = 500; + $CMap = 'ETen-B5-H'; + $registry = array('ordering' => 'CNS1', 'supplement' => 0); + $this->AddCIDFonts($family, $name, $cw, $CMap, $registry); + } + + public function AddGBFont($family = 'GB', $name = 'STSongStd-Light-Acro') + { + // Add GB font with proportional Latin + $cw = $this->GB_widths; + $CMap = 'GBKp-EUC-H'; + $registry = array('ordering' => 'GB1', 'supplement' => 2); + $this->AddCIDFonts($family, $name, $cw, $CMap, $registry); + } + + public function AddGBhwFont($family = 'GB-hw', $name = 'STSongStd-Light-Acro') + { + // Add GB font with half-width Latin + for ($i = 32; $i <= 126; $i++) + $cw[chr($i)] = 500; + $CMap = 'GBK-EUC-H'; + $registry = array('ordering' => 'GB1', 'supplement' => 2); + $this->AddCIDFonts($family, $name, $cw, $CMap, $registry); + } + + public function GetStringWidth($s) + { + if ($this->CurrentFont['type'] == 'Type0') + return $this->GetMBStringWidth($s); + else + return parent::GetStringWidth($s); + } + + public function GetMBStringWidth($s) + { + // Multi-byte version of GetStringWidth() + $l = 0; + $cw = &$this->CurrentFont['cw']; + $nb = strlen($s); + $i = 0; + while ($i < $nb) { + $c = $s[$i]; + if (ord($c) < 128) { + $l += $cw[$c]; + $i++; + } else { + $l += 1000; + $i += 2; + } + } + return $l * $this->FontSize / 1000; + } + + public function MultiCell($w, $h, $txt, $border = 0, $align = 'L', $fill = 0) + { + if ($this->CurrentFont['type'] == 'Type0') + $this->MBMultiCell($w, $h, $txt, $border, $align, $fill); + else + parent::MultiCell($w, $h, $txt, $border, $align, $fill); + } + + private function MBMultiCell($w, $h, $txt, $border = 0, $align = 'L', $fill = 0) + { + // Multi-byte version of MultiCell() + $cw = &$this->CurrentFont['cw']; + if ($w == 0) + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + if ($nb > 0 && $s[$nb - 1] == "\n") + $nb--; + $b = 0; + if ($border) { + if ($border == 1) { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } else { + $b2 = ''; + if (is_int(strpos($border, 'L'))) + $b2 .= 'L'; + if (is_int(strpos($border, 'R'))) + $b2 .= 'R'; + $b = is_int(strpos($border, 'T')) ? $b2 . 'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + // Get next character + $c = $s[$i]; + // Check if ASCII or MB + $ascii = (ord($c) < 128); + if ($c == "\n") { + // Explicit line break + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + $i++; + $sep = -1; + $j = $i; + $l = 0; + $nl++; + if ($border && $nl == 2) + $b = $b2; + continue; + } + if (!$ascii) { + $sep = $i; + $ls = $l; + } elseif ($c == ' ') { + $sep = $i; + $ls = $l; + } + $l += $ascii ? $cw[$c] : 1000; + if ($l > $wmax) { + // Automatic line break + if ($sep == -1 || $i == $j) { + if ($i == $j) + $i += $ascii ? 1 : 2; + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + } else { + $this->Cell($w, $h, substr($s, $j, $sep - $j), $b, 2, $align, $fill); + $i = ($s[$sep] == ' ') ? $sep + 1 : $sep; + } + $sep = -1; + $j = $i; + $l = 0; + $nl++; + if ($border && $nl == 2) + $b = $b2; + } else + $i += $ascii ? 1 : 2; + } + // Last chunk + if ($border && is_int(strpos($border, 'B'))) + $b .= 'B'; + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + $this->x = $this->lMargin; + } + + public function Write($h, $txt, $link = '') + { + if ($this->CurrentFont['type'] == 'Type0') + $this->MBWrite($h, $txt, $link); + else + parent::Write($h, $txt, $link); + } + + private function MBWrite($h, $txt, $link) + { + // Multi-byte version of Write() + $cw = &$this->CurrentFont['cw']; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + // Get next character + $c = $s[$i]; + // Check if ASCII or MB + $ascii = (ord($c) < 128); + if ($c == "\n") { + // Explicit line break + $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', 0, $link); + $i++; + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + } + $nl++; + continue; + } + if (!$ascii || $c == ' ') + $sep = $i; + $l += $ascii ? $cw[$c] : 1000; + if ($l > $wmax) { + // Automatic line break + if ($sep == -1 || $i == $j) { + if ($this->x > $this->lMargin) { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $i++; + $nl++; + continue; + } + if ($i == $j) + $i += $ascii ? 1 : 2; + $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', 0, $link); + } else { + $this->Cell($w, $h, substr($s, $j, $sep - $j), 0, 2, '', 0, $link); + $i = ($s[$sep] == ' ') ? $sep + 1 : $sep; + } + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + } + $nl++; + } else + $i += $ascii ? 1 : 2; + } + // Last chunk + if ($i != $j) + $this->Cell($l / 1000 * $this->FontSize, $h, substr($s, $j, $i - $j), 0, 0, '', 0, $link); + } + + public function _putType0($font) + { + // Type0 + $this->_newobj(); + $this->_out('<_out('/Subtype /Type0'); + $this->_out('/BaseFont /' . $font['name'] . '-' . $font['CMap']); + $this->_out('/Encoding /' . $font['CMap']); + $this->_out('/DescendantFonts [' . ($this->n + 1) . ' 0 R]'); + $this->_out('>>'); + $this->_out('endobj'); + // CIDFont + $this->_newobj(); + $this->_out('<_out('/Subtype /CIDFontType0'); + $this->_out('/BaseFont /' . $font['name']); + $this->_out('/CIDSystemInfo <_textstring('Adobe') . ' /Ordering ' . $this->_textstring($font['registry']['ordering']) . ' /Supplement ' . $font['registry']['supplement'] . '>>'); + $this->_out('/FontDescriptor ' . ($this->n + 1) . ' 0 R'); + if ($font['CMap'] == 'ETen-B5-H') + $W = '13648 13742 500'; + elseif ($font['CMap'] == 'GBK-EUC-H') + $W = '814 907 500 7716 [500]'; + else + $W = '1 [' . implode(' ', $font['cw']) . ']'; + $this->_out('/W [' . $W . ']>>'); + $this->_out('endobj'); + // Font descriptor + $this->_newobj(); + $this->_out('<_out('/FontName /' . $font['name']); + $this->_out('/Flags 6'); + $this->_out('/FontBBox [0 -200 1000 900]'); + $this->_out('/ItalicAngle 0'); + $this->_out('/Ascent 800'); + $this->_out('/Descent -200'); + $this->_out('/CapHeight 800'); + $this->_out('/StemV 50'); + $this->_out('>>'); + $this->_out('endobj'); + } + + /** + * 旋转文字,角度为弧度 + * + * @param int $angle 角度 + * @return void + */ + private 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)); + } + } + public function _endpage() + { + if ($this->angle != 0) { + $this->angle = 0; + $this->_out('Q'); + } + parent::_endpage(); + } + public function RotatedText($x, $y, $txt, $angle) + { + //Text rotated around its origin + $this->Rotate($angle, $x, $y); + $this->Text($x, $y, $txt); + $this->Rotate(0); + } + + public function RotatedImage($file, $x, $y, $w, $h, $angle) + { + //Image rotated around its upper-left corner + $this->Rotate($angle, $x, $y); + $this->Image($file, $x, $y, $w, $h); + $this->Rotate(0); + } +} diff --git a/Fpdi/Fpdf.php b/Fpdi/Fpdf.php new file mode 100644 index 0000000..10bcc28 --- /dev/null +++ b/Fpdi/Fpdf.php @@ -0,0 +1,1795 @@ +_dochecks(); + // Initialization of properties + $this->state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + /* + // Font path + if (defined('FPDF_FONTPATH')) { + $this->fontpath = FPDF_FONTPATH; + if (substr($this->fontpath, -1) != '/' && substr($this->fontpath, -1) != '\\') + $this->fontpath .= '/'; + } elseif (is_dir(dirname(__FILE__) . '/font')) + $this->fontpath = dirname(__FILE__) . '/font/'; + else + $this->fontpath = ''; + */ + if (is_dir(dirname(__FILE__) . '/font')) + $this->fontpath = dirname(__FILE__) . '/font/'; + else + $this->fontpath = ''; + // Core fonts + $this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats'); + // Scale factor + if ($unit == 'pt') + $this->k = 1; + elseif ($unit == 'mm') + $this->k = 72 / 25.4; + elseif ($unit == 'cm') + $this->k = 72 / 2.54; + elseif ($unit == 'in') + $this->k = 72; + else + $this->Error('Incorrect unit: ' . $unit); + // Page sizes + $this->StdPageSizes = array( + 'a3' => array(841.89, 1190.55), 'a4' => array(595.28, 841.89), 'a5' => array(420.94, 595.28), + 'letter' => array(612, 792), 'legal' => array(612, 1008) + ); + $size = $this->_getpagesize($size); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + // Page orientation + $orientation = strtolower($orientation); + if ($orientation == 'p' || $orientation == 'portrait') { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } elseif ($orientation == 'l' || $orientation == 'landscape') { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } else + $this->Error('Incorrect orientation: ' . $orientation); + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + // Page rotation + $this->CurRotation = 0; + // Page margins (1 cm) + $margin = 28.35 / $this->k; + $this->SetMargins($margin, $margin); + // Interior cell margin (1 mm) + $this->cMargin = $margin / 10; + // Line width (0.2 mm) + $this->LineWidth = .567 / $this->k; + // Automatic page break + $this->SetAutoPageBreak(true, 2 * $margin); + // Default display mode + $this->SetDisplayMode('default'); + // Enable compression + $this->SetCompression(true); + // Set default PDF version number + $this->PDFVersion = '1.3'; + } + + function SetMargins($left, $top, $right = null) + { + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if ($right === null) + $right = $left; + $this->rMargin = $right; + } + + function SetLeftMargin($margin) + { + // Set left margin + $this->lMargin = $margin; + if ($this->page > 0 && $this->x < $margin) + $this->x = $margin; + } + + function SetTopMargin($margin) + { + // Set top margin + $this->tMargin = $margin; + } + + function SetRightMargin($margin) + { + // Set right margin + $this->rMargin = $margin; + } + + function SetAutoPageBreak($auto, $margin = 0) + { + // Set auto page break mode and triggering margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h - $margin; + } + + function SetDisplayMode($zoom, $layout = 'default') + { + // Set display mode in viewer + if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real' || $zoom == 'default' || !is_string($zoom)) + $this->ZoomMode = $zoom; + else + $this->Error('Incorrect zoom display mode: ' . $zoom); + if ($layout == 'single' || $layout == 'continuous' || $layout == 'two' || $layout == 'default') + $this->LayoutMode = $layout; + else + $this->Error('Incorrect layout display mode: ' . $layout); + } + + function SetCompression($compress) + { + // Set page compression + if (function_exists('gzcompress')) + $this->compress = $compress; + else + $this->compress = false; + } + + function SetTitle($title, $isUTF8 = false) + { + // Title of document + $this->metadata['Title'] = $isUTF8 ? $title : utf8_encode($title); + } + + function SetAuthor($author, $isUTF8 = false) + { + // Author of document + $this->metadata['Author'] = $isUTF8 ? $author : utf8_encode($author); + } + + function SetSubject($subject, $isUTF8 = false) + { + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $subject : utf8_encode($subject); + } + + function SetKeywords($keywords, $isUTF8 = false) + { + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $keywords : utf8_encode($keywords); + } + + function SetCreator($creator, $isUTF8 = false) + { + // Creator of document + $this->metadata['Creator'] = $isUTF8 ? $creator : utf8_encode($creator); + } + + function AliasNbPages($alias = '{nb}') + { + // Define an alias for total number of pages + $this->AliasNbPages = $alias; + } + + function Error($msg) + { + // Fatal error + throw new \Exception('FPDF error: ' . $msg); + } + + function Close() + { + // Terminate document + if ($this->state == 3) + return; + if ($this->page == 0) + $this->AddPage(); + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); + } + + function AddPage($orientation = '', $size = '', $rotation = 0) + { + // Start a new page + if ($this->state == 3) + $this->Error('The document is closed'); + $family = $this->FontFamily; + $style = $this->FontStyle . ($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if ($this->page > 0) { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage($orientation, $size, $rotation); + // Set line cap style to square + $this->_out('2 J'); + // Set line width + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw * $this->k)); + // Set font + if ($family) + $this->SetFont($family, $style, $fontsize); + // Set colors + $this->DrawColor = $dc; + if ($dc != '0 G') + $this->_out($dc); + $this->FillColor = $fc; + if ($fc != '0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if ($this->LineWidth != $lw) { + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw * $this->k)); + } + // Restore font + if ($family) + $this->SetFont($family, $style, $fontsize); + // Restore colors + if ($this->DrawColor != $dc) { + $this->DrawColor = $dc; + $this->_out($dc); + } + if ($this->FillColor != $fc) { + $this->FillColor = $fc; + $this->_out($fc); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; + } + + function Header() + { + // To be implemented in your own inherited class + } + + function Footer() + { + // To be implemented in your own inherited class + } + + function PageNo() + { + // Get current page number + return $this->page; + } + + function SetDrawColor($r, $g = null, $b = null) + { + // Set color for all stroking operations + if (($r == 0 && $g == 0 && $b == 0) || $g === null) + $this->DrawColor = sprintf('%.3F G', $r / 255); + else + $this->DrawColor = sprintf('%.3F %.3F %.3F RG', $r / 255, $g / 255, $b / 255); + if ($this->page > 0) + $this->_out($this->DrawColor); + } + + function SetFillColor($r, $g = null, $b = null) + { + // Set color for all filling operations + if (($r == 0 && $g == 0 && $b == 0) || $g === null) + $this->FillColor = sprintf('%.3F g', $r / 255); + else + $this->FillColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255); + $this->ColorFlag = ($this->FillColor != $this->TextColor); + if ($this->page > 0) + $this->_out($this->FillColor); + } + + function SetTextColor($r, $g = null, $b = null) + { + // Set color for text + if (($r == 0 && $g == 0 && $b == 0) || $g === null) + $this->TextColor = sprintf('%.3F g', $r / 255); + else + $this->TextColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255); + $this->ColorFlag = ($this->FillColor != $this->TextColor); + } + + function GetStringWidth($s) + { + // Get width of a string in the current font + $s = (string)$s; + $cw = &$this->CurrentFont['cw']; + $w = 0; + $l = strlen($s); + for ($i = 0; $i < $l; $i++) + $w += $cw[$s[$i]]; + return $w * $this->FontSize / 1000; + } + + function SetLineWidth($width) + { + // Set line width + $this->LineWidth = $width; + if ($this->page > 0) + $this->_out(sprintf('%.2F w', $width * $this->k)); + } + + function Line($x1, $y1, $x2, $y2) + { + // Draw a line + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1 * $this->k, ($this->h - $y1) * $this->k, $x2 * $this->k, ($this->h - $y2) * $this->k)); + } + + function Rect($x, $y, $w, $h, $style = '') + { + // Draw a rectangle + if ($style == 'F') + $op = 'f'; + elseif ($style == 'FD' || $style == 'DF') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s', $x * $this->k, ($this->h - $y) * $this->k, $w * $this->k, -$h * $this->k, $op)); + } + + function AddFont($family, $style = '', $file = '') + { + // Add a TrueType, OpenType or Type1 font + $family = strtolower($family); + if ($file == '') + $file = str_replace(' ', '', $family) . strtolower($style) . '.php'; + $style = strtoupper($style); + if ($style == 'IB') + $style = 'BI'; + $fontkey = $family . $style; + if (isset($this->fonts[$fontkey])) + return; + $info = $this->_loadfont($file); + $info['i'] = count($this->fonts) + 1; + if (!empty($info['file'])) { + // Embedded font + if ($info['type'] == 'TrueType') + $this->FontFiles[$info['file']] = array('length1' => $info['originalsize']); + else + $this->FontFiles[$info['file']] = array('length1' => $info['size1'], 'length2' => $info['size2']); + } + $this->fonts[$fontkey] = $info; + } + + function SetFont($family, $style = '', $size = 0) + { + // Select a font; size given in points + if ($family == '') + $family = $this->FontFamily; + else + $family = strtolower($family); + $style = strtoupper($style); + if (strpos($style, 'U') !== false) { + $this->underline = true; + $style = str_replace('U', '', $style); + } else + $this->underline = false; + if ($style == 'IB') + $style = 'BI'; + if ($size == 0) + $size = $this->FontSizePt; + // Test if font is already selected + if ($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size) + return; + // Test if font is already loaded + $fontkey = $family . $style; + if (!isset($this->fonts[$fontkey])) { + // Test if one of the core fonts + if ($family == 'arial') + $family = 'helvetica'; + if (in_array($family, $this->CoreFonts)) { + if ($family == 'symbol' || $family == 'zapfdingbats') + $style = ''; + $fontkey = $family . $style; + if (!isset($this->fonts[$fontkey])) + $this->AddFont($family, $style); + } else + $this->Error('Undefined font: ' . $family . ' ' . $style); + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + $this->CurrentFont = &$this->fonts[$fontkey]; + if ($this->page > 0) + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + + function SetFontSize($size) + { + // Set font size in points + if ($this->FontSizePt == $size) + return; + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + if ($this->page > 0) + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + + function AddLink() + { + // Create a new internal link + $n = count($this->links) + 1; + $this->links[$n] = array(0, 0); + return $n; + } + + function SetLink($link, $y = 0, $page = -1) + { + // Set destination of internal link + if ($y == -1) + $y = $this->y; + if ($page == -1) + $page = $this->page; + $this->links[$link] = array($page, $y); + } + + function Link($x, $y, $w, $h, $link) + { + // Put a link on the page + $this->PageLinks[$this->page][] = array($x * $this->k, $this->hPt - $y * $this->k, $w * $this->k, $h * $this->k, $link); + } + + function Text($x, $y, $txt) + { + // Output a string + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->_escape($txt)); + if ($this->underline && $txt != '') + $s .= ' ' . $this->_dounderline($x, $y, $txt); + if ($this->ColorFlag) + $s = 'q ' . $this->TextColor . ' ' . $s . ' Q'; + $this->_out($s); + } + + function AcceptPageBreak() + { + // Accept automatic page break or not + return $this->AutoPageBreak; + } + + function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = false, $link = '') + { + // Output a cell + $k = $this->k; + if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if ($ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); + $this->x = $x; + if ($ws > 0) { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw', $ws * $k)); + } + } + if ($w == 0) + $w = $this->w - $this->rMargin - $this->x; + $s = ''; + if ($fill || $border == 1) { + if ($fill) + $op = ($border == 1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op); + } + if (is_string($border)) { + $x = $this->x; + $y = $this->y; + if (strpos($border, 'L') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k); + if (strpos($border, 'T') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k); + if (strpos($border, 'R') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); + if (strpos($border, 'B') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); + } + if ($txt !== '') { + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if ($align == 'R') + $dx = $w - $this->cMargin - $this->GetStringWidth($txt); + elseif ($align == 'C') + $dx = ($w - $this->GetStringWidth($txt)) / 2; + else + $dx = $this->cMargin; + if ($this->ColorFlag) + $s .= 'q ' . $this->TextColor . ' '; + $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', ($this->x + $dx) * $k, ($this->h - ($this->y + .5 * $h + .3 * $this->FontSize)) * $k, $this->_escape($txt)); + if ($this->underline) + $s .= ' ' . $this->_dounderline($this->x + $dx, $this->y + .5 * $h + .3 * $this->FontSize, $txt); + if ($this->ColorFlag) + $s .= ' Q'; + if ($link) + $this->Link($this->x + $dx, $this->y + .5 * $h - .5 * $this->FontSize, $this->GetStringWidth($txt), $this->FontSize, $link); + } + if ($s) + $this->_out($s); + $this->lasth = $h; + if ($ln > 0) { + // Go to next line + $this->y += $h; + if ($ln == 1) + $this->x = $this->lMargin; + } else + $this->x += $w; + } + + function MultiCell($w, $h, $txt, $border = 0, $align = 'J', $fill = false) + { + // Output text with automatic or explicit line breaks + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = &$this->CurrentFont['cw']; + if ($w == 0) + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + if ($nb > 0 && $s[$nb - 1] == "\n") + $nb--; + $b = 0; + if ($border) { + if ($border == 1) { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } else { + $b2 = ''; + if (strpos($border, 'L') !== false) + $b2 .= 'L'; + if (strpos($border, 'R') !== false) + $b2 .= 'R'; + $b = (strpos($border, 'T') !== false) ? $b2 . 'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while ($i < $nb) { + // Get next character + $c = $s[$i]; + if ($c == "\n") { + // Explicit line break + if ($this->ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if ($border && $nl == 2) + $b = $b2; + continue; + } + if ($c == ' ') { + $sep = $i; + $ls = $l; + $ns++; + } + $l += $cw[$c]; + if ($l > $wmax) { + // Automatic line break + if ($sep == -1) { + if ($i == $j) + $i++; + if ($this->ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + } else { + if ($align == 'J') { + $this->ws = ($ns > 1) ? ($wmax - $ls) / 1000 * $this->FontSize / ($ns - 1) : 0; + $this->_out(sprintf('%.3F Tw', $this->ws * $this->k)); + } + $this->Cell($w, $h, substr($s, $j, $sep - $j), $b, 2, $align, $fill); + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if ($border && $nl == 2) + $b = $b2; + } else + $i++; + } + // Last chunk + if ($this->ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + if ($border && strpos($border, 'B') !== false) + $b .= 'B'; + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + $this->x = $this->lMargin; + } + + function Write($h, $txt, $link = '') + { + // Output text in flowing mode + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = &$this->CurrentFont['cw']; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + // Get next character + $c = $s[$i]; + if ($c == "\n") { + // Explicit line break + $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link); + $i++; + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + } + $nl++; + continue; + } + if ($c == ' ') + $sep = $i; + $l += $cw[$c]; + if ($l > $wmax) { + // Automatic line break + if ($sep == -1) { + if ($this->x > $this->lMargin) { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $i++; + $nl++; + continue; + } + if ($i == $j) + $i++; + $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link); + } else { + $this->Cell($w, $h, substr($s, $j, $sep - $j), 0, 2, '', false, $link); + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + } + $nl++; + } else + $i++; + } + // Last chunk + if ($i != $j) + $this->Cell($l / 1000 * $this->FontSize, $h, substr($s, $j), 0, 0, '', false, $link); + } + + function Ln($h = null) + { + // Line feed; default value is the last cell height + $this->x = $this->lMargin; + if ($h === null) + $this->y += $this->lasth; + else + $this->y += $h; + } + + function Image($file, $x = null, $y = null, $w = 0, $h = 0, $type = '', $link = '') + { + // Put an image on the page + if ($file == '') + $this->Error('Image file name is empty'); + if (!isset($this->images[$file])) { + // First use of this image, get info + if ($type == '') { + $pos = strrpos($file, '.'); + if (!$pos) + $this->Error('Image file has no extension and no type was specified: ' . $file); + $type = substr($file, $pos + 1); + } + $type = strtolower($type); + if ($type == 'jpeg') + $type = 'jpg'; + $mtd = '_parse' . $type; + if (!method_exists($this, $mtd)) + $this->Error('Unsupported image type: ' . $type); + $info = $this->$mtd($file); + $info['i'] = count($this->images) + 1; + $this->images[$file] = $info; + } else + $info = $this->images[$file]; + + // Automatic width and height calculation if needed + if ($w == 0 && $h == 0) { + // Put image at 96 dpi + $w = -96; + $h = -96; + } + if ($w < 0) + $w = -$info['w'] * 72 / $w / $this->k; + if ($h < 0) + $h = -$info['h'] * 72 / $h / $this->k; + if ($w == 0) + $w = $h * $info['w'] / $info['h']; + if ($h == 0) + $h = $w * $info['h'] / $info['w']; + + // Flowing mode + if ($y === null) { + if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) { + // Automatic page break + $x2 = $this->x; + $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); + $this->x = $x2; + } + $y = $this->y; + $this->y += $h; + } + + if ($x === null) + $x = $this->x; + $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $w * $this->k, $h * $this->k, $x * $this->k, ($this->h - ($y + $h)) * $this->k, $info['i'])); + if ($link) + $this->Link($x, $y, $w, $h, $link); + } + + function GetPageWidth() + { + // Get current page width + return $this->w; + } + + function GetPageHeight() + { + // Get current page height + return $this->h; + } + + function GetX() + { + // Get x position + return $this->x; + } + + function SetX($x) + { + // Set x position + if ($x >= 0) + $this->x = $x; + else + $this->x = $this->w + $x; + } + + function GetY() + { + // Get y position + return $this->y; + } + + function SetY($y, $resetX = true) + { + // Set y position and optionally reset x + if ($y >= 0) + $this->y = $y; + else + $this->y = $this->h + $y; + if ($resetX) + $this->x = $this->lMargin; + } + + function SetXY($x, $y) + { + // Set x and y positions + $this->SetX($x); + $this->SetY($y, false); + } + + function Output($dest = '', $name = '', $isUTF8 = false) + { + // Output PDF to some destination + $this->Close(); + if (strlen($name) == 1 && strlen($dest) != 1) { + // Fix parameter order + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if ($dest == '') + $dest = 'I'; + if ($name == '') + $name = 'doc.pdf'; + switch (strtoupper($dest)) { + case 'I': + // Send to standard output + $this->_checkoutput(); + if (PHP_SAPI != 'cli') { + // We send to a browser + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; ' . $this->_httpencode('filename', $name, $isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header('Content-Type: application/x-download'); + header('Content-Disposition: attachment; ' . $this->_httpencode('filename', $name, $isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + echo $this->buffer; + break; + case 'F': + // Save to local file + if (!file_put_contents($name, $this->buffer)) + $this->Error('Unable to create output file: ' . $name); + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error('Incorrect output destination: ' . $dest); + } + return ''; + } + + /******************************************************************************* + * Protected methods * + *******************************************************************************/ + + protected function _dochecks() + { + // Check mbstring overloading + if (ini_get('mbstring.func_overload') & 2) + $this->Error('mbstring overloading must be disabled'); + } + + protected function _checkoutput() + { + if (PHP_SAPI != 'cli') { + if (headers_sent($file, $line)) + $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); + } + if (ob_get_length()) { + // The output buffer is not empty + if (preg_match('/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents())) { + // It contains only a UTF-8 BOM and/or whitespace, let's clean it + ob_clean(); + } else + $this->Error("Some data has already been output, can't send PDF file"); + } + } + + protected function _getpagesize($size) + { + if (is_string($size)) { + $size = strtolower($size); + if (!isset($this->StdPageSizes[$size])) + $this->Error('Unknown page size: ' . $size); + $a = $this->StdPageSizes[$size]; + return array($a[0] / $this->k, $a[1] / $this->k); + } else { + if ($size[0] > $size[1]) + return array($size[1], $size[0]); + else + return $size; + } + } + + protected function _beginpage($orientation, $size, $rotation) + { + $this->page++; + $this->pages[$this->page] = ''; + $this->PageLinks[$this->page] = array(); + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if ($orientation == '') + $orientation = $this->DefOrientation; + else + $orientation = strtoupper($orientation[0]); + if ($size == '') + $size = $this->DefPageSize; + else + $size = $this->_getpagesize($size); + if ($orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1]) { + // New size or orientation + if ($orientation == 'P') { + $this->w = $size[0]; + $this->h = $size[1]; + } else { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + $this->PageBreakTrigger = $this->h - $this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if ($orientation != $this->DefOrientation || $size[0] != $this->DefPageSize[0] || $size[1] != $this->DefPageSize[1]) + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + if ($rotation != 0) { + if ($rotation % 90 != 0) + $this->Error('Incorrect rotation value: ' . $rotation); + $this->CurRotation = $rotation; + $this->PageInfo[$this->page]['rotation'] = $rotation; + } + } + + protected function _endpage() + { + $this->state = 1; + } + + protected function _loadfont($font) + { + // Load a font definition file from the font directory + if (strpos($font, '/') !== false || strpos($font, "\\") !== false) + $this->Error('Incorrect font definition file name: ' . $font); + include($this->fontpath . $font); + if (!isset($name)) + $this->Error('Could not include font definition file'); + if (isset($enc)) + $enc = strtolower($enc); + if (!isset($subsetted)) + $subsetted = false; + return get_defined_vars(); + } + + protected function _isascii($s) + { + // Test if string is ASCII + $nb = strlen($s); + for ($i = 0; $i < $nb; $i++) { + if (ord($s[$i]) > 127) + return false; + } + return true; + } + + protected function _httpencode($param, $value, $isUTF8) + { + // Encode HTTP header field parameter + if ($this->_isascii($value)) + return $param . '="' . $value . '"'; + if (!$isUTF8) + $value = utf8_encode($value); + if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false) + return $param . '="' . rawurlencode($value) . '"'; + else + return $param . "*=UTF-8''" . rawurlencode($value); + } + + protected function _UTF8toUTF16($s) + { + // Convert UTF-8 to UTF-16BE with BOM + $res = "\xFE\xFF"; + $nb = strlen($s); + $i = 0; + while ($i < $nb) { + $c1 = ord($s[$i++]); + if ($c1 >= 224) { + // 3-byte character + $c2 = ord($s[$i++]); + $c3 = ord($s[$i++]); + $res .= chr((($c1 & 0x0F) << 4) + (($c2 & 0x3C) >> 2)); + $res .= chr((($c2 & 0x03) << 6) + ($c3 & 0x3F)); + } elseif ($c1 >= 192) { + // 2-byte character + $c2 = ord($s[$i++]); + $res .= chr(($c1 & 0x1C) >> 2); + $res .= chr((($c1 & 0x03) << 6) + ($c2 & 0x3F)); + } else { + // Single-byte character + $res .= "\0" . chr($c1); + } + } + return $res; + } + + protected function _escape($s) + { + // Escape special characters + if (strpos($s, '(') !== false || strpos($s, ')') !== false || strpos($s, '\\') !== false || strpos($s, "\r") !== false) + return str_replace(array('\\', '(', ')', "\r"), array('\\\\', '\\(', '\\)', '\\r'), $s); + else + return $s; + } + + protected function _textstring($s) + { + // Format a text string + if (!$this->_isascii($s)) + $s = $this->_UTF8toUTF16($s); + return '(' . $this->_escape($s) . ')'; + } + + protected function _dounderline($x, $y, $txt) + { + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth($txt) + $this->ws * substr_count($txt, ' '); + return sprintf('%.2F %.2F %.2F %.2F re f', $x * $this->k, ($this->h - ($y - $up / 1000 * $this->FontSize)) * $this->k, $w * $this->k, -$ut / 1000 * $this->FontSizePt); + } + + protected function _parsejpg($file) + { + // Extract info from a JPEG file + $a = getimagesize($file); + if (!$a) + $this->Error('Missing or incorrect image file: ' . $file); + if ($a[2] != 2) + $this->Error('Not a JPEG file: ' . $file); + if (!isset($a['channels']) || $a['channels'] == 3) + $colspace = 'DeviceRGB'; + elseif ($a['channels'] == 4) + $colspace = 'DeviceCMYK'; + else + $colspace = 'DeviceGray'; + $bpc = isset($a['bits']) ? $a['bits'] : 8; + $data = file_get_contents($file); + return array('w' => $a[0], 'h' => $a[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data); + } + + protected function _parsepng($file) + { + // Extract info from a PNG file + $f = fopen($file, 'rb'); + if (!$f) + $this->Error('Can\'t open image file: ' . $file); + $info = $this->_parsepngstream($f, $file); + fclose($f); + return $info; + } + + protected function _parsepngstream($f, $file) + { + // Check signature + if ($this->_readstream($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) + $this->Error('Not a PNG file: ' . $file); + + // Read header chunk + $this->_readstream($f, 4); + if ($this->_readstream($f, 4) != 'IHDR') + $this->Error('Incorrect PNG file: ' . $file); + $w = $this->_readint($f); + $h = $this->_readint($f); + $bpc = ord($this->_readstream($f, 1)); + if ($bpc > 8) + $this->Error('16-bit depth not supported: ' . $file); + $ct = ord($this->_readstream($f, 1)); + if ($ct == 0 || $ct == 4) + $colspace = 'DeviceGray'; + elseif ($ct == 2 || $ct == 6) + $colspace = 'DeviceRGB'; + elseif ($ct == 3) + $colspace = 'Indexed'; + else + $this->Error('Unknown color type: ' . $file); + if (ord($this->_readstream($f, 1)) != 0) + $this->Error('Unknown compression method: ' . $file); + if (ord($this->_readstream($f, 1)) != 0) + $this->Error('Unknown filter method: ' . $file); + if (ord($this->_readstream($f, 1)) != 0) + $this->Error('Interlacing not supported: ' . $file); + $this->_readstream($f, 4); + $dp = '/Predictor 15 /Colors ' . ($colspace == 'DeviceRGB' ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w; + + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do { + $n = $this->_readint($f); + $type = $this->_readstream($f, 4); + if ($type == 'PLTE') { + // Read palette + $pal = $this->_readstream($f, $n); + $this->_readstream($f, 4); + } elseif ($type == 'tRNS') { + // Read transparency info + $t = $this->_readstream($f, $n); + if ($ct == 0) + $trns = array(ord(substr($t, 1, 1))); + elseif ($ct == 2) + $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))); + else { + $pos = strpos($t, chr(0)); + if ($pos !== false) + $trns = array($pos); + } + $this->_readstream($f, 4); + } elseif ($type == 'IDAT') { + // Read image data block + $data .= $this->_readstream($f, $n); + $this->_readstream($f, 4); + } elseif ($type == 'IEND') + break; + else + $this->_readstream($f, $n + 4); + } while ($n); + + if ($colspace == 'Indexed' && empty($pal)) + $this->Error('Missing palette in ' . $file); + $info = array('w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'dp' => $dp, 'pal' => $pal, 'trns' => $trns); + if ($ct >= 4) { + // Extract alpha channel + if (!function_exists('gzuncompress')) + $this->Error('Zlib not available, can\'t handle alpha channel: ' . $file); + $data = gzuncompress($data); + $color = ''; + $alpha = ''; + if ($ct == 4) { + // Gray image + $len = 2 * $w; + for ($i = 0; $i < $h; $i++) { + $pos = (1 + $len) * $i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data, $pos + 1, $len); + $color .= preg_replace('/(.)./s', '$1', $line); + $alpha .= preg_replace('/.(.)/s', '$1', $line); + } + } else { + // RGB image + $len = 4 * $w; + for ($i = 0; $i < $h; $i++) { + $pos = (1 + $len) * $i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data, $pos + 1, $len); + $color .= preg_replace('/(.{3})./s', '$1', $line); + $alpha .= preg_replace('/.{3}(.)/s', '$1', $line); + } + } + unset($data); + $data = gzcompress($color); + $info['smask'] = gzcompress($alpha); + $this->WithAlpha = true; + if ($this->PDFVersion < '1.4') + $this->PDFVersion = '1.4'; + } + $info['data'] = $data; + return $info; + } + + protected function _readstream($f, $n) + { + // Read n bytes from stream + $res = ''; + while ($n > 0 && !feof($f)) { + $s = fread($f, $n); + if ($s === false) + $this->Error('Error while reading stream'); + $n -= strlen($s); + $res .= $s; + } + if ($n > 0) + $this->Error('Unexpected end of stream'); + return $res; + } + + protected function _readint($f) + { + // Read a 4-byte integer from stream + $a = unpack('Ni', $this->_readstream($f, 4)); + return $a['i']; + } + + protected function _parsegif($file) + { + // Extract info from a GIF file (via PNG conversion) + if (!function_exists('imagepng')) + $this->Error('GD extension is required for GIF support'); + if (!function_exists('imagecreatefromgif')) + $this->Error('GD has no GIF read support'); + $im = imagecreatefromgif($file); + if (!$im) + $this->Error('Missing or incorrect image file: ' . $file); + imageinterlace($im, 0); + ob_start(); + imagepng($im); + $data = ob_get_clean(); + imagedestroy($im); + $f = fopen('php://temp', 'rb+'); + if (!$f) + $this->Error('Unable to create memory stream'); + fwrite($f, $data); + rewind($f); + $info = $this->_parsepngstream($f, $file); + fclose($f); + return $info; + } + + protected function _out($s) + { + // Add a line to the document + if ($this->state == 2) + $this->pages[$this->page] .= $s . "\n"; + elseif ($this->state == 1) + $this->_put($s); + elseif ($this->state == 0) + $this->Error('No page has been added yet'); + elseif ($this->state == 3) + $this->Error('The document is closed'); + } + + protected function _put($s) + { + $this->buffer .= $s . "\n"; + } + + protected function _getoffset() + { + return strlen($this->buffer); + } + + protected function _newobj($n = null) + { + // Begin a new object + if ($n === null) + $n = ++$this->n; + $this->offsets[$n] = $this->_getoffset(); + $this->_put($n . ' 0 obj'); + } + + protected function _putstream($data) + { + $this->_put('stream'); + $this->_put($data); + $this->_put('endstream'); + } + + protected function _putstreamobject($data) + { + if ($this->compress) { + $entries = '/Filter /FlateDecode '; + $data = gzcompress($data); + } else + $entries = ''; + $entries .= '/Length ' . strlen($data); + $this->_newobj(); + $this->_put('<<' . $entries . '>>'); + $this->_putstream($data); + $this->_put('endobj'); + } + + protected function _putpage($n) + { + $this->_newobj(); + $this->_put('<_put('/Parent 1 0 R'); + if (isset($this->PageInfo[$n]['size'])) + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->PageInfo[$n]['size'][0], $this->PageInfo[$n]['size'][1])); + if (isset($this->PageInfo[$n]['rotation'])) + $this->_put('/Rotate ' . $this->PageInfo[$n]['rotation']); + $this->_put('/Resources 2 0 R'); + if (!empty($this->PageLinks[$n])) { + $s = '/Annots ['; + foreach ($this->PageLinks[$n] as $pl) + $s .= $pl[5] . ' 0 R '; + $s .= ']'; + $this->_put($s); + } + if ($this->WithAlpha) + $this->_put('/Group <>'); + $this->_put('/Contents ' . ($this->n + 1) . ' 0 R>>'); + $this->_put('endobj'); + // Page content + if (!empty($this->AliasNbPages)) + $this->pages[$n] = str_replace($this->AliasNbPages, $this->page, $this->pages[$n]); + $this->_putstreamobject($this->pages[$n]); + // Annotations + foreach ($this->PageLinks[$n] as $pl) { + $this->_newobj(); + $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]); + $s = '<_textstring($pl[4]) . '>>>>'; + else { + $l = $this->links[$pl[4]]; + if (isset($this->PageInfo[$l[0]]['size'])) + $h = $this->PageInfo[$l[0]]['size'][1]; + else + $h = ($this->DefOrientation == 'P') ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k; + $s .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[$l[0]]['n'], $h - $l[1] * $this->k); + } + $this->_put($s); + $this->_put('endobj'); + } + } + + protected function _putpages() + { + $nb = $this->page; + $n = $this->n; + for ($i = 1; $i <= $nb; $i++) { + $this->PageInfo[$i]['n'] = ++$n; + $n++; + foreach ($this->PageLinks[$i] as &$pl) + $pl[5] = ++$n; + unset($pl); + } + for ($i = 1; $i <= $nb; $i++) + $this->_putpage($i); + // Pages root + $this->_newobj(1); + $this->_put('<PageInfo[$i]['n'] . ' 0 R '; + $kids .= ']'; + $this->_put($kids); + $this->_put('/Count ' . $nb); + if ($this->DefOrientation == 'P') { + $w = $this->DefPageSize[0]; + $h = $this->DefPageSize[1]; + } else { + $w = $this->DefPageSize[1]; + $h = $this->DefPageSize[0]; + } + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $w * $this->k, $h * $this->k)); + $this->_put('>>'); + $this->_put('endobj'); + } + + protected function _putfonts() + { + foreach ($this->FontFiles as $file => $info) { + // Font file embedding + $this->_newobj(); + $this->FontFiles[$file]['n'] = $this->n; + $font = file_get_contents($this->fontpath . $file, true); + if (!$font) + $this->Error('Font file not found: ' . $file); + $compressed = (substr($file, -2) == '.z'); + if (!$compressed && isset($info['length2'])) + $font = substr($font, 6, $info['length1']) . substr($font, 6 + $info['length1'] + 6, $info['length2']); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('/Length1 ' . $info['length1']); + if (isset($info['length2'])) + $this->_put('/Length2 ' . $info['length2'] . ' /Length3 0'); + $this->_put('>>'); + $this->_putstream($font); + $this->_put('endobj'); + } + foreach ($this->fonts as $k => $font) { + // Encoding + if (isset($font['diff'])) { + if (!isset($this->encodings[$font['enc']])) { + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + $this->encodings[$font['enc']] = $this->n; + } + } + // ToUnicode CMap + if (isset($font['uv'])) { + if (isset($font['enc'])) + $cmapkey = $font['enc']; + else + $cmapkey = $font['name']; + if (!isset($this->cmaps[$cmapkey])) { + $cmap = $this->_tounicodecmap($font['uv']); + $this->_putstreamobject($cmap); + $this->cmaps[$cmapkey] = $this->n; + } + } + // Font object + $this->fonts[$k]['n'] = $this->n + 1; + $type = $font['type']; + $name = $font['name']; + if (isset($font['subsetted'])) + $name = 'AAAAAA+' . $name; + if ($type == 'Core') { + // Core font + $this->_newobj(); + $this->_put('<_put('/BaseFont /' . $name); + $this->_put('/Subtype /Type1'); + if ($name != 'Symbol' && $name != 'ZapfDingbats') + $this->_put('/Encoding /WinAnsiEncoding'); + if (isset($font['uv'])) + $this->_put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + } elseif ($type == 'Type1' || $type == 'TrueType') { + // Additional Type1 or TrueType/OpenType font + $this->_newobj(); + $this->_put('<_put('/BaseFont /' . $name); + $this->_put('/Subtype /' . $type); + $this->_put('/FirstChar 32 /LastChar 255'); + $this->_put('/Widths ' . ($this->n + 1) . ' 0 R'); + $this->_put('/FontDescriptor ' . ($this->n + 2) . ' 0 R'); + if (isset($font['diff'])) + $this->_put('/Encoding ' . $this->encodings[$font['enc']] . ' 0 R'); + else + $this->_put('/Encoding /WinAnsiEncoding'); + if (isset($font['uv'])) + $this->_put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + // Widths + $this->_newobj(); + $cw = &$font['cw']; + $s = '['; + for ($i = 32; $i <= 255; $i++) + $s .= $cw[chr($i)] . ' '; + $this->_put($s . ']'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '< $v) + $s .= ' /' . $k . ' ' . $v; + if (!empty($font['file'])) + $s .= ' /FontFile' . ($type == 'Type1' ? '' : '2') . ' ' . $this->FontFiles[$font['file']]['n'] . ' 0 R'; + $this->_put($s . '>>'); + $this->_put('endobj'); + } else { + // Allow for additional types + $mtd = '_put' . strtolower($type); + if (!method_exists($this, $mtd)) + $this->Error('Unsupported font type: ' . $type); + $this->$mtd($font); + } + } + } + + protected function _tounicodecmap($uv) + { + $ranges = ''; + $nbr = 0; + $chars = ''; + $nbc = 0; + foreach ($uv as $c => $v) { + if (is_array($v)) { + $ranges .= sprintf("<%02X> <%02X> <%04X>\n", $c, $c + $v[1] - 1, $v[0]); + $nbr++; + } else { + $chars .= sprintf("<%02X> <%04X>\n", $c, $v); + $nbc++; + } + } + $s = "/CIDInit /ProcSet findresource begin\n"; + $s .= "12 dict begin\n"; + $s .= "begincmap\n"; + $s .= "/CIDSystemInfo\n"; + $s .= "< 0) { + $s .= "$nbr beginbfrange\n"; + $s .= $ranges; + $s .= "endbfrange\n"; + } + if ($nbc > 0) { + $s .= "$nbc beginbfchar\n"; + $s .= $chars; + $s .= "endbfchar\n"; + } + $s .= "endcmap\n"; + $s .= "CMapName currentdict /CMap defineresource pop\n"; + $s .= "end\n"; + $s .= "end"; + return $s; + } + + protected function _putimages() + { + foreach (array_keys($this->images) as $file) { + $this->_putimage($this->images[$file]); + unset($this->images[$file]['data']); + unset($this->images[$file]['smask']); + } + } + + protected function _putimage(&$info) + { + $this->_newobj(); + $info['n'] = $this->n; + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width ' . $info['w']); + $this->_put('/Height ' . $info['h']); + if ($info['cs'] == 'Indexed') + $this->_put('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal']) / 3 - 1) . ' ' . ($this->n + 1) . ' 0 R]'); + else { + $this->_put('/ColorSpace /' . $info['cs']); + if ($info['cs'] == 'DeviceCMYK') + $this->_put('/Decode [1 0 1 0 1 0 1 0]'); + } + $this->_put('/BitsPerComponent ' . $info['bpc']); + if (isset($info['f'])) + $this->_put('/Filter /' . $info['f']); + if (isset($info['dp'])) + $this->_put('/DecodeParms <<' . $info['dp'] . '>>'); + if (isset($info['trns']) && is_array($info['trns'])) { + $trns = ''; + for ($i = 0; $i < count($info['trns']); $i++) + $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' '; + $this->_put('/Mask [' . $trns . ']'); + } + if (isset($info['smask'])) + $this->_put('/SMask ' . ($this->n + 1) . ' 0 R'); + $this->_put('/Length ' . strlen($info['data']) . '>>'); + $this->_putstream($info['data']); + $this->_put('endobj'); + // Soft mask + if (isset($info['smask'])) { + $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $info['w']; + $smask = array('w' => $info['w'], 'h' => $info['h'], 'cs' => 'DeviceGray', 'bpc' => 8, 'f' => $info['f'], 'dp' => $dp, 'data' => $info['smask']); + $this->_putimage($smask); + } + // Palette + if ($info['cs'] == 'Indexed') + $this->_putstreamobject($info['pal']); + } + + protected function _putxobjectdict() + { + foreach ($this->images as $image) + $this->_put('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + + protected function _putresourcedict() + { + $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_put('/Font <<'); + foreach ($this->fonts as $font) + $this->_put('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + $this->_put('>>'); + $this->_put('/XObject <<'); + $this->_putxobjectdict(); + $this->_put('>>'); + } + + protected function _putresources() + { + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj(2); + $this->_put('<<'); + $this->_putresourcedict(); + $this->_put('>>'); + $this->_put('endobj'); + } + + protected function _putinfo() + { + $this->metadata['Producer'] = 'FPDF ' . FPDF_VERSION; + $this->metadata['CreationDate'] = 'D:' . @date('YmdHis'); + foreach ($this->metadata as $key => $value) + $this->_put('/' . $key . ' ' . $this->_textstring($value)); + } + + protected function _putcatalog() + { + $n = $this->PageInfo[1]['n']; + $this->_put('/Type /Catalog'); + $this->_put('/Pages 1 0 R'); + if ($this->ZoomMode == 'fullpage') + $this->_put('/OpenAction [' . $n . ' 0 R /Fit]'); + elseif ($this->ZoomMode == 'fullwidth') + $this->_put('/OpenAction [' . $n . ' 0 R /FitH null]'); + elseif ($this->ZoomMode == 'real') + $this->_put('/OpenAction [' . $n . ' 0 R /XYZ null null 1]'); + elseif (!is_string($this->ZoomMode)) + $this->_put('/OpenAction [' . $n . ' 0 R /XYZ null null ' . sprintf('%.2F', $this->ZoomMode / 100) . ']'); + if ($this->LayoutMode == 'single') + $this->_put('/PageLayout /SinglePage'); + elseif ($this->LayoutMode == 'continuous') + $this->_put('/PageLayout /OneColumn'); + elseif ($this->LayoutMode == 'two') + $this->_put('/PageLayout /TwoColumnLeft'); + } + + protected function _putheader() + { + $this->_put('%PDF-' . $this->PDFVersion); + } + + protected function _puttrailer() + { + $this->_put('/Size ' . ($this->n + 1)); + $this->_put('/Root ' . $this->n . ' 0 R'); + $this->_put('/Info ' . ($this->n - 1) . ' 0 R'); + } + + protected function _enddoc() + { + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + // Info + $this->_newobj(); + $this->_put('<<'); + $this->_putinfo(); + $this->_put('>>'); + $this->_put('endobj'); + // Catalog + $this->_newobj(); + $this->_put('<<'); + $this->_putcatalog(); + $this->_put('>>'); + $this->_put('endobj'); + // Cross-ref + $offset = $this->_getoffset(); + $this->_put('xref'); + $this->_put('0 ' . ($this->n + 1)); + $this->_put('0000000000 65535 f '); + for ($i = 1; $i <= $this->n; $i++) + $this->_put(sprintf('%010d 00000 n ', $this->offsets[$i])); + // Trailer + $this->_put('trailer'); + $this->_put('<<'); + $this->_puttrailer(); + $this->_put('>>'); + $this->_put('startxref'); + $this->_put($offset); + $this->_put('%%EOF'); + $this->state = 3; + } +} diff --git a/Fpdi/FpdfTpl.php b/Fpdi/FpdfTpl.php new file mode 100644 index 0000000..c4ce68f --- /dev/null +++ b/Fpdi/FpdfTpl.php @@ -0,0 +1,21 @@ +currentTemplateId !== null) { + throw new \BadMethodCallException('The page format cannot be changed when writing to a template.'); + } + + if (!\in_array($orientation, ['P', 'L'], true)) { + throw new \InvalidArgumentException(\sprintf( + 'Invalid page orientation "%s"! Only "P" and "L" are allowed!', + $orientation + )); + } + + $size = $this->_getpagesize($size); + + if ( + $orientation != $this->CurOrientation + || $size[0] != $this->CurPageSize[0] + || $size[1] != $this->CurPageSize[1] + ) { + // New size or orientation + if ($orientation === 'P') { + $this->w = $size[0]; + $this->h = $size[1]; + } else { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + $this->PageBreakTrigger = $this->h - $this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + } + } + + /** + * Draws a template onto the page or another template. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param array|float|int $x The abscissa of upper-left corner. Alternatively you could use an assoc array + * with the keys "x", "y", "width", "height", "adjustPageSize". + * @param float|int $y The ordinate of upper-left corner. + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @param bool $adjustPageSize + * @return array The size + * @see FpdfTplTrait::getTemplateSize() + */ + public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + if (!isset($this->templates[$tpl])) { + throw new \InvalidArgumentException('Template does not exist!'); + } + + if (\is_array($x)) { + unset($x['tpl']); + \extract($x, EXTR_IF_EXISTS); + /** @noinspection NotOptimalIfConditionsInspection */ + /** @noinspection PhpConditionAlreadyCheckedInspection */ + if (\is_array($x)) { + $x = 0; + } + } + + $template = $this->templates[$tpl]; + + $originalSize = $this->getTemplateSize($tpl); + $newSize = $this->getTemplateSize($tpl, $width, $height); + if ($adjustPageSize) { + $this->setPageFormat($newSize, $newSize['orientation']); + } + + $this->_out( + // reset standard values, translate and scale + \sprintf( + 'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q', + ($newSize['width'] / $originalSize['width']), + ($newSize['height'] / $originalSize['height']), + $x * $this->k, + ($this->h - $y - $newSize['height']) * $this->k, + $template['id'] + ) + ); + + return $newSize; + } + + /** + * Get the size of a template. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) + */ + public function getTemplateSize($tpl, $width = null, $height = null) + { + if (!isset($this->templates[$tpl])) { + return false; + } + + if ($width === null && $height === null) { + $width = $this->templates[$tpl]['width']; + $height = $this->templates[$tpl]['height']; + } elseif ($width === null) { + $width = $height * $this->templates[$tpl]['width'] / $this->templates[$tpl]['height']; + } + + if ($height === null) { + $height = $width * $this->templates[$tpl]['height'] / $this->templates[$tpl]['width']; + } + + if ($height <= 0. || $width <= 0.) { + throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); + } + + return [ + 'width' => $width, + 'height' => $height, + 0 => $width, + 1 => $height, + 'orientation' => $width > $height ? 'L' : 'P' + ]; + } + + /** + * Begins a new template. + * + * @param float|int|null $width The width of the template. If null, the current page width is used. + * @param float|int|null $height The height of the template. If null, the current page height is used. + * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). + * @return int A template identifier. + */ + public function beginTemplate($width = null, $height = null, $groupXObject = false) + { + if ($width === null) { + $width = $this->w; + } + + if ($height === null) { + $height = $this->h; + } + + $templateId = $this->getNextTemplateId(); + + // initiate buffer with current state of FPDF + $buffer = "2 J\n" + . \sprintf('%.2F w', $this->LineWidth * $this->k) . "\n"; + + if ($this->FontFamily) { + $buffer .= \sprintf("BT /F%d %.2F Tf ET\n", $this->CurrentFont['i'], $this->FontSizePt); + } + + if ($this->DrawColor !== '0 G') { + $buffer .= $this->DrawColor . "\n"; + } + if ($this->FillColor !== '0 g') { + $buffer .= $this->FillColor . "\n"; + } + + if ($groupXObject && \version_compare('1.4', $this->PDFVersion, '>')) { + $this->PDFVersion = '1.4'; + } + + $this->templates[$templateId] = [ + 'objectNumber' => null, + 'id' => 'TPL' . $templateId, + 'buffer' => $buffer, + 'width' => $width, + 'height' => $height, + 'groupXObject' => $groupXObject, + 'state' => [ + 'x' => $this->x, + 'y' => $this->y, + 'AutoPageBreak' => $this->AutoPageBreak, + 'bMargin' => $this->bMargin, + 'tMargin' => $this->tMargin, + 'lMargin' => $this->lMargin, + 'rMargin' => $this->rMargin, + 'h' => $this->h, + 'w' => $this->w, + 'FontFamily' => $this->FontFamily, + 'FontStyle' => $this->FontStyle, + 'FontSizePt' => $this->FontSizePt, + 'FontSize' => $this->FontSize, + 'underline' => $this->underline, + 'TextColor' => $this->TextColor, + 'DrawColor' => $this->DrawColor, + 'FillColor' => $this->FillColor, + 'ColorFlag' => $this->ColorFlag + ] + ]; + + $this->SetAutoPageBreak(false); + $this->currentTemplateId = $templateId; + + $this->h = $height; + $this->w = $width; + + $this->SetXY($this->lMargin, $this->tMargin); + $this->SetRightMargin($this->w - $width + $this->rMargin); + + return $templateId; + } + + /** + * Ends a template. + * + * @return bool|int|null A template identifier. + */ + public function endTemplate() + { + if ($this->currentTemplateId === null) { + return false; + } + + $templateId = $this->currentTemplateId; + $template = $this->templates[$templateId]; + + $state = $template['state']; + $this->SetXY($state['x'], $state['y']); + $this->tMargin = $state['tMargin']; + $this->lMargin = $state['lMargin']; + $this->rMargin = $state['rMargin']; + $this->h = $state['h']; + $this->w = $state['w']; + $this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']); + + $this->FontFamily = $state['FontFamily']; + $this->FontStyle = $state['FontStyle']; + $this->FontSizePt = $state['FontSizePt']; + $this->FontSize = $state['FontSize']; + + $this->TextColor = $state['TextColor']; + $this->DrawColor = $state['DrawColor']; + $this->FillColor = $state['FillColor']; + $this->ColorFlag = $state['ColorFlag']; + + $this->underline = $state['underline']; + + $fontKey = $this->FontFamily . $this->FontStyle; + if ($fontKey) { + $this->CurrentFont =& $this->fonts[$fontKey]; + } else { + unset($this->CurrentFont); + } + + $this->currentTemplateId = null; + + return $templateId; + } + + /** + * Get the next template id. + * + * @return int + */ + protected function getNextTemplateId() + { + return $this->templateId++; + } + + /* overwritten FPDF methods: */ + + /** + * @inheritdoc + */ + public function AddPage($orientation = '', $size = '', $rotation = 0) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('Pages cannot be added when writing to a template.'); + } + parent::AddPage($orientation, $size, $rotation); + } + + /** + * @inheritdoc + */ + public function Link($x, $y, $w, $h, $link) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('Links cannot be set when writing to a template.'); + } + parent::Link($x, $y, $w, $h, $link); + } + + /** + * @inheritdoc + */ + public function SetLink($link, $y = 0, $page = -1) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('Links cannot be set when writing to a template.'); + } + return parent::SetLink($link, $y, $page); + } + + /** + * @inheritdoc + */ + public function SetDrawColor($r, $g = null, $b = null) + { + parent::SetDrawColor($r, $g, $b); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out($this->DrawColor); + } + } + + /** + * @inheritdoc + */ + public function SetFillColor($r, $g = null, $b = null) + { + parent::SetFillColor($r, $g, $b); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out($this->FillColor); + } + } + + /** + * @inheritdoc + */ + public function SetLineWidth($width) + { + parent::SetLineWidth($width); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out(\sprintf('%.2F w', $width * $this->k)); + } + } + + /** + * @inheritdoc + */ + public function SetFont($family, $style = '', $size = 0) + { + parent::SetFont($family, $style, $size); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out(\sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + } + + /** + * @inheritdoc + */ + public function SetFontSize($size) + { + parent::SetFontSize($size); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + } + + /** + * @inheritdoc + */ + protected function _putimages() + { + parent::_putimages(); + + foreach ($this->templates as $key => $template) { + $this->_newobj(); + $this->templates[$key]['objectNumber'] = $this->n; + + $this->_put('<_put(\sprintf( + '/BBox[0 0 %.2F %.2F]', + $template['width'] * $this->k, + $template['height'] * $this->k + )); + $this->_put('/Resources 2 0 R'); // default resources dictionary of FPDF + + if ($this->compress) { + $buffer = \gzcompress($template['buffer']); + $this->_put('/Filter/FlateDecode'); + } else { + $buffer = $template['buffer']; + } + + $this->_put('/Length ' . \strlen($buffer)); + + if ($template['groupXObject']) { + $this->_put('/Group <>'); + } + + $this->_put('>>'); + $this->_putstream($buffer); + $this->_put('endobj'); + } + } + + /** + * @inheritdoc + */ + protected function _putxobjectdict() + { + foreach ($this->templates as $key => $template) { + $this->_put('/' . $template['id'] . ' ' . $template['objectNumber'] . ' 0 R'); + } + + parent::_putxobjectdict(); + } + + /** + * @inheritdoc + */ + public function _out($s) + { + if ($this->currentTemplateId !== null) { + $this->templates[$this->currentTemplateId]['buffer'] .= $s . "\n"; + } else { + parent::_out($s); + } + } +} diff --git a/Fpdi/Fpdi.php b/Fpdi/Fpdi.php new file mode 100644 index 0000000..dbbff28 --- /dev/null +++ b/Fpdi/Fpdi.php @@ -0,0 +1,153 @@ +cleanUp(); + } + + /** + * Draws an imported page or a template onto the page or another template. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array + * with the keys "x", "y", "width", "height", "adjustPageSize". + * @param float|int $y The ordinate of upper-left corner. + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @param bool $adjustPageSize + * @return array The size + * @see Fpdi::getTemplateSize() + */ + public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + if (isset($this->importedPages[$tpl])) { + $size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); + if ($this->currentTemplateId !== null) { + $this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl; + } + return $size; + } + + return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize); + } + + /** + * Get the size of an imported page or template. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) + */ + public function getTemplateSize($tpl, $width = null, $height = null) + { + $size = parent::getTemplateSize($tpl, $width, $height); + if ($size === false) { + return $this->getImportedPageSize($tpl, $width, $height); + } + + return $size; + } + + /** + * @inheritdoc + * @throws CrossReferenceException + * @throws PdfParserException + */ + protected function _putimages() + { + $this->currentReaderId = null; + parent::_putimages(); + + foreach ($this->importedPages as $key => $pageData) { + $this->_newobj(); + $this->importedPages[$key]['objectNumber'] = $this->n; + $this->currentReaderId = $pageData['readerId']; + $this->writePdfType($pageData['stream']); + $this->_put('endobj'); + } + + foreach (\array_keys($this->readers) as $readerId) { + $parser = $this->getPdfReader($readerId)->getParser(); + $this->currentReaderId = $readerId; + + while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { + try { + $object = $parser->getIndirectObject($objectNumber); + } catch (CrossReferenceException $e) { + if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { + $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); + } else { + throw $e; + } + } + + $this->writePdfType($object); + } + } + + $this->currentReaderId = null; + } + + /** + * @inheritdoc + */ + protected function _putxobjectdict() + { + foreach ($this->importedPages as $key => $pageData) { + $this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R'); + } + + parent::_putxobjectdict(); + } + + /** + * @inheritdoc + */ + protected function _put($s, $newLine = true) + { + if ($newLine) { + $this->buffer .= $s . "\n"; + } else { + $this->buffer .= $s; + } + } +} diff --git a/Fpdi/FpdiException.php b/Fpdi/FpdiException.php new file mode 100644 index 0000000..34e9fce --- /dev/null +++ b/Fpdi/FpdiException.php @@ -0,0 +1,18 @@ +readers) : $this->createdReaders; + foreach ($readers as $id) { + $this->readers[$id]->getParser()->getStreamReader()->cleanUp(); + unset($this->readers[$id]); + } + + $this->createdReaders = []; + } + + /** + * Set the minimal PDF version. + * + * @param string $pdfVersion + */ + protected function setMinPdfVersion($pdfVersion) + { + if (\version_compare($pdfVersion, $this->PDFVersion, '>')) { + $this->PDFVersion = $pdfVersion; + } + } + + /** @noinspection PhpUndefinedClassInspection */ + /** + * Get a new pdf parser instance. + * + * @param StreamReader $streamReader + * @return PdfParser|FpdiPdfParser + */ + protected function getPdfParserInstance(StreamReader $streamReader) + { + // note: if you get an exception here - turn off errors/warnings on not found for your autoloader. + // psr-4 (https://www.php-fig.org/psr/psr-4/) says: Autoloader implementations MUST NOT throw + // exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value. + /** @noinspection PhpUndefinedClassInspection */ + if (\class_exists(FpdiPdfParser::class)) { + /** @noinspection PhpUndefinedClassInspection */ + return new FpdiPdfParser($streamReader); + } + + return new PdfParser($streamReader); + } + + /** + * Get an unique reader id by the $file parameter. + * + * @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader + * instance or a StreamReader instance. + * @return string + */ + protected function getPdfReaderId($file) + { + if (\is_resource($file)) { + $id = (string) $file; + } elseif (\is_string($file)) { + $id = \realpath($file); + if ($id === false) { + $id = $file; + } + } elseif (\is_object($file)) { + $id = \spl_object_hash($file); + } else { + throw new \InvalidArgumentException( + \sprintf('Invalid type in $file parameter (%s)', \gettype($file)) + ); + } + + /** @noinspection OffsetOperationsInspection */ + if (isset($this->readers[$id])) { + return $id; + } + + if (\is_resource($file)) { + $streamReader = new StreamReader($file); + } elseif (\is_string($file)) { + $streamReader = StreamReader::createByFile($file); + $this->createdReaders[] = $id; + } else { + $streamReader = $file; + } + + $reader = new PdfReader($this->getPdfParserInstance($streamReader)); + /** @noinspection OffsetOperationsInspection */ + $this->readers[$id] = $reader; + + return $id; + } + + /** + * Get a pdf reader instance by its id. + * + * @param string $id + * @return PdfReader + */ + protected function getPdfReader($id) + { + if (isset($this->readers[$id])) { + return $this->readers[$id]; + } + + throw new \InvalidArgumentException( + \sprintf('No pdf reader with the given id (%s) exists.', $id) + ); + } + + /** + * Set the source PDF file. + * + * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. + * @return int The page count of the PDF document. + * @throws PdfParserException + */ + public function setSourceFile($file) + { + $this->currentReaderId = $this->getPdfReaderId($file); + $this->objectsToCopy[$this->currentReaderId] = []; + + $reader = $this->getPdfReader($this->currentReaderId); + $this->setMinPdfVersion($reader->getPdfVersion()); + + return $reader->getPageCount(); + } + + /** + * Imports a page. + * + * @param int $pageNumber The page number. + * @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX. + * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). + * @return string A unique string identifying the imported page. + * @throws CrossReferenceException + * @throws FilterException + * @throws PdfParserException + * @throws PdfTypeException + * @throws PdfReaderException + * @see PageBoundaries + */ + public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true) + { + if (null === $this->currentReaderId) { + throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.'); + } + + $pageId = $this->currentReaderId; + + $pageNumber = (int)$pageNumber; + $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0'); + + // for backwards compatibility with FPDI 1 + $box = \ltrim($box, '/'); + if (!PageBoundaries::isValidName($box)) { + throw new \InvalidArgumentException( + \sprintf('Box name is invalid: "%s"', $box) + ); + } + + $pageId .= '|' . $box; + + if (isset($this->importedPages[$pageId])) { + return $pageId; + } + + $reader = $this->getPdfReader($this->currentReaderId); + $page = $reader->getPage($pageNumber); + + $bbox = $page->getBoundary($box); + if ($bbox === false) { + throw new PdfReaderException( + \sprintf("Page doesn't have a boundary box (%s).", $box), + PdfReaderException::MISSING_DATA + ); + } + + $dict = new PdfDictionary(); + $dict->value['Type'] = PdfName::create('XObject'); + $dict->value['Subtype'] = PdfName::create('Form'); + $dict->value['FormType'] = PdfNumeric::create(1); + $dict->value['BBox'] = $bbox->toPdfArray(); + + if ($groupXObject) { + $this->setMinPdfVersion('1.4'); + $dict->value['Group'] = PdfDictionary::create([ + 'Type' => PdfName::create('Group'), + 'S' => PdfName::create('Transparency') + ]); + } + + $resources = $page->getAttribute('Resources'); + if ($resources !== null) { + $dict->value['Resources'] = $resources; + } + + list($width, $height) = $page->getWidthAndHeight($box); + + $a = 1; + $b = 0; + $c = 0; + $d = 1; + $e = -$bbox->getLlx(); + $f = -$bbox->getLly(); + + $rotation = $page->getRotation(); + + if ($rotation !== 0) { + $rotation *= -1; + $angle = $rotation * M_PI / 180; + $a = \cos($angle); + $b = \sin($angle); + $c = -$b; + $d = $a; + + switch ($rotation) { + case -90: + $e = -$bbox->getLly(); + $f = $bbox->getUrx(); + break; + case -180: + $e = $bbox->getUrx(); + $f = $bbox->getUry(); + break; + case -270: + $e = $bbox->getUry(); + $f = -$bbox->getLlx(); + break; + } + } + + // we need to rotate/translate + if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) { + $dict->value['Matrix'] = PdfArray::create([ + PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c), + PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f) + ]); + } + + // try to use the existing content stream + $pageDict = $page->getPageDictionary(); + + try { + $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true); + $contents = PdfType::resolve($contentsObject, $reader->getParser()); + + // just copy the stream reference if it is only a single stream + if ( + ($contentsIsStream = ($contents instanceof PdfStream)) + || ($contents instanceof PdfArray && \count($contents->value) === 1) + ) { + if ($contentsIsStream) { + /** + * @var PdfIndirectObject $contentsObject + */ + $stream = $contents; + } else { + $stream = PdfType::resolve($contents->value[0], $reader->getParser()); + } + + $filter = PdfDictionary::get($stream->value, 'Filter'); + if (!$filter instanceof PdfNull) { + $dict->value['Filter'] = $filter; + } + $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser()); + $dict->value['Length'] = $length; + $stream->value = $dict; + // otherwise extract it from the array and re-compress the whole stream + } else { + $streamContent = $this->compress + ? \gzcompress($page->getContentStream()) + : $page->getContentStream(); + + $dict->value['Length'] = PdfNumeric::create(\strlen($streamContent)); + if ($this->compress) { + $dict->value['Filter'] = PdfName::create('FlateDecode'); + } + + $stream = PdfStream::create($dict, $streamContent); + } + // Catch faulty pages and use an empty content stream + } catch (FpdiException $e) { + $dict->value['Length'] = PdfNumeric::create(0); + $stream = PdfStream::create($dict, ''); + } + + $this->importedPages[$pageId] = [ + 'objectNumber' => null, + 'readerId' => $this->currentReaderId, + 'id' => 'TPL' . $this->getNextTemplateId(), + 'width' => $width / $this->k, + 'height' => $height / $this->k, + 'stream' => $stream + ]; + + return $pageId; + } + + /** + * Draws an imported page onto the page. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $pageId The page id + * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array + * with the keys "x", "y", "width", "height", "adjustPageSize". + * @param float|int $y The ordinate of upper-left corner. + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @param bool $adjustPageSize + * @return array The size. + * @see Fpdi::getTemplateSize() + */ + public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + if (\is_array($x)) { + /** @noinspection OffsetOperationsInspection */ + unset($x['pageId']); + \extract($x, EXTR_IF_EXISTS); + /** @noinspection NotOptimalIfConditionsInspection */ + if (\is_array($x)) { + $x = 0; + } + } + + if (!isset($this->importedPages[$pageId])) { + throw new \InvalidArgumentException('Imported page does not exist!'); + } + + $importedPage = $this->importedPages[$pageId]; + + $originalSize = $this->getTemplateSize($pageId); + $newSize = $this->getTemplateSize($pageId, $width, $height); + if ($adjustPageSize) { + $this->setPageFormat($newSize, $newSize['orientation']); + } + + $this->_out( + // reset standard values, translate and scale + \sprintf( + 'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q', + ($newSize['width'] / $originalSize['width']), + ($newSize['height'] / $originalSize['height']), + $x * $this->k, + ($this->h - $y - $newSize['height']) * $this->k, + $importedPage['id'] + ) + ); + + return $newSize; + } + + /** + * Get the size of an imported page. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) + */ + public function getImportedPageSize($tpl, $width = null, $height = null) + { + if (isset($this->importedPages[$tpl])) { + $importedPage = $this->importedPages[$tpl]; + + if ($width === null && $height === null) { + $width = $importedPage['width']; + $height = $importedPage['height']; + } elseif ($width === null) { + $width = $height * $importedPage['width'] / $importedPage['height']; + } + + if ($height === null) { + $height = $width * $importedPage['height'] / $importedPage['width']; + } + + if ($height <= 0. || $width <= 0.) { + throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); + } + + return [ + 'width' => $width, + 'height' => $height, + 0 => $width, + 1 => $height, + 'orientation' => $width > $height ? 'L' : 'P' + ]; + } + + return false; + } + + /** + * Writes a PdfType object to the resulting buffer. + * + * @param PdfType $value + * @throws PdfTypeException + */ + protected function writePdfType(PdfType $value) + { + if ($value instanceof PdfNumeric) { + if (\is_int($value->value)) { + $this->_put($value->value . ' ', false); + } else { + $this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false); + } + } elseif ($value instanceof PdfName) { + $this->_put('/' . $value->value . ' ', false); + } elseif ($value instanceof PdfString) { + $this->_put('(' . $value->value . ')', false); + } elseif ($value instanceof PdfHexString) { + $this->_put('<' . $value->value . '>'); + } elseif ($value instanceof PdfBoolean) { + $this->_put($value->value ? 'true ' : 'false ', false); + } elseif ($value instanceof PdfArray) { + $this->_put('[', false); + foreach ($value->value as $entry) { + $this->writePdfType($entry); + } + $this->_put(']'); + } elseif ($value instanceof PdfDictionary) { + $this->_put('<<', false); + foreach ($value->value as $name => $entry) { + $this->_put('/' . $name . ' ', false); + $this->writePdfType($entry); + } + $this->_put('>>'); + } elseif ($value instanceof PdfToken) { + $this->_put($value->value); + } elseif ($value instanceof PdfNull) { + $this->_put('null '); + } elseif ($value instanceof PdfStream) { + /** + * @var $value PdfStream + */ + $this->writePdfType($value->value); + $this->_put('stream'); + $this->_put($value->getStream()); + $this->_put('endstream'); + } elseif ($value instanceof PdfIndirectObjectReference) { + if (!isset($this->objectMap[$this->currentReaderId])) { + $this->objectMap[$this->currentReaderId] = []; + } + + if (!isset($this->objectMap[$this->currentReaderId][$value->value])) { + $this->objectMap[$this->currentReaderId][$value->value] = ++$this->n; + $this->objectsToCopy[$this->currentReaderId][] = $value->value; + } + + $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false); + } elseif ($value instanceof PdfIndirectObject) { + /** + * @var PdfIndirectObject $value + */ + $n = $this->objectMap[$this->currentReaderId][$value->objectNumber]; + $this->_newobj($n); + $this->writePdfType($value->value); + $this->_put('endobj'); + } + } +} diff --git a/Fpdi/PdfParser/CrossReference/AbstractReader.php b/Fpdi/PdfParser/CrossReference/AbstractReader.php new file mode 100644 index 0000000..5e2b215 --- /dev/null +++ b/Fpdi/PdfParser/CrossReference/AbstractReader.php @@ -0,0 +1,95 @@ +parser = $parser; + $this->readTrailer(); + } + + /** + * Get the trailer dictionary. + * + * @return PdfDictionary + */ + public function getTrailer() + { + return $this->trailer; + } + + /** + * Read the trailer dictionary. + * + * @throws CrossReferenceException + * @throws PdfTypeException + */ + protected function readTrailer() + { + try { + $trailerKeyword = $this->parser->readValue(null, PdfToken::class); + if ($trailerKeyword->value !== 'trailer') { + throw new CrossReferenceException( + \sprintf( + 'Unexpected end of cross reference. "trailer"-keyword expected, got: %s.', + $trailerKeyword->value + ), + CrossReferenceException::UNEXPECTED_END + ); + } + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Unexpected end of cross reference. "trailer"-keyword expected, got an invalid object type.', + CrossReferenceException::UNEXPECTED_END, + $e + ); + } + + try { + $trailer = $this->parser->readValue(null, PdfDictionary::class); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Unexpected end of cross reference. Trailer not found.', + CrossReferenceException::UNEXPECTED_END, + $e + ); + } + + $this->trailer = $trailer; + } +} diff --git a/Fpdi/PdfParser/CrossReference/CrossReference.php b/Fpdi/PdfParser/CrossReference/CrossReference.php new file mode 100644 index 0000000..cfa725d --- /dev/null +++ b/Fpdi/PdfParser/CrossReference/CrossReference.php @@ -0,0 +1,326 @@ +parser = $parser; + $this->fileHeaderOffset = $fileHeaderOffset; + + $offset = $this->findStartXref(); + $reader = null; + /** @noinspection TypeUnsafeComparisonInspection */ + while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0 + try { + $reader = $this->readXref($offset + $this->fileHeaderOffset); + } catch (CrossReferenceException $e) { + // sometimes the file header offset is part of the byte offsets, so let's retry by resetting it to zero. + if ($e->getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) { + $this->fileHeaderOffset = 0; + $reader = $this->readXref($offset + $this->fileHeaderOffset); + } else { + throw $e; + } + } + + $trailer = $reader->getTrailer(); + $this->checkForEncryption($trailer); + $this->readers[] = $reader; + + if (isset($trailer->value['Prev'])) { + $offset = $trailer->value['Prev']->value; + } else { + $offset = false; + } + } + + // fix faulty sub-section header + if ($reader instanceof FixedReader) { + /** + * @var FixedReader $reader + */ + $reader->fixFaultySubSectionShift(); + } + + if ($reader === null) { + throw new CrossReferenceException('No cross-reference found.', CrossReferenceException::NO_XREF_FOUND); + } + } + + /** + * Get the size of the cross reference. + * + * @return integer + */ + public function getSize() + { + return $this->getTrailer()->value['Size']->value; + } + + /** + * Get the trailer dictionary. + * + * @return PdfDictionary + */ + public function getTrailer() + { + return $this->readers[0]->getTrailer(); + } + + /** + * Get the cross reference readser instances. + * + * @return ReaderInterface[] + */ + public function getReaders() + { + return $this->readers; + } + + /** + * Get the offset by an object number. + * + * @param int $objectNumber + * @return integer|bool + */ + public function getOffsetFor($objectNumber) + { + foreach ($this->getReaders() as $reader) { + $offset = $reader->getOffsetFor($objectNumber); + if ($offset !== false) { + return $offset; + } + } + + return false; + } + + /** + * Get an indirect object by its object number. + * + * @param int $objectNumber + * @return PdfIndirectObject + * @throws CrossReferenceException + */ + public function getIndirectObject($objectNumber) + { + $offset = $this->getOffsetFor($objectNumber); + if ($offset === false) { + throw new CrossReferenceException( + \sprintf('Object (id:%s) not found.', $objectNumber), + CrossReferenceException::OBJECT_NOT_FOUND + ); + } + + $parser = $this->parser; + + $parser->getTokenizer()->clearStack(); + $parser->getStreamReader()->reset($offset + $this->fileHeaderOffset); + + try { + /** @var PdfIndirectObject $object */ + $object = $parser->readValue(null, PdfIndirectObject::class); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + \sprintf('Object (id:%s) not found at location (%s).', $objectNumber, $offset), + CrossReferenceException::OBJECT_NOT_FOUND, + $e + ); + } + + if ($object->objectNumber !== $objectNumber) { + throw new CrossReferenceException( + \sprintf('Wrong object found, got %s while %s was expected.', $object->objectNumber, $objectNumber), + CrossReferenceException::OBJECT_NOT_FOUND + ); + } + + return $object; + } + + /** + * Read the cross-reference table at a given offset. + * + * Internally the method will try to evaluate the best reader for this cross-reference. + * + * @param int $offset + * @return ReaderInterface + * @throws CrossReferenceException + * @throws PdfTypeException + */ + protected function readXref($offset) + { + $this->parser->getStreamReader()->reset($offset); + $this->parser->getTokenizer()->clearStack(); + $initValue = $this->parser->readValue(); + + return $this->initReaderInstance($initValue); + } + + /** + * Get a cross-reference reader instance. + * + * @param PdfToken|PdfIndirectObject $initValue + * @return ReaderInterface|bool + * @throws CrossReferenceException + * @throws PdfTypeException + */ + protected function initReaderInstance($initValue) + { + $position = $this->parser->getStreamReader()->getPosition() + + $this->parser->getStreamReader()->getOffset() + $this->fileHeaderOffset; + + if ($initValue instanceof PdfToken && $initValue->value === 'xref') { + try { + return new FixedReader($this->parser); + } catch (CrossReferenceException $e) { + $this->parser->getStreamReader()->reset($position); + $this->parser->getTokenizer()->clearStack(); + + return new LineReader($this->parser); + } + } + + if ($initValue instanceof PdfIndirectObject) { + try { + $stream = PdfStream::ensure($initValue->value); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Invalid object type at xref reference offset.', + CrossReferenceException::INVALID_DATA, + $e + ); + } + + $type = PdfDictionary::get($stream->value, 'Type'); + if ($type->value !== 'XRef') { + throw new CrossReferenceException( + 'The xref position points to an incorrect object type.', + CrossReferenceException::INVALID_DATA + ); + } + + $this->checkForEncryption($stream->value); + + throw new CrossReferenceException( + 'This PDF document probably uses a compression technique which is not supported by the ' . + 'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)', + CrossReferenceException::COMPRESSED_XREF + ); + } + + throw new CrossReferenceException( + 'The xref position points to an incorrect object type.', + CrossReferenceException::INVALID_DATA + ); + } + + /** + * Check for encryption. + * + * @param PdfDictionary $dictionary + * @throws CrossReferenceException + */ + protected function checkForEncryption(PdfDictionary $dictionary) + { + if (isset($dictionary->value['Encrypt'])) { + throw new CrossReferenceException( + 'This PDF document is encrypted and cannot be processed with FPDI.', + CrossReferenceException::ENCRYPTED + ); + } + } + + /** + * Find the start position for the first cross-reference. + * + * @return int The byte-offset position of the first cross-reference. + * @throws CrossReferenceException + */ + protected function findStartXref() + { + $reader = $this->parser->getStreamReader(); + $reader->reset(-self::$trailerSearchLength, self::$trailerSearchLength); + + $buffer = $reader->getBuffer(false); + $pos = \strrpos($buffer, 'startxref'); + $addOffset = 9; + if ($pos === false) { + // Some corrupted documents uses startref, instead of startxref + $pos = \strrpos($buffer, 'startref'); + if ($pos === false) { + throw new CrossReferenceException( + 'Unable to find pointer to xref table', + CrossReferenceException::NO_STARTXREF_FOUND + ); + } + $addOffset = 8; + } + + $reader->setOffset($pos + $addOffset); + + try { + $value = $this->parser->readValue(null, PdfNumeric::class); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Invalid data after startxref keyword.', + CrossReferenceException::INVALID_DATA, + $e + ); + } + + return $value->value; + } +} diff --git a/Fpdi/PdfParser/CrossReference/CrossReferenceException.php b/Fpdi/PdfParser/CrossReference/CrossReferenceException.php new file mode 100644 index 0000000..52c7ee1 --- /dev/null +++ b/Fpdi/PdfParser/CrossReference/CrossReferenceException.php @@ -0,0 +1,79 @@ +reader = $parser->getStreamReader(); + $this->read(); + parent::__construct($parser); + } + + /** + * Get all subsection data. + * + * @return array + */ + public function getSubSections() + { + return $this->subSections; + } + + /** + * @inheritdoc + */ + public function getOffsetFor($objectNumber) + { + foreach ($this->subSections as $offset => list($startObject, $objectCount)) { + /** + * @var int $startObject + * @var int $objectCount + */ + if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) { + $position = $offset + 20 * ($objectNumber - $startObject); + $this->reader->ensure($position, 20); + $line = $this->reader->readBytes(20); + if ($line[17] === 'f') { + return false; + } + + return (int) \substr($line, 0, 10); + } + } + + return false; + } + + /** + * Read the cross-reference. + * + * This reader will only read the subsections in this method. The offsets were resolved individually by this + * information. + * + * @throws CrossReferenceException + */ + protected function read() + { + $subSections = []; + + $startObject = $entryCount = $lastLineStart = null; + $validityChecked = false; + while (($line = $this->reader->readLine(20)) !== false) { + if (\strpos($line, 'trailer') !== false) { + $this->reader->reset($lastLineStart); + break; + } + + // jump over if line content doesn't match the expected string + if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) { + continue; + } + + $oldPosition = $this->reader->getPosition(); + $position = $oldPosition + $this->reader->getOffset(); + + if (!$validityChecked && $entryCount > 0) { + $nextLine = $this->reader->readBytes(21); + /* Check the next line for maximum of 20 bytes and not longer + * By catching 21 bytes and trimming the length should be still 21. + */ + if (\strlen(\trim($nextLine)) !== 21) { + throw new CrossReferenceException( + 'Cross-reference entries are larger than 20 bytes.', + CrossReferenceException::ENTRIES_TOO_LARGE + ); + } + + /* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes. + * If it would have less bytes the substring would get the first bytes of the next line which would + * evaluate to a 20 bytes long string after trimming. + */ + if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) { + throw new CrossReferenceException( + 'Cross-reference entries are less than 20 bytes.', + CrossReferenceException::ENTRIES_TOO_SHORT + ); + } + + $validityChecked = true; + } + + $subSections[$position] = [$startObject, $entryCount]; + + $lastLineStart = $position + $entryCount * 20; + $this->reader->reset($lastLineStart); + } + + // reset after the last correct parsed line + $this->reader->reset($lastLineStart); + + if (\count($subSections) === 0) { + throw new CrossReferenceException( + 'No entries found in cross-reference.', + CrossReferenceException::NO_ENTRIES + ); + } + + $this->subSections = $subSections; + } + + /** + * Fixes an invalid object number shift. + * + * This method can be used to repair documents with an invalid subsection header: + * + * + * xref + * 1 7 + * 0000000000 65535 f + * 0000000009 00000 n + * 0000412075 00000 n + * 0000412172 00000 n + * 0000412359 00000 n + * 0000412417 00000 n + * 0000412468 00000 n + * + * + * It shall only be called on the first table. + * + * @return bool + */ + public function fixFaultySubSectionShift() + { + $subSections = $this->getSubSections(); + if (\count($subSections) > 1) { + return false; + } + + $subSection = \current($subSections); + if ($subSection[0] != 1) { + return false; + } + + if ($this->getOffsetFor(1) === false) { + foreach ($subSections as $offset => list($startObject, $objectCount)) { + $this->subSections[$offset] = [$startObject - 1, $objectCount]; + } + return true; + } + + return false; + } +} diff --git a/Fpdi/PdfParser/CrossReference/LineReader.php b/Fpdi/PdfParser/CrossReference/LineReader.php new file mode 100644 index 0000000..e557a91 --- /dev/null +++ b/Fpdi/PdfParser/CrossReference/LineReader.php @@ -0,0 +1,167 @@ + 20 bytes). + */ +class LineReader extends AbstractReader implements ReaderInterface +{ + /** + * The object offsets. + * + * @var array + */ + protected $offsets; + + /** + * LineReader constructor. + * + * @param PdfParser $parser + * @throws CrossReferenceException + */ + public function __construct(PdfParser $parser) + { + $this->read($this->extract($parser->getStreamReader())); + parent::__construct($parser); + } + + /** + * @inheritdoc + */ + public function getOffsetFor($objectNumber) + { + if (isset($this->offsets[$objectNumber])) { + return $this->offsets[$objectNumber][0]; + } + + return false; + } + + /** + * Get all found offsets. + * + * @return array + */ + public function getOffsets() + { + return $this->offsets; + } + + /** + * Extracts the cross reference data from the stream reader. + * + * @param StreamReader $reader + * @return string + * @throws CrossReferenceException + */ + protected function extract(StreamReader $reader) + { + $bytesPerCycle = 100; + $reader->reset(null, $bytesPerCycle); + + $cycles = 0; + do { + // 6 = length of "trailer" - 1 + $pos = \max(($bytesPerCycle * $cycles) - 6, 0); + $trailerPos = \strpos($reader->getBuffer(false), 'trailer', $pos); + $cycles++; + } while ($trailerPos === false && $reader->increaseLength($bytesPerCycle) !== false); + + if ($trailerPos === false) { + throw new CrossReferenceException( + 'Unexpected end of cross reference. "trailer"-keyword not found.', + CrossReferenceException::NO_TRAILER_FOUND + ); + } + + $xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos); + $reader->reset($reader->getPosition() + $trailerPos); + + return $xrefContent; + } + + /** + * Read the cross-reference entries. + * + * @param string $xrefContent + * @throws CrossReferenceException + */ + protected function read($xrefContent) + { + // get eol markers in the first 100 bytes + \preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m); + + if (\count($m[0]) === 0) { + throw new CrossReferenceException( + 'No data found in cross-reference.', + CrossReferenceException::INVALID_DATA + ); + } + + // count(array_count_values()) is faster then count(array_unique()) + // @see https://github.com/symfony/symfony/pull/23731 + // can be reverted for php7.2 + $differentLineEndings = \count(\array_count_values($m[0])); + if ($differentLineEndings > 1) { + $lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY); + } else { + $lines = \explode($m[0][0], $xrefContent); + } + + unset($differentLineEndings, $m); + if (!\is_array($lines)) { + $this->offsets = []; + return; + } + + $start = 0; + $offsets = []; + + // trim all lines and remove empty lines + $lines = \array_filter(\array_map('\trim', $lines)); + foreach ($lines as $line) { + $pieces = \explode(' ', $line); + + switch (\count($pieces)) { + case 2: + $start = (int) $pieces[0]; + break; + + case 3: + switch ($pieces[2]) { + case 'n': + $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]]; + $start++; + break 2; + case 'f': + $start++; + break 2; + } + // fall through if pieces doesn't match + + default: + throw new CrossReferenceException( + \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)), + CrossReferenceException::INVALID_DATA + ); + } + } + + $this->offsets = $offsets; + } +} diff --git a/Fpdi/PdfParser/CrossReference/ReaderInterface.php b/Fpdi/PdfParser/CrossReference/ReaderInterface.php new file mode 100644 index 0000000..4d3a929 --- /dev/null +++ b/Fpdi/PdfParser/CrossReference/ReaderInterface.php @@ -0,0 +1,34 @@ + + if ($ch === 126 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 62) { + break; + } + + if ($ch === 122 /* z */ && $state === 0) { + $out .= \chr(0) . \chr(0) . \chr(0) . \chr(0); + continue; + } + + if ($ch < 33 /* ! */ || $ch > 117 /* u */) { + throw new Ascii85Exception( + 'Illegal character found while ASCII85 decode.', + Ascii85Exception::ILLEGAL_CHAR_FOUND + ); + } + + $chn[$state] = $ch - 33;/* ! */ + $state++; + + if ($state === 5) { + $state = 0; + $r = 0; + for ($j = 0; $j < 5; ++$j) { + /** @noinspection UnnecessaryCastingInspection */ + $r = (int)($r * 85 + $chn[$j]); + } + + $out .= \chr($r >> 24) + . \chr($r >> 16) + . \chr($r >> 8) + . \chr($r); + } + } + + if ($state === 1) { + throw new Ascii85Exception( + 'Illegal length while ASCII85 decode.', + Ascii85Exception::ILLEGAL_LENGTH + ); + } + + if ($state === 2) { + $r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1] + 1) * 85 * 85 * 85; + $out .= \chr($r >> 24); + } elseif ($state === 3) { + $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2] + 1) * 85 * 85; + $out .= \chr($r >> 24); + $out .= \chr($r >> 16); + } elseif ($state === 4) { + $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3] + 1) * 85; + $out .= \chr($r >> 24); + $out .= \chr($r >> 16); + $out .= \chr($r >> 8); + } + + return $out; + } +} diff --git a/Fpdi/PdfParser/Filter/Ascii85Exception.php b/Fpdi/PdfParser/Filter/Ascii85Exception.php new file mode 100644 index 0000000..e9d27c5 --- /dev/null +++ b/Fpdi/PdfParser/Filter/Ascii85Exception.php @@ -0,0 +1,27 @@ +')); + if ((\strlen($data) % 2) === 1) { + $data .= '0'; + } + + return \pack('H*', $data); + } + + /** + * Converts a string into ASCII hexadecimal representation. + * + * @param string $data The input string + * @param boolean $leaveEOD + * @return string + */ + public function encode($data, $leaveEOD = false) + { + $t = \unpack('H*', $data); + return \current($t) + . ($leaveEOD ? '' : '>'); + } +} diff --git a/Fpdi/PdfParser/Filter/FilterException.php b/Fpdi/PdfParser/Filter/FilterException.php new file mode 100644 index 0000000..2a63753 --- /dev/null +++ b/Fpdi/PdfParser/Filter/FilterException.php @@ -0,0 +1,23 @@ +extensionLoaded()) { + $oData = $data; + $data = (($data !== '') ? @\gzuncompress($data) : ''); + if ($data === false) { + // let's try if the checksum is CRC32 + $fh = fopen('php://temp', 'w+b'); + fwrite($fh, "\x1f\x8b\x08\x00\x00\x00\x00\x00" . $oData); + stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 30]); + fseek($fh, 0); + $data = @stream_get_contents($fh); + fclose($fh); + + if ($data) { + return $data; + } + + // Try this fallback + $tries = 0; + + $oDataLen = strlen($oData); + while ($tries < 6 && ($data === false || (strlen($data) < ($oDataLen - $tries - 1)))) { + $data = @(gzinflate(substr($oData, $tries))); + $tries++; + } + + // let's use this fallback only if the $data is longer than the original data + if (strlen($data) > ($oDataLen - $tries - 1)) { + return $data; + } + + if (!$data) { + throw new FlateException( + 'Error while decompressing stream.', + FlateException::DECOMPRESS_ERROR + ); + } + } + } else { + throw new FlateException( + 'To handle FlateDecode filter, enable zlib support in PHP.', + FlateException::NO_ZLIB + ); + } + + return $data; + } +} diff --git a/Fpdi/PdfParser/Filter/FlateException.php b/Fpdi/PdfParser/Filter/FlateException.php new file mode 100644 index 0000000..c86b5db --- /dev/null +++ b/Fpdi/PdfParser/Filter/FlateException.php @@ -0,0 +1,27 @@ +initsTable(); + + $this->data = $data; + $this->dataLength = \strlen($data); + + // Initialize pointers + $this->bytePointer = 0; + + $this->nextData = 0; + $this->nextBits = 0; + + $oldCode = 0; + + $uncompData = ''; + + while (($code = $this->getNextCode()) !== 257) { + if ($code === 256) { + $this->initsTable(); + $code = $this->getNextCode(); + + if ($code === 257) { + break; + } + + $uncompData .= $this->sTable[$code]; + $oldCode = $code; + } else { + if ($code < $this->tIdx) { + $string = $this->sTable[$code]; + $uncompData .= $string; + + $this->addStringToTable($this->sTable[$oldCode], $string[0]); + $oldCode = $code; + } else { + $string = $this->sTable[$oldCode]; + $string .= $string[0]; + $uncompData .= $string; + + $this->addStringToTable($string); + $oldCode = $code; + } + } + } + + return $uncompData; + } + + /** + * Initialize the string table. + */ + protected function initsTable() + { + $this->sTable = []; + + for ($i = 0; $i < 256; $i++) { + $this->sTable[$i] = \chr($i); + } + + $this->tIdx = 258; + $this->bitsToGet = 9; + } + + /** + * Add a new string to the string table. + * + * @param string $oldString + * @param string $newString + */ + protected function addStringToTable($oldString, $newString = '') + { + $string = $oldString . $newString; + + // Add this new String to the table + $this->sTable[$this->tIdx++] = $string; + + if ($this->tIdx === 511) { + $this->bitsToGet = 10; + } elseif ($this->tIdx === 1023) { + $this->bitsToGet = 11; + } elseif ($this->tIdx === 2047) { + $this->bitsToGet = 12; + } + } + + /** + * Returns the next 9, 10, 11 or 12 bits. + * + * @return integer + */ + protected function getNextCode() + { + if ($this->bytePointer === $this->dataLength) { + return 257; + } + + $this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff); + $this->nextBits += 8; + + if ($this->nextBits < $this->bitsToGet) { + $this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff); + $this->nextBits += 8; + } + + $code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet - 9]; + $this->nextBits -= $this->bitsToGet; + + return $code; + } +} diff --git a/Fpdi/PdfParser/Filter/LzwException.php b/Fpdi/PdfParser/Filter/LzwException.php new file mode 100644 index 0000000..e638adf --- /dev/null +++ b/Fpdi/PdfParser/Filter/LzwException.php @@ -0,0 +1,22 @@ +streamReader = $streamReader; + $this->tokenizer = new Tokenizer($streamReader); + } + + /** + * Removes cycled references. + * + * @internal + */ + public function cleanUp() + { + $this->xref = null; + } + + /** + * Get the stream reader instance. + * + * @return StreamReader + */ + public function getStreamReader() + { + return $this->streamReader; + } + + /** + * Get the tokenizer instance. + * + * @return Tokenizer + */ + public function getTokenizer() + { + return $this->tokenizer; + } + + /** + * Resolves the file header. + * + * @throws PdfParserException + * @return int + */ + protected function resolveFileHeader() + { + if ($this->fileHeader) { + return $this->fileHeaderOffset; + } + + $this->streamReader->reset(0); + $maxIterations = 1000; + while (true) { + $buffer = $this->streamReader->getBuffer(false); + $offset = \strpos($buffer, '%PDF-'); + if ($offset === false) { + if (!$this->streamReader->increaseLength(100) || (--$maxIterations === 0)) { + throw new PdfParserException( + 'Unable to find PDF file header.', + PdfParserException::FILE_HEADER_NOT_FOUND + ); + } + continue; + } + break; + } + + $this->fileHeaderOffset = $offset; + $this->streamReader->setOffset($offset); + + $this->fileHeader = \trim($this->streamReader->readLine()); + return $this->fileHeaderOffset; + } + + /** + * Get the cross reference instance. + * + * @return CrossReference + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getCrossReference() + { + if ($this->xref === null) { + $this->xref = new CrossReference($this, $this->resolveFileHeader()); + } + + return $this->xref; + } + + /** + * Get the PDF version. + * + * @return int[] An array of major and minor version. + * @throws PdfParserException + */ + public function getPdfVersion() + { + $this->resolveFileHeader(); + + if (\preg_match('/%PDF-(\d)\.(\d)/', $this->fileHeader, $result) === 0) { + throw new PdfParserException( + 'Unable to extract PDF version from file header.', + PdfParserException::PDF_VERSION_NOT_FOUND + ); + } + list(, $major, $minor) = $result; + + $catalog = $this->getCatalog(); + if (isset($catalog->value['Version'])) { + $versionParts = \explode( + '.', + PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value) + ); + if (count($versionParts) === 2) { + list($major, $minor) = $versionParts; + } + } + + return [(int) $major, (int) $minor]; + } + + /** + * Get the catalog dictionary. + * + * @return PdfDictionary + * @throws Type\PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getCatalog() + { + $trailer = $this->getCrossReference()->getTrailer(); + + $catalog = PdfType::resolve(PdfDictionary::get($trailer, 'Root'), $this); + + return PdfDictionary::ensure($catalog); + } + + /** + * Get an indirect object by its object number. + * + * @param int $objectNumber + * @param bool $cache + * @return PdfIndirectObject + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getIndirectObject($objectNumber, $cache = false) + { + $objectNumber = (int) $objectNumber; + if (isset($this->objects[$objectNumber])) { + return $this->objects[$objectNumber]; + } + + $object = $this->getCrossReference()->getIndirectObject($objectNumber); + + if ($cache) { + $this->objects[$objectNumber] = $object; + } + + return $object; + } + + /** + * Read a PDF value. + * + * @param null|bool|string $token + * @param null|string $expectedType + * @return false|PdfArray|PdfBoolean|PdfDictionary|PdfHexString|PdfIndirectObject|PdfIndirectObjectReference|PdfName|PdfNull|PdfNumeric|PdfStream|PdfString|PdfToken + * @throws Type\PdfTypeException + */ + public function readValue($token = null, $expectedType = null) + { + if ($token === null) { + $token = $this->tokenizer->getNextToken(); + } + + if ($token === false) { + if ($expectedType !== null) { + throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE); + } + return false; + } + + switch ($token) { + case '(': + $this->ensureExpectedType($token, $expectedType); + return PdfString::parse($this->streamReader); + + case '<': + if ($this->streamReader->getByte() === '<') { + $this->ensureExpectedType('<<', $expectedType); + $this->streamReader->addOffset(1); + return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this); + } + + $this->ensureExpectedType($token, $expectedType); + return PdfHexString::parse($this->streamReader); + + case '/': + $this->ensureExpectedType($token, $expectedType); + return PdfName::parse($this->tokenizer, $this->streamReader); + + case '[': + $this->ensureExpectedType($token, $expectedType); + return PdfArray::parse($this->tokenizer, $this); + + default: + if (\is_numeric($token)) { + if (($token2 = $this->tokenizer->getNextToken()) !== false) { + if (\is_numeric($token2) && ($token3 = $this->tokenizer->getNextToken()) !== false) { + switch ($token3) { + case 'obj': + if ($expectedType !== null && $expectedType !== PdfIndirectObject::class) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + + return PdfIndirectObject::parse( + (int) $token, + (int) $token2, + $this, + $this->tokenizer, + $this->streamReader + ); + case 'R': + if ( + $expectedType !== null && + $expectedType !== PdfIndirectObjectReference::class + ) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + + return PdfIndirectObjectReference::create((int) $token, (int) $token2); + } + + $this->tokenizer->pushStack($token3); + } + + $this->tokenizer->pushStack($token2); + } + + if ($expectedType !== null && $expectedType !== PdfNumeric::class) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + return PdfNumeric::create($token + 0); + } + + if ($token === 'true' || $token === 'false') { + $this->ensureExpectedType($token, $expectedType); + return PdfBoolean::create($token === 'true'); + } + + if ($token === 'null') { + $this->ensureExpectedType($token, $expectedType); + return new PdfNull(); + } + + if ($expectedType !== null && $expectedType !== PdfToken::class) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + + $v = new PdfToken(); + $v->value = $token; + + return $v; + } + } + + /** + * Ensures that the token will evaluate to an expected object type (or not). + * + * @param string $token + * @param string|null $expectedType + * @return bool + * @throws Type\PdfTypeException + */ + private function ensureExpectedType($token, $expectedType) + { + static $mapping = [ + '(' => PdfString::class, + '<' => PdfHexString::class, + '<<' => PdfDictionary::class, + '/' => PdfName::class, + '[' => PdfArray::class, + 'true' => PdfBoolean::class, + 'false' => PdfBoolean::class, + 'null' => PdfNull::class + ]; + + if ($expectedType === null || $mapping[$token] === $expectedType) { + return true; + } + + throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE); + } +} diff --git a/Fpdi/PdfParser/PdfParserException.php b/Fpdi/PdfParser/PdfParserException.php new file mode 100644 index 0000000..e9f907a --- /dev/null +++ b/Fpdi/PdfParser/PdfParserException.php @@ -0,0 +1,49 @@ +stream = $stream; + $this->closeStream = $closeStream; + $this->reset(); + } + + /** + * The destructor. + */ + public function __destruct() + { + $this->cleanUp(); + } + + /** + * Closes the file handle. + */ + public function cleanUp() + { + if ($this->closeStream && is_resource($this->stream)) { + \fclose($this->stream); + } + } + + /** + * Returns the byte length of the buffer. + * + * @param bool $atOffset + * @return int + */ + public function getBufferLength($atOffset = false) + { + if ($atOffset === false) { + return $this->bufferLength; + } + + return $this->bufferLength - $this->offset; + } + + /** + * Get the current position in the stream. + * + * @return int + */ + public function getPosition() + { + return $this->position; + } + + /** + * Returns the current buffer. + * + * @param bool $atOffset + * @return string + */ + public function getBuffer($atOffset = true) + { + if ($atOffset === false) { + return $this->buffer; + } + + $string = \substr($this->buffer, $this->offset); + + return (string) $string; + } + + /** + * Gets a byte at a specific position in the buffer. + * + * If the position is invalid the method will return false. + * + * If the $position parameter is set to null the value of $this->offset will be used. + * + * @param int|null $position + * @return string|bool + */ + public function getByte($position = null) + { + $position = (int) ($position !== null ? $position : $this->offset); + if ( + $position >= $this->bufferLength + && (!$this->increaseLength() || $position >= $this->bufferLength) + ) { + return false; + } + + return $this->buffer[$position]; + } + + /** + * Returns a byte at a specific position, and set the offset to the next byte position. + * + * If the position is invalid the method will return false. + * + * If the $position parameter is set to null the value of $this->offset will be used. + * + * @param int|null $position + * @return string|bool + */ + public function readByte($position = null) + { + if ($position !== null) { + $position = (int) $position; + // check if needed bytes are available in the current buffer + if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) { + $this->reset($position); + $offset = $this->offset; + } else { + $offset = $position - $this->position; + } + } else { + $offset = $this->offset; + } + + if ( + $offset >= $this->bufferLength + && ((!$this->increaseLength()) || $offset >= $this->bufferLength) + ) { + return false; + } + + $this->offset = $offset + 1; + return $this->buffer[$offset]; + } + + /** + * Read bytes from the current or a specific offset position and set the internal pointer to the next byte. + * + * If the position is invalid the method will return false. + * + * If the $position parameter is set to null the value of $this->offset will be used. + * + * @param int $length + * @param int|null $position + * @return string|false + */ + public function readBytes($length, $position = null) + { + $length = (int) $length; + if ($position !== null) { + // check if needed bytes are available in the current buffer + if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) { + $this->reset($position, $length); + $offset = $this->offset; + } else { + $offset = $position - $this->position; + } + } else { + $offset = $this->offset; + } + + if ( + ($offset + $length) > $this->bufferLength + && ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength) + ) { + return false; + } + + $bytes = \substr($this->buffer, $offset, $length); + $this->offset = $offset + $length; + + return $bytes; + } + + /** + * Read a line from the current position. + * + * @param int $length + * @return string|bool + */ + public function readLine($length = 1024) + { + if ($this->ensureContent() === false) { + return false; + } + + $line = ''; + while ($this->ensureContent()) { + $char = $this->readByte(); + + if ($char === "\n") { + break; + } + + if ($char === "\r") { + if ($this->getByte() === "\n") { + $this->addOffset(1); + } + break; + } + + $line .= $char; + + if (\strlen($line) >= $length) { + break; + } + } + + return $line; + } + + /** + * Set the offset position in the current buffer. + * + * @param int $offset + */ + public function setOffset($offset) + { + if ($offset > $this->bufferLength || $offset < 0) { + throw new \OutOfRangeException( + \sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength) + ); + } + + $this->offset = (int) $offset; + } + + /** + * Returns the current offset in the current buffer. + * + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * Add an offset to the current offset. + * + * @param int $offset + */ + public function addOffset($offset) + { + $this->setOffset($this->offset + $offset); + } + + /** + * Make sure that there is at least one character beyond the current offset in the buffer. + * + * @return bool + */ + public function ensureContent() + { + while ($this->offset >= $this->bufferLength) { + if (!$this->increaseLength()) { + return false; + } + } + return true; + } + + /** + * Returns the stream. + * + * @return resource + */ + public function getStream() + { + return $this->stream; + } + + /** + * Gets the total available length. + * + * @return int + */ + public function getTotalLength() + { + if ($this->totalLength === null) { + $stat = \fstat($this->stream); + $this->totalLength = $stat['size']; + } + + return $this->totalLength; + } + + /** + * Resets the buffer to a position and re-read the buffer with the given length. + * + * If the $pos parameter is negative the start buffer position will be the $pos'th position from + * the end of the file. + * + * If the $pos parameter is negative and the absolute value is bigger then the totalLength of + * the file $pos will set to zero. + * + * @param int|null $pos Start position of the new buffer + * @param int $length Length of the new buffer. Mustn't be negative + */ + public function reset($pos = 0, $length = 200) + { + if ($pos === null) { + $pos = $this->position + $this->offset; + } elseif ($pos < 0) { + $pos = \max(0, $this->getTotalLength() + $pos); + } + + \fseek($this->stream, $pos); + + $this->position = $pos; + $this->buffer = $length > 0 ? \fread($this->stream, $length) : ''; + $this->bufferLength = \strlen($this->buffer); + $this->offset = 0; + + // If a stream wrapper is in use it is possible that + // length values > 8096 will be ignored, so use the + // increaseLength()-method to correct that behavior + if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) { + // increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer + $this->buffer = \substr($this->buffer, 0, $length); + $this->bufferLength = \strlen($this->buffer); + } + } + + /** + * Ensures bytes in the buffer with a specific length and location in the file. + * + * @param int $pos + * @param int $length + * @see reset() + */ + public function ensure($pos, $length) + { + if ( + $pos >= $this->position + && $pos < ($this->position + $this->bufferLength) + && ($this->position + $this->bufferLength) >= ($pos + $length) + ) { + $this->offset = $pos - $this->position; + } else { + $this->reset($pos, $length); + } + } + + /** + * Forcefully read more data into the buffer. + * + * @param int $minLength + * @return bool Returns false if the stream reaches the end + */ + public function increaseLength($minLength = 100) + { + $length = \max($minLength, 100); + + if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) { + return false; + } + + $newLength = $this->bufferLength + $length; + do { + $this->buffer .= \fread($this->stream, $newLength - $this->bufferLength); + $this->bufferLength = \strlen($this->buffer); + } while (($this->bufferLength !== $newLength) && !\feof($this->stream)); + + return true; + } +} diff --git a/Fpdi/PdfParser/Tokenizer.php b/Fpdi/PdfParser/Tokenizer.php new file mode 100644 index 0000000..36eb062 --- /dev/null +++ b/Fpdi/PdfParser/Tokenizer.php @@ -0,0 +1,154 @@ +streamReader = $streamReader; + } + + /** + * Get the stream reader instance. + * + * @return StreamReader + */ + public function getStreamReader() + { + return $this->streamReader; + } + + /** + * Clear the token stack. + */ + public function clearStack() + { + $this->stack = []; + } + + /** + * Push a token onto the stack. + * + * @param string $token + */ + public function pushStack($token) + { + $this->stack[] = $token; + } + + /** + * Get next token. + * + * @return bool|string + */ + public function getNextToken() + { + $token = \array_pop($this->stack); + if ($token !== null) { + return $token; + } + + if (($byte = $this->streamReader->readByte()) === false) { + return false; + } + + if (\in_array($byte, ["\x20", "\x0A", "\x0D", "\x0C", "\x09", "\x00"], true)) { + if ($this->leapWhiteSpaces() === false) { + return false; + } + $byte = $this->streamReader->readByte(); + } + + switch ($byte) { + case '/': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + case '<': + case '>': + return $byte; + case '%': + $this->streamReader->readLine(); + return $this->getNextToken(); + } + + /* This way is faster than checking single bytes. + */ + $bufferOffset = $this->streamReader->getOffset(); + do { + $lastBuffer = $this->streamReader->getBuffer(false); + $pos = \strcspn( + $lastBuffer, + "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%", + $bufferOffset + ); + } while ( + // Break the loop if a delimiter or white space char is matched + // in the current buffer or increase the buffers length + $lastBuffer !== false && + ( + $bufferOffset + $pos === \strlen($lastBuffer) && + $this->streamReader->increaseLength() + ) + ); + + $result = \substr($lastBuffer, $bufferOffset - 1, $pos + 1); + $this->streamReader->setOffset($bufferOffset + $pos); + + return $result; + } + + /** + * Leap white spaces. + * + * @return boolean + */ + public function leapWhiteSpaces() + { + do { + if (!$this->streamReader->ensureContent()) { + return false; + } + + $buffer = $this->streamReader->getBuffer(false); + $matches = \strspn($buffer, "\x20\x0A\x0C\x0D\x09\x00", $this->streamReader->getOffset()); + if ($matches > 0) { + $this->streamReader->addOffset($matches); + } + } while ($this->streamReader->getOffset() >= $this->streamReader->getBufferLength()); + + return true; + } +} diff --git a/Fpdi/PdfParser/Type/PdfArray.php b/Fpdi/PdfParser/Type/PdfArray.php new file mode 100644 index 0000000..e1ab08a --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfArray.php @@ -0,0 +1,85 @@ +getNextToken()) !== ']') { + if ($token === false || ($value = $parser->readValue($token)) === false) { + return false; + } + + $result[] = $value; + } + + $v = new self(); + $v->value = $result; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param PdfType[] $values + * @return self + */ + public static function create(array $values = []) + { + $v = new self(); + $v->value = $values; + + return $v; + } + + /** + * Ensures that the passed array is a PdfArray instance with a (optional) specific size. + * + * @param mixed $array + * @param null|int $size + * @return self + * @throws PdfTypeException + */ + public static function ensure($array, $size = null) + { + $result = PdfType::ensureType(self::class, $array, 'Array value expected.'); + + if ($size !== null && \count($array->value) !== $size) { + throw new PdfTypeException( + \sprintf('Array with %s entries expected.', $size), + PdfTypeException::INVALID_DATA_SIZE + ); + } + + return $result; + } +} diff --git a/Fpdi/PdfParser/Type/PdfBoolean.php b/Fpdi/PdfParser/Type/PdfBoolean.php new file mode 100644 index 0000000..68502d7 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfBoolean.php @@ -0,0 +1,42 @@ +value = (bool) $value; + return $v; + } + + /** + * Ensures that the passed value is a PdfBoolean instance. + * + * @param mixed $value + * @return self + * @throws PdfTypeException + */ + public static function ensure($value) + { + return PdfType::ensureType(self::class, $value, 'Boolean value expected.'); + } +} diff --git a/Fpdi/PdfParser/Type/PdfDictionary.php b/Fpdi/PdfParser/Type/PdfDictionary.php new file mode 100644 index 0000000..1b5d164 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfDictionary.php @@ -0,0 +1,134 @@ +getNextToken(); + if ($token === '>' && $streamReader->getByte() === '>') { + $streamReader->addOffset(1); + break; + } + + $key = $parser->readValue($token); + if ($key === false) { + return false; + } + + // ensure the first value to be a Name object + if (!($key instanceof PdfName)) { + $lastToken = null; + // ignore all other entries and search for the closing brackets + while (($token = $tokenizer->getNextToken()) !== '>' && $token !== false && $lastToken !== '>') { + $lastToken = $token; + } + + if ($token === false) { + return false; + } + + break; + } + + + $value = $parser->readValue(); + if ($value === false) { + return false; + } + + if ($value instanceof PdfNull) { + continue; + } + + // catch missing value + if ($value instanceof PdfToken && $value->value === '>' && $streamReader->getByte() === '>') { + $streamReader->addOffset(1); + break; + } + + $entries[$key->value] = $value; + } + + $v = new self(); + $v->value = $entries; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param PdfType[] $entries The keys are the name entries of the dictionary. + * @return self + */ + public static function create(array $entries = []) + { + $v = new self(); + $v->value = $entries; + + return $v; + } + + /** + * Get a value by its key from a dictionary or a default value. + * + * @param mixed $dictionary + * @param string $key + * @param PdfType|null $default + * @return PdfNull|PdfType + * @throws PdfTypeException + */ + public static function get($dictionary, $key, PdfType $default = null) + { + $dictionary = self::ensure($dictionary); + + if (isset($dictionary->value[$key])) { + return $dictionary->value[$key]; + } + + return $default === null + ? new PdfNull() + : $default; + } + + /** + * Ensures that the passed value is a PdfDictionary instance. + * + * @param mixed $dictionary + * @return self + * @throws PdfTypeException + */ + public static function ensure($dictionary) + { + return PdfType::ensureType(self::class, $dictionary, 'Dictionary value expected.'); + } +} diff --git a/Fpdi/PdfParser/Type/PdfHexString.php b/Fpdi/PdfParser/Type/PdfHexString.php new file mode 100644 index 0000000..99fdeb0 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfHexString.php @@ -0,0 +1,77 @@ +getOffset(); + + while (true) { + $buffer = $streamReader->getBuffer(false); + $pos = \strpos($buffer, '>', $bufferOffset); + if ($pos === false) { + if (!$streamReader->increaseLength()) { + return false; + } + continue; + } + + break; + } + + $result = \substr($buffer, $bufferOffset, $pos - $bufferOffset); + $streamReader->setOffset($pos + 1); + + $v = new self(); + $v->value = $result; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param string $string The hex encoded string. + * @return self + */ + public static function create($string) + { + $v = new self(); + $v->value = $string; + + return $v; + } + + /** + * Ensures that the passed value is a PdfHexString instance. + * + * @param mixed $hexString + * @return self + * @throws PdfTypeException + */ + public static function ensure($hexString) + { + return PdfType::ensureType(self::class, $hexString, 'Hex string value expected.'); + } +} diff --git a/Fpdi/PdfParser/Type/PdfIndirectObject.php b/Fpdi/PdfParser/Type/PdfIndirectObject.php new file mode 100644 index 0000000..87b2ea0 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfIndirectObject.php @@ -0,0 +1,103 @@ +readValue(); + if ($value === false) { + return false; + } + + $nextToken = $tokenizer->getNextToken(); + if ($nextToken === 'stream') { + $value = PdfStream::parse($value, $reader, $parser); + } elseif ($nextToken !== false) { + $tokenizer->pushStack($nextToken); + } + + $v = new self(); + $v->objectNumber = (int) $objectNumberToken; + $v->generationNumber = (int) $objectGenerationNumberToken; + $v->value = $value; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param int $objectNumber + * @param int $generationNumber + * @param PdfType $value + * @return self + */ + public static function create($objectNumber, $generationNumber, PdfType $value) + { + $v = new self(); + $v->objectNumber = (int) $objectNumber; + $v->generationNumber = (int) $generationNumber; + $v->value = $value; + + return $v; + } + + /** + * Ensures that the passed value is a PdfIndirectObject instance. + * + * @param mixed $indirectObject + * @return self + * @throws PdfTypeException + */ + public static function ensure($indirectObject) + { + return PdfType::ensureType(self::class, $indirectObject, 'Indirect object expected.'); + } + + /** + * The object number. + * + * @var int + */ + public $objectNumber; + + /** + * The generation number. + * + * @var int + */ + public $generationNumber; +} diff --git a/Fpdi/PdfParser/Type/PdfIndirectObjectReference.php b/Fpdi/PdfParser/Type/PdfIndirectObjectReference.php new file mode 100644 index 0000000..a3e24ab --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfIndirectObjectReference.php @@ -0,0 +1,52 @@ +value = (int) $objectNumber; + $v->generationNumber = (int) $generationNumber; + + return $v; + } + + /** + * Ensures that the passed value is a PdfIndirectObject instance. + * + * @param mixed $value + * @return self + * @throws PdfTypeException + */ + public static function ensure($value) + { + return PdfType::ensureType(self::class, $value, 'Indirect reference value expected.'); + } + + /** + * The generation number. + * + * @var int + */ + public $generationNumber; +} diff --git a/Fpdi/PdfParser/Type/PdfName.php b/Fpdi/PdfParser/Type/PdfName.php new file mode 100644 index 0000000..a1ba066 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfName.php @@ -0,0 +1,82 @@ +getByte(), "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%") === 0) { + $v->value = (string) $tokenizer->getNextToken(); + return $v; + } + + $v->value = ''; + return $v; + } + + /** + * Unescapes a name string. + * + * @param string $value + * @return string + */ + public static function unescape($value) + { + if (strpos($value, '#') === false) { + return $value; + } + + return preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) { + return chr(hexdec($matches[1])); + }, $value); + } + + /** + * Helper method to create an instance. + * + * @param string $string + * @return self + */ + public static function create($string) + { + $v = new self(); + $v->value = $string; + + return $v; + } + + /** + * Ensures that the passed value is a PdfName instance. + * + * @param mixed $name + * @return self + * @throws PdfTypeException + */ + public static function ensure($name) + { + return PdfType::ensureType(self::class, $name, 'Name value expected.'); + } +} diff --git a/Fpdi/PdfParser/Type/PdfNull.php b/Fpdi/PdfParser/Type/PdfNull.php new file mode 100644 index 0000000..ebcc234 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfNull.php @@ -0,0 +1,19 @@ +value = $value + 0; + + return $v; + } + + /** + * Ensures that the passed value is a PdfNumeric instance. + * + * @param mixed $value + * @return self + * @throws PdfTypeException + */ + public static function ensure($value) + { + return PdfType::ensureType(self::class, $value, 'Numeric value expected.'); + } +} diff --git a/Fpdi/PdfParser/Type/PdfStream.php b/Fpdi/PdfParser/Type/PdfStream.php new file mode 100644 index 0000000..700e50a --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfStream.php @@ -0,0 +1,326 @@ +value = $dictionary; + $v->reader = $reader; + $v->parser = $parser; + + $offset = $reader->getOffset(); + + // Find the first "newline" + while (($firstByte = $reader->getByte($offset)) !== false) { + if ($firstByte !== "\n" && $firstByte !== "\r") { + $offset++; + } else { + break; + } + } + + if ($firstByte === false) { + throw new PdfTypeException( + 'Unable to parse stream data. No newline after the stream keyword found.', + PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD + ); + } + + $sndByte = $reader->getByte($offset + 1); + if ($firstByte === "\n" || $firstByte === "\r") { + $offset++; + } + + if ($sndByte === "\n" && $firstByte !== "\n") { + $offset++; + } + + $reader->setOffset($offset); + // let's only save the byte-offset and read the stream only when needed + $v->stream = $reader->getPosition() + $reader->getOffset(); + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param PdfDictionary $dictionary + * @param string $stream + * @return self + */ + public static function create(PdfDictionary $dictionary, $stream) + { + $v = new self(); + $v->value = $dictionary; + $v->stream = (string) $stream; + + return $v; + } + + /** + * Ensures that the passed value is a PdfStream instance. + * + * @param mixed $stream + * @return self + * @throws PdfTypeException + */ + public static function ensure($stream) + { + return PdfType::ensureType(self::class, $stream, 'Stream value expected.'); + } + + /** + * The stream or its byte-offset position. + * + * @var int|string + */ + protected $stream; + + /** + * The stream reader instance. + * + * @var StreamReader|null + */ + protected $reader; + + /** + * The PDF parser instance. + * + * @var PdfParser + */ + protected $parser; + + /** + * Get the stream data. + * + * @param bool $cache Whether cache the stream data or not. + * @return bool|string + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getStream($cache = false) + { + if (\is_int($this->stream)) { + $length = PdfDictionary::get($this->value, 'Length'); + if ($this->parser !== null) { + $length = PdfType::resolve($length, $this->parser); + } + + if (!($length instanceof PdfNumeric) || $length->value === 0) { + $this->reader->reset($this->stream, 100000); + $buffer = $this->extractStream(); + } else { + $this->reader->reset($this->stream, $length->value); + $buffer = $this->reader->getBuffer(false); + if ($this->parser !== null) { + $this->reader->reset($this->stream + strlen($buffer)); + $this->parser->getTokenizer()->clearStack(); + $token = $this->parser->readValue(); + if ($token === false || !($token instanceof PdfToken) || $token->value !== 'endstream') { + $this->reader->reset($this->stream, 100000); + $buffer = $this->extractStream(); + $this->reader->reset($this->stream + strlen($buffer)); + } + } + } + + if ($cache === false) { + return $buffer; + } + + $this->stream = $buffer; + $this->reader = null; + } + + return $this->stream; + } + + /** + * Extract the stream "manually". + * + * @return string + * @throws PdfTypeException + */ + protected function extractStream() + { + while (true) { + $buffer = $this->reader->getBuffer(false); + $length = \strpos($buffer, 'endstream'); + if ($length === false) { + if (!$this->reader->increaseLength(100000)) { + throw new PdfTypeException('Cannot extract stream.'); + } + continue; + } + break; + } + + $buffer = \substr($buffer, 0, $length); + $lastByte = \substr($buffer, -1); + + /* Check for EOL marker = + * CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n}, + * and not by a CARRIAGE RETURN (\r) alone + */ + if ($lastByte === "\n") { + $buffer = \substr($buffer, 0, -1); + + $lastByte = \substr($buffer, -1); + if ($lastByte === "\r") { + $buffer = \substr($buffer, 0, -1); + } + } + + // There are streams in the wild, which have only white signs in them but need to be parsed manually due + // to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems + // in further processing (e.g. applying of filters). + if (trim($buffer) === '') { + $buffer = ''; + } + + return $buffer; + } + + /** + * Get the unfiltered stream data. + * + * @return string + * @throws FilterException + * @throws PdfParserException + */ + public function getUnfilteredStream() + { + $stream = $this->getStream(); + $filters = PdfDictionary::get($this->value, 'Filter'); + if ($filters instanceof PdfNull) { + return $stream; + } + + if ($filters instanceof PdfArray) { + $filters = $filters->value; + } else { + $filters = [$filters]; + } + + $decodeParams = PdfDictionary::get($this->value, 'DecodeParms'); + if ($decodeParams instanceof PdfArray) { + $decodeParams = $decodeParams->value; + } else { + $decodeParams = [$decodeParams]; + } + + foreach ($filters as $key => $filter) { + if (!($filter instanceof PdfName)) { + continue; + } + + $decodeParam = null; + if (isset($decodeParams[$key])) { + $decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null); + } + + switch ($filter->value) { + case 'FlateDecode': + case 'Fl': + case 'LZWDecode': + case 'LZW': + if (\strpos($filter->value, 'LZW') === 0) { + $filterObject = new Lzw(); + } else { + $filterObject = new Flate(); + } + + $stream = $filterObject->decode($stream); + + if ($decodeParam instanceof PdfDictionary) { + $predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1)); + if ($predictor->value !== 1) { + if (!\class_exists(Predictor::class)) { + throw new PdfParserException( + 'This PDF document makes use of features which are only implemented in the ' . + 'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' . + 'parser).', + PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER + ); + } + + $colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1)); + $bitsPerComponent = PdfDictionary::get( + $decodeParam, + 'BitsPerComponent', + PdfNumeric::create(8) + ); + + $columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1)); + + $filterObject = new Predictor( + $predictor->value, + $colors->value, + $bitsPerComponent->value, + $columns->value + ); + + $stream = $filterObject->decode($stream); + } + } + + break; + case 'ASCII85Decode': + case 'A85': + $filterObject = new Ascii85(); + $stream = $filterObject->decode($stream); + break; + + case 'ASCIIHexDecode': + case 'AHx': + $filterObject = new AsciiHex(); + $stream = $filterObject->decode($stream); + break; + + default: + throw new FilterException( + \sprintf('Unsupported filter "%s".', $filter->value), + FilterException::UNSUPPORTED_FILTER + ); + } + } + + return $stream; + } +} diff --git a/Fpdi/PdfParser/Type/PdfString.php b/Fpdi/PdfParser/Type/PdfString.php new file mode 100644 index 0000000..45da6d8 --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfString.php @@ -0,0 +1,172 @@ +getOffset(); + $openBrackets = 1; + do { + $buffer = $streamReader->getBuffer(false); + for ($length = \strlen($buffer); $openBrackets !== 0 && $pos < $length; $pos++) { + switch ($buffer[$pos]) { + case '(': + $openBrackets++; + break; + case ')': + $openBrackets--; + break; + case '\\': + $pos++; + } + } + } while ($openBrackets !== 0 && $streamReader->increaseLength()); + + $result = \substr($buffer, $startPos, $openBrackets + $pos - $startPos - 1); + $streamReader->setOffset($pos); + + $v = new self(); + $v->value = $result; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param string $value The string needs to be escaped accordingly. + * @return self + */ + public static function create($value) + { + $v = new self(); + $v->value = $value; + + return $v; + } + + /** + * Ensures that the passed value is a PdfString instance. + * + * @param mixed $string + * @return self + * @throws PdfTypeException + */ + public static function ensure($string) + { + return PdfType::ensureType(self::class, $string, 'String value expected.'); + } + + /** + * Unescapes escaped sequences in a PDF string according to the PDF specification. + * + * @param string $s + * @return string + */ + public static function unescape($s) + { + $out = ''; + /** @noinspection ForeachInvariantsInspection */ + for ($count = 0, $n = \strlen($s); $count < $n; $count++) { + if ($s[$count] !== '\\') { + $out .= $s[$count]; + } else { + // A backslash at the end of the string - ignore it + if ($count === ($n - 1)) { + break; + } + + switch ($s[++$count]) { + case ')': + case '(': + case '\\': + $out .= $s[$count]; + break; + + case 'f': + $out .= "\x0C"; + break; + + case 'b': + $out .= "\x08"; + break; + + case 't': + $out .= "\x09"; + break; + + case 'r': + $out .= "\x0D"; + break; + + case 'n': + $out .= "\x0A"; + break; + + case "\r": + if ($count !== $n - 1 && $s[$count + 1] === "\n") { + $count++; + } + break; + + case "\n": + break; + + default: + $actualChar = \ord($s[$count]); + // ascii 48 = number 0 + // ascii 57 = number 9 + if ($actualChar >= 48 && $actualChar <= 57) { + $oct = '' . $s[$count]; + + /** @noinspection NotOptimalIfConditionsInspection */ + if ( + $count + 1 < $n + && \ord($s[$count + 1]) >= 48 + && \ord($s[$count + 1]) <= 57 + ) { + $count++; + $oct .= $s[$count]; + + /** @noinspection NotOptimalIfConditionsInspection */ + if ( + $count + 1 < $n + && \ord($s[$count + 1]) >= 48 + && \ord($s[$count + 1]) <= 57 + ) { + $oct .= $s[++$count]; + } + } + + $out .= \chr(\octdec($oct)); + } else { + // If the character is not one of those defined, the backslash is ignored + $out .= $s[$count]; + } + } + } + } + return $out; + } +} diff --git a/Fpdi/PdfParser/Type/PdfToken.php b/Fpdi/PdfParser/Type/PdfToken.php new file mode 100644 index 0000000..4caa91a --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfToken.php @@ -0,0 +1,43 @@ +value = $token; + + return $v; + } + + /** + * Ensures that the passed value is a PdfToken instance. + * + * @param mixed $token + * @return self + * @throws PdfTypeException + */ + public static function ensure($token) + { + return PdfType::ensureType(self::class, $token, 'Token value expected.'); + } +} diff --git a/Fpdi/PdfParser/Type/PdfType.php b/Fpdi/PdfParser/Type/PdfType.php new file mode 100644 index 0000000..336e1bb --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfType.php @@ -0,0 +1,78 @@ +value, $parser, $stopAtIndirectObject); + } + + if ($value instanceof PdfIndirectObjectReference) { + return self::resolve($parser->getIndirectObject($value->value), $parser, $stopAtIndirectObject); + } + + return $value; + } + + /** + * Ensure that a value is an instance of a specific PDF type. + * + * @param string $type + * @param PdfType $value + * @param string $errorMessage + * @return mixed + * @throws PdfTypeException + */ + protected static function ensureType($type, $value, $errorMessage) + { + if (!($value instanceof $type)) { + throw new PdfTypeException( + $errorMessage, + PdfTypeException::INVALID_DATA_TYPE + ); + } + + return $value; + } + + /** + * The value of the PDF type. + * + * @var mixed + */ + public $value; +} diff --git a/Fpdi/PdfParser/Type/PdfTypeException.php b/Fpdi/PdfParser/Type/PdfTypeException.php new file mode 100644 index 0000000..b4c718b --- /dev/null +++ b/Fpdi/PdfParser/Type/PdfTypeException.php @@ -0,0 +1,24 @@ +value; + $ax = PdfNumeric::ensure(PdfType::resolve($array[0], $parser))->value; + $ay = PdfNumeric::ensure(PdfType::resolve($array[1], $parser))->value; + $bx = PdfNumeric::ensure(PdfType::resolve($array[2], $parser))->value; + $by = PdfNumeric::ensure(PdfType::resolve($array[3], $parser))->value; + + return new self($ax, $ay, $bx, $by); + } + + /** + * Rectangle constructor. + * + * @param float|int $ax + * @param float|int $ay + * @param float|int $bx + * @param float|int $by + */ + public function __construct($ax, $ay, $bx, $by) + { + $this->llx = \min($ax, $bx); + $this->lly = \min($ay, $by); + $this->urx = \max($ax, $bx); + $this->ury = \max($ay, $by); + } + + /** + * Get the width of the rectangle. + * + * @return float|int + */ + public function getWidth() + { + return $this->urx - $this->llx; + } + + /** + * Get the height of the rectangle. + * + * @return float|int + */ + public function getHeight() + { + return $this->ury - $this->lly; + } + + /** + * Get the lower left abscissa. + * + * @return float|int + */ + public function getLlx() + { + return $this->llx; + } + + /** + * Get the lower left ordinate. + * + * @return float|int + */ + public function getLly() + { + return $this->lly; + } + + /** + * Get the upper right abscissa. + * + * @return float|int + */ + public function getUrx() + { + return $this->urx; + } + + /** + * Get the upper right ordinate. + * + * @return float|int + */ + public function getUry() + { + return $this->ury; + } + + /** + * Get the rectangle as an array. + * + * @return array + */ + public function toArray() + { + return [ + $this->llx, + $this->lly, + $this->urx, + $this->ury + ]; + } + + /** + * Get the rectangle as a PdfArray. + * + * @return PdfArray + */ + public function toPdfArray() + { + $array = new PdfArray(); + $array->value[] = PdfNumeric::create($this->llx); + $array->value[] = PdfNumeric::create($this->lly); + $array->value[] = PdfNumeric::create($this->urx); + $array->value[] = PdfNumeric::create($this->ury); + + return $array; + } +} diff --git a/Fpdi/PdfReader/Page.php b/Fpdi/PdfReader/Page.php new file mode 100644 index 0000000..d45980d --- /dev/null +++ b/Fpdi/PdfReader/Page.php @@ -0,0 +1,271 @@ +pageObject = $page; + $this->parser = $parser; + } + + /** + * Get the indirect object of this page. + * + * @return PdfIndirectObject + */ + public function getPageObject() + { + return $this->pageObject; + } + + /** + * Get the dictionary of this page. + * + * @return PdfDictionary + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getPageDictionary() + { + if (null === $this->pageDictionary) { + $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser)); + } + + return $this->pageDictionary; + } + + /** + * Get a page attribute. + * + * @param string $name + * @param bool $inherited + * @return PdfType|null + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getAttribute($name, $inherited = true) + { + $dict = $this->getPageDictionary(); + + if (isset($dict->value[$name])) { + return $dict->value[$name]; + } + + $inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate']; + if ($inherited && \in_array($name, $inheritedKeys, true)) { + if ($this->inheritedAttributes === null) { + $this->inheritedAttributes = []; + $inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) { + return !isset($dict->value[$key]); + }); + + if (\count($inheritedKeys) > 0) { + $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser); + while ($parentDict instanceof PdfDictionary) { + foreach ($inheritedKeys as $index => $key) { + if (isset($parentDict->value[$key])) { + $this->inheritedAttributes[$key] = $parentDict->value[$key]; + unset($inheritedKeys[$index]); + } + } + + /** @noinspection NotOptimalIfConditionsInspection */ + if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) { + $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser); + } else { + break; + } + } + } + } + + if (isset($this->inheritedAttributes[$name])) { + return $this->inheritedAttributes[$name]; + } + } + + return null; + } + + /** + * Get the rotation value. + * + * @return int + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getRotation() + { + $rotation = $this->getAttribute('Rotate'); + if (null === $rotation) { + return 0; + } + + $rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360; + + if ($rotation < 0) { + $rotation += 360; + } + + return $rotation; + } + + /** + * Get a boundary of this page. + * + * @param string $box + * @param bool $fallback + * @return bool|Rectangle + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + * @see PageBoundaries + */ + public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true) + { + $value = $this->getAttribute($box); + + if ($value !== null) { + return Rectangle::byPdfArray($value, $this->parser); + } + + if ($fallback === false) { + return false; + } + + switch ($box) { + case PageBoundaries::BLEED_BOX: + case PageBoundaries::TRIM_BOX: + case PageBoundaries::ART_BOX: + return $this->getBoundary(PageBoundaries::CROP_BOX, true); + case PageBoundaries::CROP_BOX: + return $this->getBoundary(PageBoundaries::MEDIA_BOX, true); + } + + return false; + } + + /** + * Get the width and height of this page. + * + * @param string $box + * @param bool $fallback + * @return array|bool + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true) + { + $boundary = $this->getBoundary($box, $fallback); + if ($boundary === false) { + return false; + } + + $rotation = $this->getRotation(); + $interchange = ($rotation / 90) % 2; + + return [ + $interchange ? $boundary->getHeight() : $boundary->getWidth(), + $interchange ? $boundary->getWidth() : $boundary->getHeight() + ]; + } + + /** + * Get the raw content stream. + * + * @return string + * @throws PdfReaderException + * @throws PdfTypeException + * @throws FilterException + * @throws PdfParserException + */ + public function getContentStream() + { + $dict = $this->getPageDictionary(); + $contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser); + if ($contents instanceof PdfNull) { + return ''; + } + + if ($contents instanceof PdfArray) { + $result = []; + foreach ($contents->value as $content) { + $content = PdfType::resolve($content, $this->parser); + if (!($content instanceof PdfStream)) { + continue; + } + $result[] = $content->getUnfilteredStream(); + } + + return \implode("\n", $result); + } + + if ($contents instanceof PdfStream) { + return $contents->getUnfilteredStream(); + } + + throw new PdfReaderException( + 'Array or stream expected.', + PdfReaderException::UNEXPECTED_DATA_TYPE + ); + } +} diff --git a/Fpdi/PdfReader/PageBoundaries.php b/Fpdi/PdfReader/PageBoundaries.php new file mode 100644 index 0000000..a6290c5 --- /dev/null +++ b/Fpdi/PdfReader/PageBoundaries.php @@ -0,0 +1,94 @@ +parser = $parser; + } + + /** + * PdfReader destructor. + */ + public function __destruct() + { + if ($this->parser !== null) { + $this->parser->cleanUp(); + } + } + + /** + * Get the pdf parser instance. + * + * @return PdfParser + */ + public function getParser() + { + return $this->parser; + } + + /** + * Get the PDF version. + * + * @return string + * @throws PdfParserException + */ + public function getPdfVersion() + { + return \implode('.', $this->parser->getPdfVersion()); + } + + /** + * Get the page count. + * + * @return int + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getPageCount() + { + if ($this->pageCount === null) { + $catalog = $this->parser->getCatalog(); + + $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser); + $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser); + + $this->pageCount = PdfNumeric::ensure($count)->value; + } + + return $this->pageCount; + } + + /** + * Get a page instance. + * + * @param int $pageNumber + * @return Page + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + * @throws \InvalidArgumentException + */ + public function getPage($pageNumber) + { + if (!\is_numeric($pageNumber)) { + throw new \InvalidArgumentException( + 'Page number needs to be a number.' + ); + } + + if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) { + throw new \InvalidArgumentException( + \sprintf( + 'Page number "%s" out of available page range (1 - %s)', + $pageNumber, + $this->getPageCount() + ) + ); + } + + $this->readPages(); + + $page = $this->pages[$pageNumber - 1]; + + if ($page instanceof PdfIndirectObjectReference) { + $readPages = function ($kids) use (&$readPages) { + $kids = PdfArray::ensure($kids); + + /** @noinspection LoopWhichDoesNotLoopInspection */ + foreach ($kids->value as $reference) { + $reference = PdfIndirectObjectReference::ensure($reference); + $object = $this->parser->getIndirectObject($reference->value); + $type = PdfDictionary::get($object->value, 'Type'); + + if ($type->value === 'Pages') { + return $readPages(PdfDictionary::get($object->value, 'Kids')); + } + + return $object; + } + + throw new PdfReaderException( + 'Kids array cannot be empty.', + PdfReaderException::KIDS_EMPTY + ); + }; + + $page = $this->parser->getIndirectObject($page->value); + $dict = PdfType::resolve($page, $this->parser); + $type = PdfDictionary::get($dict, 'Type'); + + if ($type->value === 'Pages') { + $kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser); + try { + $page = $this->pages[$pageNumber - 1] = $readPages($kids); + } catch (PdfReaderException $e) { + if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) { + throw $e; + } + + // let's reset the pages array and read all page objects + $this->pages = []; + $this->readPages(true); + // @phpstan-ignore-next-line + $page = $this->pages[$pageNumber - 1]; + } + } else { + $this->pages[$pageNumber - 1] = $page; + } + } + + return new Page($page, $this->parser); + } + + /** + * Walk the page tree and resolve all indirect objects of all pages. + * + * @param bool $readAll + * @throws CrossReferenceException + * @throws PdfParserException + * @throws PdfTypeException + */ + protected function readPages($readAll = false) + { + if (\count($this->pages) > 0) { + return; + } + + $readPages = function ($kids, $count) use (&$readPages, $readAll) { + $kids = PdfArray::ensure($kids); + $isLeaf = ($count->value === \count($kids->value)); + + foreach ($kids->value as $reference) { + $reference = PdfIndirectObjectReference::ensure($reference); + + if (!$readAll && $isLeaf) { + $this->pages[] = $reference; + continue; + } + + $object = $this->parser->getIndirectObject($reference->value); + $type = PdfDictionary::get($object->value, 'Type'); + + if ($type->value === 'Pages') { + $readPages(PdfDictionary::get($object->value, 'Kids'), PdfDictionary::get($object->value, 'Count')); + } else { + $this->pages[] = $object; + } + } + }; + + $catalog = $this->parser->getCatalog(); + $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser); + $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser); + $kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser); + $readPages($kids, $count); + } +} diff --git a/Fpdi/PdfReader/PdfReaderException.php b/Fpdi/PdfReader/PdfReaderException.php new file mode 100644 index 0000000..30d9b2b --- /dev/null +++ b/Fpdi/PdfReader/PdfReaderException.php @@ -0,0 +1,34 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/courierb.php b/Fpdi/font/courierb.php new file mode 100644 index 0000000..97ecd70 --- /dev/null +++ b/Fpdi/font/courierb.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/courierbi.php b/Fpdi/font/courierbi.php new file mode 100644 index 0000000..c4bfff8 --- /dev/null +++ b/Fpdi/font/courierbi.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/courieri.php b/Fpdi/font/courieri.php new file mode 100644 index 0000000..015a15a --- /dev/null +++ b/Fpdi/font/courieri.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/helvetica.php b/Fpdi/font/helvetica.php new file mode 100644 index 0000000..927759b --- /dev/null +++ b/Fpdi/font/helvetica.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/helveticab.php b/Fpdi/font/helveticab.php new file mode 100644 index 0000000..bcd7367 --- /dev/null +++ b/Fpdi/font/helveticab.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/helveticabi.php b/Fpdi/font/helveticabi.php new file mode 100644 index 0000000..0243cde --- /dev/null +++ b/Fpdi/font/helveticabi.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/helveticai.php b/Fpdi/font/helveticai.php new file mode 100644 index 0000000..06ec735 --- /dev/null +++ b/Fpdi/font/helveticai.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/symbol.php b/Fpdi/font/symbol.php new file mode 100644 index 0000000..f8f0c33 --- /dev/null +++ b/Fpdi/font/symbol.php @@ -0,0 +1,20 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549, + ','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722, + 'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768, + 'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576, + 'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0, + chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603, + chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768, + chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042, + chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329, + chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0); +$uv = array(32=>160,33=>33,34=>8704,35=>35,36=>8707,37=>array(37,2),39=>8715,40=>array(40,2),42=>8727,43=>array(43,2),45=>8722,46=>array(46,18),64=>8773,65=>array(913,2),67=>935,68=>array(916,2),70=>934,71=>915,72=>919,73=>921,74=>977,75=>array(922,4),79=>array(927,2),81=>920,82=>929,83=>array(931,3),86=>962,87=>937,88=>926,89=>936,90=>918,91=>91,92=>8756,93=>93,94=>8869,95=>95,96=>63717,97=>array(945,2),99=>967,100=>array(948,2),102=>966,103=>947,104=>951,105=>953,106=>981,107=>array(954,4),111=>array(959,2),113=>952,114=>961,115=>array(963,3),118=>982,119=>969,120=>958,121=>968,122=>950,123=>array(123,3),126=>8764,160=>8364,161=>978,162=>8242,163=>8804,164=>8725,165=>8734,166=>402,167=>9827,168=>9830,169=>9829,170=>9824,171=>8596,172=>array(8592,4),176=>array(176,2),178=>8243,179=>8805,180=>215,181=>8733,182=>8706,183=>8226,184=>247,185=>array(8800,2),187=>8776,188=>8230,189=>array(63718,2),191=>8629,192=>8501,193=>8465,194=>8476,195=>8472,196=>8855,197=>8853,198=>8709,199=>array(8745,2),201=>8835,202=>8839,203=>8836,204=>8834,205=>8838,206=>array(8712,2),208=>8736,209=>8711,210=>63194,211=>63193,212=>63195,213=>8719,214=>8730,215=>8901,216=>172,217=>array(8743,2),219=>8660,220=>array(8656,4),224=>9674,225=>9001,226=>array(63720,3),229=>8721,230=>array(63723,10),241=>9002,242=>8747,243=>8992,244=>63733,245=>8993,246=>array(63734,9)); +?> diff --git a/Fpdi/font/times.php b/Fpdi/font/times.php new file mode 100644 index 0000000..81f2a8b --- /dev/null +++ b/Fpdi/font/times.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722, + 'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944, + 'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/timesb.php b/Fpdi/font/timesb.php new file mode 100644 index 0000000..7db704f --- /dev/null +++ b/Fpdi/font/timesb.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000, + 'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833, + 'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/timesbi.php b/Fpdi/font/timesbi.php new file mode 100644 index 0000000..089f21a --- /dev/null +++ b/Fpdi/font/timesbi.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667, + 'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889, + 'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/timesi.php b/Fpdi/font/timesi.php new file mode 100644 index 0000000..f958b5b --- /dev/null +++ b/Fpdi/font/timesi.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611, + 'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833, + 'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/Fpdi/font/zapfdingbats.php b/Fpdi/font/zapfdingbats.php new file mode 100644 index 0000000..7c2cb5e --- /dev/null +++ b/Fpdi/font/zapfdingbats.php @@ -0,0 +1,20 @@ +0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0, + chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939, + ','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692, + 'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776, + 'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873, + 'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317, + chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788, + chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788, + chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918, + chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874, + chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0); +$uv = array(32=>32,33=>array(9985,4),37=>9742,38=>array(9990,4),42=>9755,43=>9758,44=>array(9996,28),72=>9733,73=>array(10025,35),108=>9679,109=>10061,110=>9632,111=>array(10063,4),115=>9650,116=>9660,117=>9670,118=>10070,119=>9687,120=>array(10072,7),128=>array(10088,14),161=>array(10081,7),168=>9827,169=>9830,170=>9829,171=>9824,172=>array(9312,10),182=>array(10102,31),213=>8594,214=>array(8596,2),216=>array(10136,24),241=>array(10161,14)); +?> diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7faf0c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 wander + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d877cc7 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# FPDI +PDF文件生成,FPDF,FPDI,中文支持,旋转文字方向,水印,模版生成,图片合并等 +---- + +建议在 PHP7.1 上运行以获取最佳性能; + +功能描述 +---- + +* PDF文件生成,FPDF,FPDI,中文支持,旋转文字方向,水印,模版生成,图片合并等 +* 参考_test目录下例子 + + +开源协议 +---- + +* fpdi 基于`MIT`协议发布,任何人可以用在任何地方,不受约束 +* fpdi 部分代码来自互联网,若有异议,可以联系作者(13834563@qq.com)进行删除 + diff --git a/_test/111.jpeg b/_test/111.jpeg new file mode 100644 index 0000000..167b523 Binary files /dev/null and b/_test/111.jpeg differ diff --git a/_test/create.php b/_test/create.php new file mode 100644 index 0000000..5ea5dc9 --- /dev/null +++ b/_test/create.php @@ -0,0 +1,28 @@ +AddGBFont(); +$pdf1->AddPage(); +$pdf1->SetFont('GB', '', 10); +$txt = <<Write(10, iconv("UTF-8", "gbk", $txt)); +$pdf1->Output('F', 'test.pdf'); diff --git a/_test/test.pdf b/_test/test.pdf new file mode 100644 index 0000000..ab78829 Binary files /dev/null and b/_test/test.pdf differ diff --git a/_test/test.php b/_test/test.php new file mode 100644 index 0000000..a81b219 --- /dev/null +++ b/_test/test.php @@ -0,0 +1,41 @@ +AddGBFont(); +$file = __DIR__ . '/test.pdf'; +//获取页数 +$pageCount = $pdf->setSourceFile($file); +//遍历所有页面 +for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) { + //导入页面 + $templateId = $pdf->importPage($pageNo); + //获取导入页面的大小 + $size = $pdf->getTemplateSize($templateId); + //创建页面(横向或纵向取决于导入的页面大小) + if ($size['width'] > $size['height']) { + $pdf->AddPage('L', array($size['width'], $size['height'])); + } else { + $pdf->AddPage('P', array($size['width'], $size['height'])); + } + if ($pageNo == $pageCount) { + $imgFile = __DIR__ . '/111.jpeg'; + $pdf->Image($imgFile, 120, 60, 50, 50, 'jpeg'); + } + $pdf->SetFont('GB', '', 10); + if ($pageNo == 1) { + $pdf->Text(22, 26, iconv("UTF-8", "gbk", '张三')); + $pdf->Text(55, 26, iconv("UTF-8", "gbk", '330108192238333333')); + } + $pdf->SetTextColor(211, 211, 211); + $txt = iconv("UTF-8", "gbk", '张三 330192238333333 张三 330192238333333 张三 330192238333333 张三 330192238333333 张三 330192238333333 张三 330192238333333'); + for ($i=2; $i<6; $i++) { + $pdf->RotatedText(15, $i*50, $txt, 30); + } + //使用导入的页面 + $pdf->useTemplate($templateId); +} +$pdf->Output('F', '2.pdf'); +die(); diff --git a/autoload.php b/autoload.php new file mode 100644 index 0000000..61230ae --- /dev/null +++ b/autoload.php @@ -0,0 +1,20 @@ +=7.1", + "ext-zlib": "*" + }, + "autoload": { + "psr-4": { + "Fpdi\\": "Fpdi" + } + } +} diff --git a/include.php b/include.php new file mode 100644 index 0000000..cdd23be --- /dev/null +++ b/include.php @@ -0,0 +1,14 @@ +