fjffk简单的咖啡店科开发2方法2
环滁皆山也。其西南诸峰,林壑尤美,望之蔚然而深秀者,琅琊也。山行六七里,渐闻水声潺潺,而泻出于两峰之间者,酿泉也
???????????????????????????????
??????????????/?????????????????
............/...................
.........................../...
???????????????????????????????
>>>>>>>>>>>>>/>>>>>>>>>>>>>>>>>
ÿØÿà JFIF ÿÛ „ ( %!1!%)+//.383,7(-.+
-%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ ¥2" ÿÄ ÿÄ J ! 1AQ"aq2‘#BR‚¡ÁÑ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ ÿÄ * !1AQa‘"2q3±ð#b¡ÿÚ ? ¼QxJQaÍuò¸Zö Úü8,ÐÚú
"SSn<rçù–´âE—^ªBÖ9À\†¸ÔÁTÃÛ5
ëd´³Í#Ý;Þ38œî ¶H£M:wÎ3…³…âpÔF&‚FK¸9„â4àGEõªfÿ ‘ñ(ßwpŽF|È¥ù®häðÍѶ¹‘[ÒinÙW¶ùñY˜Q{›K"išÒ[Ú8žë\F¹@-?v"ÔU”,ìöžkÿ {I‡£šÍ?e
ríV
?>
.........................................
.............................................................................
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>/>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
dsdfpdfparser/README.md 0000644 00000004133 15266563345 0010031 0 ustar 00 # PDF parser
[](//packagist.org/packages/smalot/pdfparser)


[](https://scrutinizer-ci.com/g/smalot/pdfparser/?branch=master)
[](//packagist.org/packages/smalot/pdfparser)
The `smalot/pdfparser` is a standalone PHP package that provides various tools to extract data from PDF files.
This library is under **active maintenance**.
There is no active development by the author of this library (at the moment), but we welcome any pull request adding/extending functionality!
See [CONTRIBUTING.md](./CONTRIBUTING.md) for further information about how to contribute.
## Features
- Load/parse objects and headers
- Extract metadata (author, description, ...)
- Extract text from ordered pages
- Support of compressed PDFs
- Support of MAC OS Roman charset encoding
- Handling of hexa and octal encoding in text sections
- Create custom configurations (see [CustomConfig.md](/doc/CustomConfig.md)).
Currently, secured documents and extracting form data are not supported.
## License
This library is under the [LGPLv3 license](https://github.com/smalot/pdfparser/blob/master/LICENSE.txt).
## Install
This library requires PHP 7.1+ since [v1](https://github.com/smalot/pdfparser/releases/tag/v1.0.0).
You can install it via [Composer](https://getcomposer.org/):
```bash
composer require smalot/pdfparser
```
In case you can't use Composer, you can include `alt_autoload.php-dist`. It will include all required files automatically.
## Quick example
```php
parseFile('/path/to/document.pdf');
$text = $pdf->getText();
echo $text;
```
Further usage information can be found [here](/doc/Usage.md).
## Documentation
Documentation can be found in the [doc](/doc) folder.
pdfparser/phpunit-windows.xml 0000644 00000001651 15266563345 0012455 0 ustar 00
src
tests/PHPUnit
pdfparser/.php-cs-fixer.php 0000644 00000001445 15266563345 0011651 0 ustar 00 in([
__DIR__.'/src',
__DIR__.'/tests',
])
->name('*.php')
;
$config = new Config();
$config
->setFinder($finder)
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'no_empty_phpdoc' => true,
'no_unused_imports' => true,
'no_superfluous_phpdoc_tags' => true,
'ordered_imports' => true,
'phpdoc_summary' => false,
'protected_to_private' => false,
'get_class_to_class_keyword' => false, // override for PHP < 8.0 (because ::class usage is not allowed there)
'modernize_strpos' => false, // override for PHP < 8.0 (because str_contains not available in PHP 7.x)
])
;
return $config;
pdfparser/LICENSE.txt 0000644 00000016743 15266563345 0010407 0 ustar 00 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
pdfparser/src/Smalot/PdfParser/Header.php 0000644 00000012100 15266563345 0014360 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Element\ElementArray;
use Smalot\PdfParser\Element\ElementMissing;
use Smalot\PdfParser\Element\ElementStruct;
use Smalot\PdfParser\Element\ElementXRef;
/**
* Class Header
*/
class Header
{
/**
* @var Document|null
*/
protected $document;
/**
* @var Element[]
*/
protected $elements;
/**
* @param Element[] $elements list of elements
* @param Document $document document
*/
public function __construct(array $elements = [], ?Document $document = null)
{
$this->elements = $elements;
$this->document = $document;
}
public function init()
{
foreach ($this->elements as $element) {
if ($element instanceof Element) {
$element->init();
}
}
}
/**
* Returns all elements.
*/
public function getElements()
{
foreach ($this->elements as $name => $element) {
$this->resolveXRef($name);
}
return $this->elements;
}
/**
* Used only for debug.
*/
public function getElementTypes(): array
{
$types = [];
foreach ($this->elements as $key => $element) {
$types[$key] = \get_class($element);
}
return $types;
}
public function getDetails(bool $deep = true): array
{
$values = [];
$elements = $this->getElements();
foreach ($elements as $key => $element) {
if ($element instanceof self && $deep) {
$values[$key] = $element->getDetails($deep);
} elseif ($element instanceof PDFObject && $deep) {
$values[$key] = $element->getDetails(false);
} elseif ($element instanceof ElementArray) {
if ($deep) {
$values[$key] = $element->getDetails();
}
} elseif ($element instanceof Element) {
$values[$key] = (string) $element;
}
}
return $values;
}
/**
* Indicate if an element name is available in header.
*
* @param string $name the name of the element
*/
public function has(string $name): bool
{
return \array_key_exists($name, $this->elements);
}
/**
* @return Element|PDFObject
*/
public function get(string $name)
{
if (\array_key_exists($name, $this->elements) && $element = $this->resolveXRef($name)) {
return $element;
}
return new ElementMissing();
}
/**
* Resolve XRef to object.
*
* @return Element|PDFObject
*
* @throws \Exception
*/
protected function resolveXRef(string $name)
{
if (($obj = $this->elements[$name]) instanceof ElementXRef && null !== $this->document) {
/** @var ElementXRef $obj */
$object = $this->document->getObjectById($obj->getId());
if (null === $object) {
return new ElementMissing();
}
// Update elements list for future calls.
$this->elements[$name] = $object;
}
return $this->elements[$name];
}
/**
* @param string $content The content to parse
* @param Document $document The document
* @param int $position The new position of the cursor after parsing
*/
public static function parse(string $content, Document $document, int &$position = 0): self
{
/* @var Header $header */
if ('<<' == substr(trim($content), 0, 2)) {
$header = ElementStruct::parse($content, $document, $position);
} else {
$elements = ElementArray::parse($content, $document, $position);
$header = new self([], $document);
if ($elements) {
$header = new self($elements->getRawContent(), null);
}
}
if ($header) {
return $header;
}
// Build an empty header.
return new self([], $document);
}
}
pdfparser/src/Smalot/PdfParser/Pages.php 0000644 00000007201 15266563345 0014235 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Element\ElementArray;
/**
* Class Pages
*/
class Pages extends PDFObject
{
/**
* @var array<\Smalot\PdfParser\Font>|null
*/
protected $fonts;
/**
* @todo Objects other than Pages or Page might need to be treated specifically
* in order to get Page objects out of them.
*
* @see https://github.com/smalot/pdfparser/issues/331
*/
public function getPages(bool $deep = false): array
{
if (!$this->has('Kids')) {
return [];
}
/** @var ElementArray $kidsElement */
$kidsElement = $this->get('Kids');
if (!$deep) {
return $kidsElement->getContent();
}
// Prepare to apply the Pages' object's fonts to each page
if (false === \is_array($this->fonts)) {
$this->setupFonts();
}
$fontsAvailable = 0 < \count($this->fonts);
$kids = $kidsElement->getContent();
$pages = [];
foreach ($kids as $kid) {
if ($kid instanceof self) {
$pages = array_merge($pages, $kid->getPages(true));
} elseif ($kid instanceof Page) {
if ($fontsAvailable) {
$kid->setFonts($this->fonts);
}
$pages[] = $kid;
}
}
return $pages;
}
/**
* Gathers information about fonts and collects them in a list.
*
* @return void
*
* @internal
*/
protected function setupFonts()
{
$resources = $this->get('Resources');
if (method_exists($resources, 'has') && $resources->has('Font')) {
// no fonts available, therefore stop here
if ($resources->get('Font') instanceof Element\ElementMissing) {
return;
}
if ($resources->get('Font') instanceof Header) {
$fonts = $resources->get('Font')->getElements();
} else {
$fonts = $resources->get('Font')->getHeader()->getElements();
}
$table = [];
foreach ($fonts as $id => $font) {
if ($font instanceof Font) {
$table[$id] = $font;
// Store too on cleaned id value (only numeric)
$id = preg_replace('/[^0-9\.\-_]/', '', $id);
if ('' != $id) {
$table[$id] = $font;
}
}
}
$this->fonts = $table;
} else {
$this->fonts = [];
}
}
}
pdfparser/src/Smalot/PdfParser/Page.php 0000644 00000112153 15266563345 0014055 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Element\ElementArray;
use Smalot\PdfParser\Element\ElementMissing;
use Smalot\PdfParser\Element\ElementNull;
use Smalot\PdfParser\Element\ElementXRef;
class Page extends PDFObject
{
/**
* @var Font[]
*/
protected $fonts;
/**
* @var PDFObject[]
*/
protected $xobjects;
/**
* @var array
*/
protected $dataTm;
/**
* @param array<\Smalot\PdfParser\Font> $fonts
*
* @internal
*/
public function setFonts($fonts)
{
if (empty($this->fonts)) {
$this->fonts = $fonts;
}
}
/**
* @return Font[]
*/
public function getFonts()
{
if (null !== $this->fonts) {
return $this->fonts;
}
$resources = $this->get('Resources');
if (method_exists($resources, 'has') && $resources->has('Font')) {
if ($resources->get('Font') instanceof ElementMissing) {
return [];
}
if ($resources->get('Font') instanceof Header) {
$fonts = $resources->get('Font')->getElements();
} else {
$fonts = $resources->get('Font')->getHeader()->getElements();
}
$table = [];
foreach ($fonts as $id => $font) {
if ($font instanceof Font) {
$table[$id] = $font;
// Store too on cleaned id value (only numeric)
$id = preg_replace('/[^0-9\.\-_]/', '', $id);
if ('' != $id) {
$table[$id] = $font;
}
}
}
return $this->fonts = $table;
}
return [];
}
public function getFont(string $id): ?Font
{
$fonts = $this->getFonts();
if (isset($fonts[$id])) {
return $fonts[$id];
}
// According to the PDF specs (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 238)
// "The font resource name presented to the Tf operator is arbitrary, as are the names for all kinds of resources"
// Instead, we search for the unfiltered name first and then do this cleaning as a fallback, so all tests still pass.
if (isset($fonts[$id])) {
return $fonts[$id];
} else {
$id = preg_replace('/[^0-9\.\-_]/', '', $id);
if (isset($fonts[$id])) {
return $fonts[$id];
}
}
return null;
}
/**
* Support for XObject
*
* @return PDFObject[]
*/
public function getXObjects()
{
if (null !== $this->xobjects) {
return $this->xobjects;
}
$resources = $this->get('Resources');
if (method_exists($resources, 'has') && $resources->has('XObject')) {
if ($resources->get('XObject') instanceof Header) {
$xobjects = $resources->get('XObject')->getElements();
} else {
$xobjects = $resources->get('XObject')->getHeader()->getElements();
}
$table = [];
foreach ($xobjects as $id => $xobject) {
$table[$id] = $xobject;
// Store too on cleaned id value (only numeric)
$id = preg_replace('/[^0-9\.\-_]/', '', $id);
if ('' != $id) {
$table[$id] = $xobject;
}
}
return $this->xobjects = $table;
}
return [];
}
public function getXObject(string $id): ?PDFObject
{
$xobjects = $this->getXObjects();
if (isset($xobjects[$id])) {
return $xobjects[$id];
}
return null;
/*$id = preg_replace('/[^0-9\.\-_]/', '', $id);
if (isset($xobjects[$id])) {
return $xobjects[$id];
} else {
return null;
}*/
}
public function getText(?self $page = null): string
{
if ($contents = $this->get('Contents')) {
if ($contents instanceof ElementMissing) {
return '';
} elseif ($contents instanceof ElementNull) {
return '';
} elseif ($contents instanceof PDFObject) {
$elements = $contents->getHeader()->getElements();
if (is_numeric(key($elements))) {
$new_content = '';
foreach ($elements as $element) {
if ($element instanceof ElementXRef) {
$new_content .= $element->getObject()->getContent();
} else {
$new_content .= $element->getContent();
}
}
$header = new Header([], $this->document);
$contents = new PDFObject($this->document, $header, $new_content, $this->config);
}
} elseif ($contents instanceof ElementArray) {
// Create a virtual global content.
$new_content = '';
foreach ($contents->getContent() as $content) {
$new_content .= $content->getContent()."\n";
}
$header = new Header([], $this->document);
$contents = new PDFObject($this->document, $header, $new_content, $this->config);
}
/*
* Elements referencing each other on the same page can cause endless loops during text parsing.
* To combat this we keep a recursionStack containing already parsed elements on the page.
* The stack is only emptied here after getting text from a page.
*/
$contentsText = $contents->getText($this);
PDFObject::$recursionStack = [];
return $contentsText;
}
return '';
}
/**
* Return true if the current page is a (setasign\Fpdi\Fpdi) FPDI/FPDF document
*
* The metadata 'Producer' should have the value of "FPDF" . FPDF_VERSION if the
* pdf file was generated by FPDF/Fpfi.
*
* @return bool true is the current page is a FPDI/FPDF document
*/
public function isFpdf(): bool
{
if (\array_key_exists('Producer', $this->document->getDetails())
&& \is_string($this->document->getDetails()['Producer'])
&& 0 === strncmp($this->document->getDetails()['Producer'], 'FPDF', 4)) {
return true;
}
return false;
}
/**
* Return the page number of the PDF document of the page object
*
* @return int the page number
*/
public function getPageNumber(): int
{
$pages = $this->document->getPages();
$numOfPages = \count($pages);
for ($pageNum = 0; $pageNum < $numOfPages; ++$pageNum) {
if ($pages[$pageNum] === $this) {
break;
}
}
return $pageNum;
}
/**
* Return the Object of the page if the document is a FPDF/FPDI document
*
* If the document was generated by FPDF/FPDI it returns the
* PDFObject of the given page
*
* @return PDFObject The PDFObject for the page
*/
public function getPDFObjectForFpdf(): PDFObject
{
$pageNum = $this->getPageNumber();
$xObjects = $this->getXObjects();
return $xObjects[$pageNum];
}
/**
* Return a new PDFObject of the document created with FPDF/FPDI
*
* For a document generated by FPDF/FPDI, it generates a
* new PDFObject for that document
*
* @return PDFObject The PDFObject
*/
public function createPDFObjectForFpdf(): PDFObject
{
$pdfObject = $this->getPDFObjectForFpdf();
$new_content = $pdfObject->getContent();
$header = $pdfObject->getHeader();
$config = $pdfObject->config;
return new PDFObject($pdfObject->document, $header, $new_content, $config);
}
/**
* Return page if document is a FPDF/FPDI document
*
* @return Page The page
*/
public function createPageForFpdf(): self
{
$pdfObject = $this->getPDFObjectForFpdf();
$new_content = $pdfObject->getContent();
$header = $pdfObject->getHeader();
$config = $pdfObject->config;
return new self($pdfObject->document, $header, $new_content, $config);
}
public function getTextArray(?self $page = null): array
{
if ($this->isFpdf()) {
$pdfObject = $this->getPDFObjectForFpdf();
$newPdfObject = $this->createPDFObjectForFpdf();
return $newPdfObject->getTextArray($pdfObject);
} else {
if ($contents = $this->get('Contents')) {
if ($contents instanceof ElementMissing) {
return [];
} elseif ($contents instanceof ElementNull) {
return [];
} elseif ($contents instanceof PDFObject) {
$elements = $contents->getHeader()->getElements();
if (is_numeric(key($elements))) {
$new_content = '';
/** @var PDFObject $element */
foreach ($elements as $element) {
if ($element instanceof ElementXRef) {
$new_content .= $element->getObject()->getContent();
} else {
$new_content .= $element->getContent();
}
}
$header = new Header([], $this->document);
$contents = new PDFObject($this->document, $header, $new_content, $this->config);
} else {
try {
$contents->getTextArray($this);
} catch (\Throwable $e) {
return $contents->getTextArray();
}
}
} elseif ($contents instanceof ElementArray) {
// Create a virtual global content.
$new_content = '';
/** @var PDFObject $content */
foreach ($contents->getContent() as $content) {
$new_content .= $content->getContent()."\n";
}
$header = new Header([], $this->document);
$contents = new PDFObject($this->document, $header, $new_content, $this->config);
}
return $contents->getTextArray($this);
}
return [];
}
}
/**
* Gets all the text data with its internal representation of the page.
*
* Returns an array with the data and the internal representation
*/
public function extractRawData(): array
{
/*
* Now you can get the complete content of the object with the text on it
*/
$extractedData = [];
$content = $this->get('Contents');
$values = $content->getContent();
if (isset($values) && \is_array($values)) {
$text = '';
foreach ($values as $section) {
$text .= $section->getContent();
}
$sectionsText = $this->getSectionsText($text);
foreach ($sectionsText as $sectionText) {
$commandsText = $this->getCommandsText($sectionText);
foreach ($commandsText as $command) {
$extractedData[] = $command;
}
}
} else {
if ($this->isFpdf()) {
$content = $this->getPDFObjectForFpdf();
}
$sectionsText = $content->getSectionsText($content->getContent());
foreach ($sectionsText as $sectionText) {
$commandsText = $content->getCommandsText($sectionText);
foreach ($commandsText as $command) {
$extractedData[] = $command;
}
}
}
return $extractedData;
}
/**
* Gets all the decoded text data with it internal representation from a page.
*
* @param array $extractedRawData the extracted data return by extractRawData or
* null if extractRawData should be called
*
* @return array An array with the data and the internal representation
*/
public function extractDecodedRawData(?array $extractedRawData = null): array
{
if (!isset($extractedRawData) || !$extractedRawData) {
$extractedRawData = $this->extractRawData();
}
$currentFont = null; /** @var Font $currentFont */
$clippedFont = null;
$fpdfPage = null;
if ($this->isFpdf()) {
$fpdfPage = $this->createPageForFpdf();
}
foreach ($extractedRawData as &$command) {
if ('Tj' == $command['o'] || 'TJ' == $command['o']) {
$data = $command['c'];
if (!\is_array($data)) {
$tmpText = '';
if (isset($currentFont)) {
$tmpText = $currentFont->decodeOctal($data);
// $tmpText = $currentFont->decodeHexadecimal($tmpText, false);
}
$tmpText = str_replace(
['\\\\', '\(', '\)', '\n', '\r', '\t', '\ '],
['\\', '(', ')', "\n", "\r", "\t", ' '],
$tmpText
);
$tmpText = mb_convert_encoding($tmpText, 'UTF-8', 'ISO-8859-1');
if (isset($currentFont)) {
$tmpText = $currentFont->decodeContent($tmpText);
}
$command['c'] = $tmpText;
continue;
}
$numText = \count($data);
for ($i = 0; $i < $numText; ++$i) {
if (0 != ($i % 2)) {
continue;
}
$tmpText = $data[$i]['c'];
$decodedText = isset($currentFont) ? $currentFont->decodeOctal($tmpText) : $tmpText;
$decodedText = str_replace(
['\\\\', '\(', '\)', '\n', '\r', '\t', '\ '],
['\\', '(', ')', "\n", "\r", "\t", ' '],
$decodedText
);
$decodedText = mb_convert_encoding($decodedText, 'UTF-8', 'ISO-8859-1');
if (isset($currentFont)) {
$decodedText = $currentFont->decodeContent($decodedText);
}
$command['c'][$i]['c'] = $decodedText;
continue;
}
} elseif ('Tf' == $command['o'] || 'TF' == $command['o']) {
$fontId = explode(' ', $command['c'])[0];
// If document is a FPDI/FPDF the $page has the correct font
$currentFont = isset($fpdfPage) ? $fpdfPage->getFont($fontId) : $this->getFont($fontId);
continue;
} elseif ('Q' == $command['o']) {
$currentFont = $clippedFont;
} elseif ('q' == $command['o']) {
$clippedFont = $currentFont;
}
}
return $extractedRawData;
}
/**
* Gets just the Text commands that are involved in text positions and
* Text Matrix (Tm)
*
* It extract just the PDF commands that are involved with text positions, and
* the Text Matrix (Tm). These are: BT, ET, TL, Td, TD, Tm, T*, Tj, ', ", and TJ
*
* @param array $extractedDecodedRawData The data extracted by extractDecodeRawData.
* If it is null, the method extractDecodeRawData is called.
*
* @return array An array with the text command of the page
*/
public function getDataCommands(?array $extractedDecodedRawData = null): array
{
if (!isset($extractedDecodedRawData) || !$extractedDecodedRawData) {
$extractedDecodedRawData = $this->extractDecodedRawData();
}
$extractedData = [];
foreach ($extractedDecodedRawData as $command) {
switch ($command['o']) {
/*
* BT
* Begin a text object, inicializind the Tm and Tlm to identity matrix
*/
case 'BT':
$extractedData[] = $command;
break;
/*
* cm
* Concatenation Matrix that will transform all following Tm
*/
case 'cm':
$extractedData[] = $command;
break;
/*
* ET
* End a text object, discarding the text matrix
*/
case 'ET':
$extractedData[] = $command;
break;
/*
* leading TL
* Set the text leading, Tl, to leading. Tl is used by the T*, ' and " operators.
* Initial value: 0
*/
case 'TL':
$extractedData[] = $command;
break;
/*
* tx ty Td
* Move to the start of the next line, offset form the start of the
* current line by tx, ty.
*/
case 'Td':
$extractedData[] = $command;
break;
/*
* tx ty TD
* Move to the start of the next line, offset form the start of the
* current line by tx, ty. As a side effect, this operator set the leading
* parameter in the text state. This operator has the same effect as the
* code:
* -ty TL
* tx ty Td
*/
case 'TD':
$extractedData[] = $command;
break;
/*
* a b c d e f Tm
* Set the text matrix, Tm, and the text line matrix, Tlm. The operands are
* all numbers, and the initial value for Tm and Tlm is the identity matrix
* [1 0 0 1 0 0]
*/
case 'Tm':
$extractedData[] = $command;
break;
/*
* T*
* Move to the start of the next line. This operator has the same effect
* as the code:
* 0 Tl Td
* Where Tl is the current leading parameter in the text state.
*/
case 'T*':
$extractedData[] = $command;
break;
/*
* string Tj
* Show a Text String
*/
case 'Tj':
$extractedData[] = $command;
break;
/*
* string '
* Move to the next line and show a text string. This operator has the
* same effect as the code:
* T*
* string Tj
*/
case "'":
$extractedData[] = $command;
break;
/*
* aw ac string "
* Move to the next lkine and show a text string, using aw as the word
* spacing and ac as the character spacing. This operator has the same
* effect as the code:
* aw Tw
* ac Tc
* string '
* Tw set the word spacing, Tw, to wordSpace.
* Tc Set the character spacing, Tc, to charsSpace.
*/
case '"':
$extractedData[] = $command;
break;
case 'Tf':
case 'TF':
$extractedData[] = $command;
break;
/*
* array TJ
* Show one or more text strings allow individual glyph positioning.
* Each lement of array con be a string or a number. If the element is
* a string, this operator shows the string. If it is a number, the
* operator adjust the text position by that amount; that is, it translates
* the text matrix, Tm. This amount is substracted form the current
* horizontal or vertical coordinate, depending on the writing mode.
* in the default coordinate system, a positive adjustment has the effect
* of moving the next glyph painted either to the left or down by the given
* amount.
*/
case 'TJ':
$extractedData[] = $command;
break;
/*
* q
* Save current graphics state to stack
*/
case 'q':
/*
* Q
* Load last saved graphics state from stack
*/
case 'Q':
$extractedData[] = $command;
break;
default:
}
}
return $extractedData;
}
/**
* Gets the Text Matrix of the text in the page
*
* Return an array where every item is an array where the first item is the
* Text Matrix (Tm) and the second is a string with the text data. The Text matrix
* is an array of 6 numbers. The last 2 numbers are the coordinates X and Y of the
* text. The first 4 numbers has to be with Scalation, Rotation and Skew of the text.
*
* @param array $dataCommands the data extracted by getDataCommands
* if null getDataCommands is called
*
* @return array an array with the data of the page including the Tm information
* of any text in the page
*/
public function getDataTm(?array $dataCommands = null): array
{
if (!isset($dataCommands) || !$dataCommands) {
$dataCommands = $this->getDataCommands();
}
/*
* At the beginning of a text object Tm is the identity matrix
*/
$defaultTm = ['1', '0', '0', '1', '0', '0'];
$concatTm = ['1', '0', '0', '1', '0', '0'];
$graphicsStatesStack = [];
/*
* Set the text leading used by T*, ' and " operators
*/
$defaultTl = 0;
/*
* Set default values for font data
*/
$defaultFontId = -1;
$defaultFontSize = 1;
/*
* Indexes of horizontal/vertical scaling and X,Y-coordinates in the matrix (Tm)
*/
$hSc = 0; // horizontal scaling
/**
* index of vertical scaling in the array that encodes the text matrix.
* for more information: https://github.com/smalot/pdfparser/pull/559#discussion_r1053415500
*/
$vSc = 3;
$x = 4;
$y = 5;
/*
* x,y-coordinates of text space origin in user units
*
* These will be assigned the value of the currently printed string
*/
$Tx = 0;
$Ty = 0;
$Tm = $defaultTm;
$Tl = $defaultTl;
$fontId = $defaultFontId;
$fontSize = $defaultFontSize; // reflects fontSize set by Tf or Tfs
$extractedTexts = $this->getTextArray();
$extractedData = [];
foreach ($dataCommands as $command) {
// If we've used up all the texts from getTextArray(), exit
// so we aren't accessing non-existent array indices
// Fixes 'undefined array key' errors in Issues #575, #576
if (\count($extractedTexts) <= \count($extractedData)) {
break;
}
$currentText = $extractedTexts[\count($extractedData)];
switch ($command['o']) {
/*
* BT
* Begin a text object, initializing the Tm and Tlm to identity matrix
*/
case 'BT':
$Tm = $defaultTm;
$Tl = $defaultTl;
$Tx = 0;
$Ty = 0;
break;
case 'cm':
$newConcatTm = (array) explode(' ', $command['c']);
$TempMatrix = [];
// Multiply with previous concatTm
$TempMatrix[0] = (float) $concatTm[0] * (float) $newConcatTm[0] + (float) $concatTm[1] * (float) $newConcatTm[2];
$TempMatrix[1] = (float) $concatTm[0] * (float) $newConcatTm[1] + (float) $concatTm[1] * (float) $newConcatTm[3];
$TempMatrix[2] = (float) $concatTm[2] * (float) $newConcatTm[0] + (float) $concatTm[3] * (float) $newConcatTm[2];
$TempMatrix[3] = (float) $concatTm[2] * (float) $newConcatTm[1] + (float) $concatTm[3] * (float) $newConcatTm[3];
$TempMatrix[4] = (float) $concatTm[4] * (float) $newConcatTm[0] + (float) $concatTm[5] * (float) $newConcatTm[2] + (float) $newConcatTm[4];
$TempMatrix[5] = (float) $concatTm[4] * (float) $newConcatTm[1] + (float) $concatTm[5] * (float) $newConcatTm[3] + (float) $newConcatTm[5];
$concatTm = $TempMatrix;
break;
/*
* ET
* End a text object
*/
case 'ET':
break;
/*
* text leading TL
* Set the text leading, Tl, to leading. Tl is used by the T*, ' and " operators.
* Initial value: 0
*/
case 'TL':
// scaled text leading
$Tl = (float) $command['c'] * (float) $Tm[$vSc];
break;
/*
* tx ty Td
* Move to the start of the next line, offset from the start of the
* current line by tx, ty.
*/
case 'Td':
$coord = explode(' ', $command['c']);
$Tx += (float) $coord[0] * (float) $Tm[$hSc];
$Ty += (float) $coord[1] * (float) $Tm[$vSc];
$Tm[$x] = (string) $Tx;
$Tm[$y] = (string) $Ty;
break;
/*
* tx ty TD
* Move to the start of the next line, offset form the start of the
* current line by tx, ty. As a side effect, this operator set the leading
* parameter in the text state. This operator has the same effect as the
* code:
* -ty TL
* tx ty Td
*/
case 'TD':
$coord = explode(' ', $command['c']);
$Tl = -((float) $coord[1] * (float) $Tm[$vSc]);
$Tx += (float) $coord[0] * (float) $Tm[$hSc];
$Ty += (float) $coord[1] * (float) $Tm[$vSc];
$Tm[$x] = (string) $Tx;
$Tm[$y] = (string) $Ty;
break;
/*
* a b c d e f Tm
* Set the text matrix, Tm, and the text line matrix, Tlm. The operands are
* all numbers, and the initial value for Tm and Tlm is the identity matrix
* [1 0 0 1 0 0]
*/
case 'Tm':
$Tm = explode(' ', $command['c']);
$TempMatrix = [];
$TempMatrix[0] = (float) $Tm[0] * (float) $concatTm[0] + (float) $Tm[1] * (float) $concatTm[2];
$TempMatrix[1] = (float) $Tm[0] * (float) $concatTm[1] + (float) $Tm[1] * (float) $concatTm[3];
$TempMatrix[2] = (float) $Tm[2] * (float) $concatTm[0] + (float) $Tm[3] * (float) $concatTm[2];
$TempMatrix[3] = (float) $Tm[2] * (float) $concatTm[1] + (float) $Tm[3] * (float) $concatTm[3];
$TempMatrix[4] = (float) $Tm[4] * (float) $concatTm[0] + (float) $Tm[5] * (float) $concatTm[2] + (float) $concatTm[4];
$TempMatrix[5] = (float) $Tm[4] * (float) $concatTm[1] + (float) $Tm[5] * (float) $concatTm[3] + (float) $concatTm[5];
$Tm = $TempMatrix;
$Tx = (float) $Tm[$x];
$Ty = (float) $Tm[$y];
break;
/*
* T*
* Move to the start of the next line. This operator has the same effect
* as the code:
* 0 Tl Td
* Where Tl is the current leading parameter in the text state.
*/
case 'T*':
$Ty -= $Tl;
$Tm[$y] = (string) $Ty;
break;
/*
* string Tj
* Show a Text String
*/
case 'Tj':
$data = [$Tm, $currentText];
if ($this->config->getDataTmFontInfoHasToBeIncluded()) {
$data[] = $fontId;
$data[] = $fontSize;
}
$extractedData[] = $data;
break;
/*
* string '
* Move to the next line and show a text string. This operator has the
* same effect as the code:
* T*
* string Tj
*/
case "'":
$Ty -= $Tl;
$Tm[$y] = (string) $Ty;
$extractedData[] = [$Tm, $currentText];
break;
/*
* aw ac string "
* Move to the next line and show a text string, using aw as the word
* spacing and ac as the character spacing. This operator has the same
* effect as the code:
* aw Tw
* ac Tc
* string '
* Tw set the word spacing, Tw, to wordSpace.
* Tc Set the character spacing, Tc, to charsSpace.
*/
case '"':
$data = explode(' ', $currentText);
$Ty -= $Tl;
$Tm[$y] = (string) $Ty;
$extractedData[] = [$Tm, $data[2]]; // Verify
break;
case 'Tf':
/*
* From PDF 1.0 specification, page 106:
* fontname size Tf Set font and size
* Sets the text font and text size in the graphics state. There is no default value for
* either fontname or size; they must be selected using Tf before drawing any text.
* fontname is a resource name. size is a number expressed in text space units.
*
* Source: https://ia902503.us.archive.org/10/items/pdfy-0vt8s-egqFwDl7L2/PDF%20Reference%201.0.pdf
* Introduced with https://github.com/smalot/pdfparser/pull/516
*/
list($fontId, $fontSize) = explode(' ', $command['c'], 2);
break;
/*
* array TJ
* Show one or more text strings allow individual glyph positioning.
* Each lement of array con be a string or a number. If the element is
* a string, this operator shows the string. If it is a number, the
* operator adjust the text position by that amount; that is, it translates
* the text matrix, Tm. This amount is substracted form the current
* horizontal or vertical coordinate, depending on the writing mode.
* in the default coordinate system, a positive adjustment has the effect
* of moving the next glyph painted either to the left or down by the given
* amount.
*/
case 'TJ':
$data = [$Tm, $currentText];
if ($this->config->getDataTmFontInfoHasToBeIncluded()) {
$data[] = $fontId;
$data[] = $fontSize;
}
$extractedData[] = $data;
break;
/*
* q
* Save current graphics state to stack
*/
case 'q':
$graphicsStatesStack[] = $concatTm;
break;
/*
* Q
* Load last saved graphics state from stack
*/
case 'Q':
$concatTm = array_pop($graphicsStatesStack);
break;
default:
}
}
$this->dataTm = $extractedData;
return $extractedData;
}
/**
* Gets text data that are around the given coordinates (X,Y)
*
* If the text is in near the given coordinates (X,Y) (or the TM info),
* the text is returned. The extractedData return by getDataTm, could be use to see
* where is the coordinates of a given text, using the TM info for it.
*
* @param float $x The X value of the coordinate to search for. if null
* just the Y value is considered (same Row)
* @param float $y The Y value of the coordinate to search for
* just the X value is considered (same column)
* @param float $xError The value less or more to consider an X to be "near"
* @param float $yError The value less or more to consider an Y to be "near"
*
* @return array An array of text that are near the given coordinates. If no text
* "near" the x,y coordinate, an empty array is returned. If Both, x
* and y coordinates are null, null is returned.
*/
public function getTextXY(?float $x = null, ?float $y = null, float $xError = 0, float $yError = 0): array
{
if (!isset($this->dataTm) || !$this->dataTm) {
$this->getDataTm();
}
if (null !== $x) {
$x = (float) $x;
}
if (null !== $y) {
$y = (float) $y;
}
if (null === $x && null === $y) {
return [];
}
$xError = (float) $xError;
$yError = (float) $yError;
$extractedData = [];
foreach ($this->dataTm as $item) {
$tm = $item[0];
$xTm = (float) $tm[4];
$yTm = (float) $tm[5];
$text = $item[1];
if (null === $y) {
if (($xTm >= ($x - $xError))
&& ($xTm <= ($x + $xError))) {
$extractedData[] = [$tm, $text];
continue;
}
}
if (null === $x) {
if (($yTm >= ($y - $yError))
&& ($yTm <= ($y + $yError))) {
$extractedData[] = [$tm, $text];
continue;
}
}
if (($xTm >= ($x - $xError))
&& ($xTm <= ($x + $xError))
&& ($yTm >= ($y - $yError))
&& ($yTm <= ($y + $yError))) {
$extractedData[] = [$tm, $text];
continue;
}
}
return $extractedData;
}
}
pdfparser/src/Smalot/PdfParser/Exception/MissingPdfHeaderException.php 0000644 00000000305 15266563345 0022165 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Element;
/**
* Class ElementMissing
*/
class ElementMissing extends Element
{
public function __construct()
{
parent::__construct(null, null);
}
public function equals($value): bool
{
return false;
}
public function contains($value): bool
{
return false;
}
public function getContent(): bool
{
return false;
}
public function __toString(): string
{
return '';
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementDate.php 0000644 00000007640 15266563345 0016765 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHPi, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
/**
* Class ElementDate
*/
class ElementDate extends ElementString
{
/**
* @var array
*/
protected static $formats = [
4 => 'Y',
6 => 'Ym',
8 => 'Ymd',
10 => 'YmdH',
12 => 'YmdHi',
14 => 'YmdHis',
15 => 'YmdHise',
17 => 'YmdHisO',
18 => 'YmdHisO',
19 => 'YmdHisO',
];
/**
* @var string
*/
protected $format = 'c';
/**
* @var \DateTime
*/
protected $value;
public function __construct($value)
{
if (!($value instanceof \DateTime)) {
throw new \Exception('DateTime required.'); // FIXME: Sometimes strings are passed to this function
}
parent::__construct($value);
}
public function setFormat(string $format)
{
$this->format = $format;
}
public function equals($value): bool
{
if ($value instanceof \DateTime) {
$timestamp = $value->getTimeStamp();
} else {
$timestamp = strtotime($value);
}
return $timestamp == $this->value->getTimeStamp();
}
public function __toString(): string
{
return (string) $this->value->format($this->format);
}
/**
* @return bool|ElementDate
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*\(D\:(?P.*?)\)/s', $content, $match)) {
$name = $match['name'];
$name = str_replace("'", '', $name);
$date = false;
// Smallest format : Y
// Full format : YmdHisP
if (preg_match('/^\d{4}(\d{2}(\d{2}(\d{2}(\d{2}(\d{2}(Z(\d{2,4})?|[\+-]?\d{2}(\d{2})?)?)?)?)?)?)?$/', $name)) {
if ($pos = strpos($name, 'Z')) {
$name = substr($name, 0, $pos + 1);
} elseif (18 == \strlen($name) && preg_match('/[^\+-]0000$/', $name)) {
$name = substr($name, 0, -4).'+0000';
}
$format = self::$formats[\strlen($name)];
$date = \DateTime::createFromFormat($format, $name, new \DateTimeZone('UTC'));
} else {
// special cases
if (preg_match('/^\d{1,2}-\d{1,2}-\d{4},?\s+\d{2}:\d{2}:\d{2}[\+-]\d{4}$/', $name)) {
$name = str_replace(',', '', $name);
$format = 'n-j-Y H:i:sO';
$date = \DateTime::createFromFormat($format, $name, new \DateTimeZone('UTC'));
}
}
if (!$date) {
return false;
}
$offset += strpos($content, '(D:') + \strlen($match['name']) + 4; // 1 for '(D:' and ')'
return new self($date);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementNull.php 0000644 00000003477 15266563345 0017026 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
/**
* Class ElementNull
*/
class ElementNull extends Element
{
public function __construct()
{
parent::__construct(null, null);
}
public function __toString(): string
{
return 'null';
}
public function equals($value): bool
{
return $this->getContent() === $value;
}
/**
* @return bool|ElementNull
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*(null)/s', $content, $match)) {
$offset += strpos($content, 'null') + \strlen('null');
return new self();
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementName.php 0000644 00000003567 15266563345 0016774 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
use Smalot\PdfParser\Font;
/**
* Class ElementName
*/
class ElementName extends Element
{
public function __construct(string $value)
{
parent::__construct($value, null);
}
public function equals($value): bool
{
return $value == $this->value;
}
/**
* @return bool|ElementName
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*\/([A-Z0-9\-\+,#\.]+)/is', $content, $match)) {
$name = $match[1];
$offset += strpos($content, $name) + \strlen($name);
$name = Font::decodeEntities($name);
return new self($name);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php 0000644 00000003351 15266563345 0017505 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
/**
* Class ElementNumeric
*/
class ElementNumeric extends Element
{
public function __construct(string $value)
{
parent::__construct((float) $value, null);
}
/**
* @return bool|ElementNumeric
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*(?P\-?[0-9\.]+)/s', $content, $match)) {
$value = $match['value'];
$offset += strpos($content, $value) + \strlen($value);
return new self($value);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementArray.php 0000644 00000007674 15266563345 0017175 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
use Smalot\PdfParser\Header;
use Smalot\PdfParser\PDFObject;
/**
* Class ElementArray
*/
class ElementArray extends Element
{
public function __construct($value, ?Document $document = null)
{
parent::__construct($value, $document);
}
public function getContent()
{
foreach ($this->value as $name => $element) {
$this->resolveXRef($name);
}
return parent::getContent();
}
public function getRawContent(): array
{
return $this->value;
}
public function getDetails(bool $deep = true): array
{
$values = [];
$elements = $this->getContent();
foreach ($elements as $key => $element) {
if ($element instanceof Header && $deep) {
$values[$key] = $element->getDetails($deep);
} elseif ($element instanceof PDFObject && $deep) {
$values[$key] = $element->getDetails(false);
} elseif ($element instanceof self) {
if ($deep) {
$values[$key] = $element->getDetails();
}
} elseif ($element instanceof Element && !($element instanceof self)) {
$values[$key] = $element->getContent();
}
}
return $values;
}
public function __toString(): string
{
return implode(',', $this->value);
}
/**
* @return Element|PDFObject
*/
protected function resolveXRef(string $name)
{
if (($obj = $this->value[$name]) instanceof ElementXRef) {
/** @var ElementXRef $obj */
$obj = $this->document->getObjectById($obj->getId());
$this->value[$name] = $obj;
}
return $this->value[$name];
}
/**
* @todo: These methods return mixed and mismatched types throughout the hierarchy
*
* @return bool|ElementArray
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*\[(?P.*)/is', $content, $match)) {
preg_match_all('/(.*?)(\[|\])/s', trim($content), $matches);
$level = 0;
$sub = '';
foreach ($matches[0] as $part) {
$sub .= $part;
$level += (false !== strpos($part, '[') ? 1 : -1);
if ($level <= 0) {
break;
}
}
// Removes 1 level [ and ].
$sub = substr(trim($sub), 1, -1);
$sub_offset = 0;
$values = Element::parse($sub, $document, $sub_offset, true);
$offset += strpos($content, '[') + 1;
// Find next ']' position
$offset += \strlen($sub) + 1;
return new self($values, $document);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php 0000644 00000005614 15266563345 0016774 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
/**
* Class ElementHexa
*/
class ElementHexa extends ElementString
{
/**
* @return bool|ElementHexa|ElementDate
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*\<(?P[A-F0-9]+)\>/is', $content, $match)) {
$name = $match['name'];
$offset += strpos($content, '<'.$name) + \strlen($name) + 2; // 1 for '>'
// repackage string as standard
$name = '('.self::decode($name).')';
$element = ElementDate::parse($name, $document);
if (!$element) {
$element = ElementString::parse($name, $document);
}
return $element;
}
return false;
}
public static function decode(string $value): string
{
$text = '';
// Filter $value of non-hexadecimal characters
$value = (string) preg_replace('/[^0-9a-f]/i', '', $value);
// Check for leading zeros (4-byte hexadecimal indicator), or
// the BE BOM
if ('00' === substr($value, 0, 2) || 'feff' === strtolower(substr($value, 0, 4))) {
$value = (string) preg_replace('/^feff/i', '', $value);
for ($i = 0, $length = \strlen($value); $i < $length; $i += 4) {
$hex = substr($value, $i, 4);
$text .= ''.str_pad(hexdec($hex), 4, '0', \STR_PAD_LEFT).';';
}
} else {
// Otherwise decode this as 2-byte hexadecimal
for ($i = 0, $length = \strlen($value); $i < $length; $i += 2) {
$hex = substr($value, $i, 2);
$text .= \chr(hexdec($hex));
}
}
$text = html_entity_decode($text, \ENT_NOQUOTES, 'UTF-8');
return $text;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php 0000644 00000004261 15266563345 0017370 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
use Smalot\PdfParser\Header;
/**
* Class ElementStruct
*/
class ElementStruct extends Element
{
/**
* @return false|Header
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*<<(?P.*)/is', $content)) {
preg_match_all('/(.*?)(<<|>>)/s', trim($content), $matches);
$level = 0;
$sub = '';
foreach ($matches[0] as $part) {
$sub .= $part;
$level += (false !== strpos($part, '<<') ? 1 : -1);
if ($level <= 0) {
break;
}
}
$offset += strpos($content, '<<') + \strlen(rtrim($sub));
// Removes '<<' and '>>'.
$sub = trim((string) preg_replace('/^\s*<<(.*)>>\s*$/s', '\\1', $sub));
$position = 0;
$elements = Element::parse($sub, $document, $position);
return new Header($elements, $document);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php 0000644 00000005330 15266563345 0016746 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
/**
* Class ElementXRef
*/
class ElementXRef extends Element
{
public function getId(): string
{
return $this->getContent();
}
public function getObject()
{
return $this->document->getObjectById($this->getId());
}
public function equals($value): bool
{
/**
* In case $value is a number and $this->value is a string like 5_0
*
* Without this if-clause code like:
*
* $element = new ElementXRef('5_0');
* $this->assertTrue($element->equals(5));
*
* would fail (= 5_0 and 5 are not equal in PHP 8.0+).
*/
if (
true === is_numeric($value)
&& true === \is_string($this->getContent())
&& 1 === preg_match('/[0-9]+\_[0-9]+/', $this->getContent(), $matches)
) {
return (float) $this->getContent() == $value;
}
$id = ($value instanceof self) ? $value->getId() : $value;
return $this->getId() == $id;
}
public function __toString(): string
{
return '#Obj#'.$this->getId();
}
/**
* @return bool|ElementXRef
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*(?P[0-9]+\s+[0-9]+\s+R)/s', $content, $match)) {
$id = $match['id'];
$offset += strpos($content, $id) + \strlen($id);
$id = str_replace(' ', '_', rtrim($id, ' R'));
return new self($id, $document);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php 0000644 00000003777 15266563345 0017476 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
/**
* Class ElementBoolean
*/
class ElementBoolean extends Element
{
/**
* @param string|bool $value
*/
public function __construct($value)
{
parent::__construct('true' == strtolower($value) || true === $value, null);
}
public function __toString(): string
{
return $this->value ? 'true' : 'false';
}
public function equals($value): bool
{
return $this->getContent() === $value;
}
/**
* @return bool|ElementBoolean
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*(?Ptrue|false)/is', $content, $match)) {
$value = $match['value'];
$offset += strpos($content, $value) + \strlen($value);
return new self($value);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Element/ElementString.php 0000644 00000005601 15266563345 0017351 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Element;
use Smalot\PdfParser\Document;
use Smalot\PdfParser\Element;
use Smalot\PdfParser\Font;
/**
* Class ElementString
*/
class ElementString extends Element
{
public function __construct($value)
{
parent::__construct($value, null);
}
public function equals($value): bool
{
return $value == $this->value;
}
/**
* @return bool|ElementString
*/
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
{
if (preg_match('/^\s*\((?P.*)/s', $content, $match)) {
$name = $match['name'];
// Find next ')' not escaped.
$cur_start_text = $start_search_end = 0;
while (false !== ($cur_start_pos = strpos($name, ')', $start_search_end))) {
$cur_extract = substr($name, $cur_start_text, $cur_start_pos - $cur_start_text);
preg_match('/(?P[\\\]*)$/s', $cur_extract, $match);
if (!(\strlen($match['escape']) % 2)) {
break;
}
$start_search_end = $cur_start_pos + 1;
}
// Extract string.
$name = substr($name, 0, (int) $cur_start_pos);
$offset += strpos($content, '(') + $cur_start_pos + 2; // 2 for '(' and ')'
$name = str_replace(
['\\\\', '\\ ', '\\/', '\(', '\)', '\n', '\r', '\t'],
['\\', ' ', '/', '(', ')', "\n", "\r", "\t"],
$name
);
// Decode string.
$name = Font::decodeOctal($name);
$name = Font::decodeEntities($name);
$name = Font::decodeHexadecimal($name, false);
$name = Font::decodeUnicode($name);
return new self($name);
}
return false;
}
}
pdfparser/src/Smalot/PdfParser/Encoding.php 0000644 00000011154 15266563345 0014726 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Element\ElementNumeric;
use Smalot\PdfParser\Encoding\EncodingLocator;
use Smalot\PdfParser\Encoding\PostScriptGlyphs;
use Smalot\PdfParser\Exception\EncodingNotFoundException;
/**
* Class Encoding
*/
class Encoding extends PDFObject
{
/**
* @var array
*/
protected $encoding;
/**
* @var array
*/
protected $differences;
/**
* @var array
*/
protected $mapping;
public function init()
{
$this->mapping = [];
$this->differences = [];
$this->encoding = [];
if ($this->has('BaseEncoding')) {
$this->encoding = EncodingLocator::getEncoding($this->getEncodingClass())->getTranslations();
// Build table including differences.
$differences = $this->get('Differences')->getContent();
$code = 0;
if (!\is_array($differences)) {
return;
}
foreach ($differences as $difference) {
/** @var ElementNumeric $difference */
if ($difference instanceof ElementNumeric) {
$code = $difference->getContent();
continue;
}
// ElementName
$this->differences[$code] = $difference;
if (\is_object($difference)) {
$this->differences[$code] = $difference->getContent();
}
// For the next char.
++$code;
}
$this->mapping = $this->encoding;
foreach ($this->differences as $code => $difference) {
/* @var string $difference */
$this->mapping[$code] = $difference;
}
}
}
public function getDetails(bool $deep = true): array
{
$details = [];
$details['BaseEncoding'] = ($this->has('BaseEncoding') ? (string) $this->get('BaseEncoding') : 'Ansi');
$details['Differences'] = ($this->has('Differences') ? (string) $this->get('Differences') : '');
$details += parent::getDetails($deep);
return $details;
}
public function translateChar($dec): ?int
{
if (isset($this->mapping[$dec])) {
$dec = $this->mapping[$dec];
}
return PostScriptGlyphs::getCodePoint($dec);
}
/**
* Returns encoding class name if available or empty string (only prior PHP 7.4).
*
* @throws \Exception On PHP 7.4+ an exception is thrown if encoding class doesn't exist.
*/
public function __toString(): string
{
try {
return $this->getEncodingClass();
} catch (\Exception $e) {
// prior to PHP 7.4 toString has to return an empty string.
if (version_compare(\PHP_VERSION, '7.4.0', '<')) {
return '';
}
throw $e;
}
}
/**
* @throws EncodingNotFoundException
*/
protected function getEncodingClass(): string
{
// Load reference table charset.
$baseEncoding = preg_replace('/[^A-Z0-9]/is', '', $this->get('BaseEncoding')->getContent());
// Check for empty BaseEncoding field value
if (!\is_string($baseEncoding) || 0 == \strlen($baseEncoding)) {
$baseEncoding = 'StandardEncoding';
}
$className = '\\Smalot\\PdfParser\\Encoding\\'.$baseEncoding;
if (!class_exists($className)) {
throw new EncodingNotFoundException('Missing encoding data for: "'.$baseEncoding.'".');
}
return $className;
}
}
pdfparser/src/Smalot/PdfParser/Parser.php 0000644 00000025130 15266563345 0014433 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Element\ElementArray;
use Smalot\PdfParser\Element\ElementBoolean;
use Smalot\PdfParser\Element\ElementDate;
use Smalot\PdfParser\Element\ElementHexa;
use Smalot\PdfParser\Element\ElementName;
use Smalot\PdfParser\Element\ElementNull;
use Smalot\PdfParser\Element\ElementNumeric;
use Smalot\PdfParser\Element\ElementString;
use Smalot\PdfParser\Element\ElementXRef;
use Smalot\PdfParser\RawData\RawDataParser;
/**
* Class Parser
*/
class Parser
{
/**
* @var Config
*/
private $config;
/**
* @var PDFObject[]
*/
protected $objects = [];
protected $rawDataParser;
public function __construct($cfg = [], ?Config $config = null)
{
$this->config = $config ?: new Config();
$this->rawDataParser = new RawDataParser($cfg, $this->config);
}
public function getConfig(): Config
{
return $this->config;
}
/**
* @throws \Exception
*/
public function parseFile(string $filename): Document
{
$content = file_get_contents($filename);
/*
* 2018/06/20 @doganoo as multiple times a
* users have complained that the parseFile()
* method dies silently, it is an better option
* to remove the error control operator (@) and
* let the users know that the method throws an exception
* by adding @throws tag to PHPDoc.
*
* See here for an example: https://github.com/smalot/pdfparser/issues/204
*/
return $this->parseContent($content);
}
/**
* @param string $content PDF content to parse
*
* @throws \Exception if secured PDF file was detected
* @throws \Exception if no object list was found
*/
public function parseContent(string $content): Document
{
// Create structure from raw data.
list($xref, $data) = $this->rawDataParser->parseData($content);
if (isset($xref['trailer']['encrypt']) && false === $this->config->getIgnoreEncryption()) {
throw new \Exception('Secured pdf file are currently not supported.');
}
if (empty($data)) {
throw new \Exception('Object list not found. Possible secured file.');
}
// Create destination object.
$document = new Document();
$this->objects = [];
foreach ($data as $id => $structure) {
$this->parseObject($id, $structure, $document);
unset($data[$id]);
}
$document->setTrailer($this->parseTrailer($xref['trailer'], $document));
$document->setObjects($this->objects);
return $document;
}
protected function parseTrailer(array $structure, ?Document $document)
{
$trailer = [];
foreach ($structure as $name => $values) {
$name = ucfirst($name);
if (is_numeric($values)) {
$trailer[$name] = new ElementNumeric($values);
} elseif (\is_array($values)) {
$value = $this->parseTrailer($values, null);
$trailer[$name] = new ElementArray($value, null);
} elseif (false !== strpos($values, '_')) {
$trailer[$name] = new ElementXRef($values, $document);
} else {
$trailer[$name] = $this->parseHeaderElement('(', $values, $document);
}
}
return new Header($trailer, $document);
}
protected function parseObject(string $id, array $structure, ?Document $document)
{
$header = new Header([], $document);
$content = '';
foreach ($structure as $position => $part) {
if (\is_int($part)) {
$part = [null, null];
}
switch ($part[0]) {
case '[':
$elements = [];
foreach ($part[1] as $sub_element) {
$sub_type = $sub_element[0];
$sub_value = $sub_element[1];
$elements[] = $this->parseHeaderElement($sub_type, $sub_value, $document);
}
$header = new Header($elements, $document);
break;
case '<<':
$header = $this->parseHeader($part[1], $document);
break;
case 'stream':
$content = isset($part[3][0]) ? $part[3][0] : $part[1];
if ($header->get('Type')->equals('ObjStm')) {
$match = [];
// Split xrefs and contents.
preg_match('/^((\d+\s+\d+\s*)*)(.*)$/s', $content, $match);
$content = $match[3];
// Extract xrefs.
$xrefs = preg_split(
'/(\d+\s+\d+\s*)/s',
$match[1],
-1,
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
);
$table = [];
foreach ($xrefs as $xref) {
list($id, $position) = preg_split("/\s+/", trim($xref));
$table[$position] = $id;
}
ksort($table);
$ids = array_values($table);
$positions = array_keys($table);
foreach ($positions as $index => $position) {
$id = $ids[$index].'_0';
$next_position = isset($positions[$index + 1]) ? $positions[$index + 1] : \strlen($content);
$sub_content = substr($content, $position, (int) $next_position - (int) $position);
$sub_header = Header::parse($sub_content, $document);
$object = PDFObject::factory($document, $sub_header, '', $this->config);
$this->objects[$id] = $object;
}
// It is not necessary to store this content.
return;
} elseif ($header->get('Type')->equals('Metadata')) {
// Attempt to parse XMP XML Metadata
$document->extractXMPMetadata($content);
}
break;
default:
if ('null' != $part) {
$element = $this->parseHeaderElement($part[0], $part[1], $document);
if ($element) {
$header = new Header([$element], $document);
}
}
break;
}
}
if (!isset($this->objects[$id])) {
$this->objects[$id] = PDFObject::factory($document, $header, $content, $this->config);
}
}
/**
* @throws \Exception
*/
protected function parseHeader(array $structure, ?Document $document): Header
{
$elements = [];
$count = \count($structure);
for ($position = 0; $position < $count; $position += 2) {
$name = $structure[$position][1];
$type = $structure[$position + 1][0];
$value = $structure[$position + 1][1];
$elements[$name] = $this->parseHeaderElement($type, $value, $document);
}
return new Header($elements, $document);
}
/**
* @param string|array $value
*
* @return Element|Header|null
*
* @throws \Exception
*/
protected function parseHeaderElement(?string $type, $value, ?Document $document)
{
$valueIsEmpty = null == $value || '' == $value || false == $value;
if (('<<' === $type || '>>' === $type) && $valueIsEmpty) {
$value = [];
}
switch ($type) {
case '<<':
case '>>':
$header = $this->parseHeader($value, $document);
PDFObject::factory($document, $header, null, $this->config);
return $header;
case 'numeric':
return new ElementNumeric($value);
case 'boolean':
return new ElementBoolean($value);
case 'null':
return new ElementNull();
case '(':
if ($date = ElementDate::parse('('.$value.')', $document)) {
return $date;
}
return ElementString::parse('('.$value.')', $document);
case '<':
return $this->parseHeaderElement('(', ElementHexa::decode($value), $document);
case '/':
return ElementName::parse('/'.$value, $document);
case 'ojbref': // old mistake in tcpdf parser
case 'objref':
return new ElementXRef($value, $document);
case '[':
$values = [];
if (\is_array($value)) {
foreach ($value as $sub_element) {
$sub_type = $sub_element[0];
$sub_value = $sub_element[1];
$values[] = $this->parseHeaderElement($sub_type, $sub_value, $document);
}
}
return new ElementArray($values, $document);
case 'endstream':
case 'obj': // I don't know what it means but got my project fixed.
case '':
// Nothing to do with.
return null;
default:
throw new \Exception('Invalid type: "'.$type.'".');
}
}
}
pdfparser/src/Smalot/PdfParser/PDFObject.php 0000644 00000136537 15266563345 0014755 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Exception\InvalidDictionaryObjectException;
use Smalot\PdfParser\XObject\Form;
use Smalot\PdfParser\XObject\Image;
/**
* Class PDFObject
*/
class PDFObject
{
public const TYPE = 't';
public const OPERATOR = 'o';
public const COMMAND = 'c';
/**
* The recursion stack.
*
* @var array
*/
public static $recursionStack = [];
/**
* @var Document|null
*/
protected $document;
/**
* @var Header
*/
protected $header;
/**
* @var string
*/
protected $content;
/**
* @var Config|null
*/
protected $config;
/**
* @var bool
*/
protected $addPositionWhitespace = false;
public function __construct(
Document $document,
?Header $header = null,
?string $content = null,
?Config $config = null
) {
$this->document = $document;
$this->header = $header ?? new Header();
$this->content = $content;
$this->config = $config;
}
public function init()
{
}
public function getDocument(): Document
{
return $this->document;
}
public function getHeader(): ?Header
{
return $this->header;
}
public function getConfig(): ?Config
{
return $this->config;
}
/**
* @return Element|PDFObject|Header
*/
public function get(string $name)
{
return $this->header->get($name);
}
public function has(string $name): bool
{
return $this->header->has($name);
}
public function getDetails(bool $deep = true): array
{
return $this->header->getDetails($deep);
}
public function getContent(): ?string
{
return $this->content;
}
/**
* Creates a duplicate of the document stream with
* strings and other items replaced by $char. Formerly
* getSectionsText() used this output to more easily gather offset
* values to extract text from the *actual* document stream.
*
* @deprecated function is no longer used and will be removed in a future release
*
* @internal
*/
public function cleanContent(string $content, string $char = 'X')
{
$char = $char[0];
$content = str_replace(['\\\\', '\\)', '\\('], $char.$char, $content);
// Remove image bloc with binary content
preg_match_all('/\s(BI\s.*?(\sID\s).*?(\sEI))\s/s', $content, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $part) {
$content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
}
// Clean content in square brackets [.....]
preg_match_all('/\[((\(.*?\)|[0-9\.\-\s]*)*)\]/s', $content, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[1] as $part) {
$content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
}
// Clean content in round brackets (.....)
preg_match_all('/\((.*?)\)/s', $content, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[1] as $part) {
$content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
}
// Clean structure
if ($parts = preg_split('/(<|>)/s', $content, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE)) {
$content = '';
$level = 0;
foreach ($parts as $part) {
if ('<' == $part) {
++$level;
}
$content .= (0 == $level ? $part : str_repeat($char, \strlen($part)));
if ('>' == $part) {
--$level;
}
}
}
// Clean BDC and EMC markup
preg_match_all(
'/(\/[A-Za-z0-9\_]*\s*'.preg_quote($char).'*BDC)/s',
$content,
$matches,
\PREG_OFFSET_CAPTURE
);
foreach ($matches[1] as $part) {
$content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
}
preg_match_all('/\s(EMC)\s/s', $content, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[1] as $part) {
$content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
}
return $content;
}
/**
* Takes a string of PDF document stream text and formats
* it into a multi-line string with one PDF command on each line,
* separated by \r\n. If the given string is null, or binary data
* is detected instead of a document stream then return an empty
* string.
*/
private function formatContent(?string $content): string
{
if (null === $content) {
return '';
}
// Outside of (String) and inline image content in PDF document
// streams, all text should conform to UTF-8. Test for binary
// content by deleting everything after the first open-
// parenthesis ( which indicates the beginning of a string, or
// the first ID command which indicates the beginning of binary
// inline image content. Then test what remains for valid
// UTF-8. If it's not UTF-8, return an empty string as this
// $content is most likely binary. Unfortunately, using
// mb_check_encoding(..., 'UTF-8') is not strict enough, so the
// following regexp, adapted from the W3, is used. See:
// https://www.w3.org/International/questions/qa-forms-utf-8.en
// We use preg_replace() instead of preg_match() to avoid "JIT
// stack limit exhausted" errors on larger files.
$utf8Filter = preg_replace('/(
[\x09\x0A\x0D\x20-\x7E] | # ASCII
[\xC2-\xDF][\x80-\xBF] | # non-overlong 2-byte
\xE0[\xA0-\xBF][\x80-\xBF] | # excluding overlongs
[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} | # straight 3-byte
\xED[\x80-\x9F][\x80-\xBF] | # excluding surrogates
\xF0[\x90-\xBF][\x80-\xBF]{2} | # planes 1-3
[\xF1-\xF3][\x80-\xBF]{3} | # planes 4-15
\xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)/xs', '', preg_replace('/(\(|ID\s).*$/s', '', $content));
if ('' !== $utf8Filter) {
return '';
}
// Find all inline image content and replace them so they aren't
// affected by the next steps
$pdfInlineImages = [];
$offsetBI = 0;
while (preg_match('/\sBI\s(\/.+?)\sID\s(.+?)\sEI(?=\s|$)/s', $content, $text, \PREG_OFFSET_CAPTURE, $offsetBI)) {
// Attempt to detemine if this instance of the 'BI' command
// actually occured within a (string) using the following
// steps:
// Step 1: Remove any escaped slashes and parentheses from
// the alleged image characteristics data
$para = str_replace(['\\\\', '\\(', '\\)'], '', $text[1][0]);
// Step 2: Remove all correctly ordered and balanced
// parentheses from (strings)
do {
$paraTest = $para;
$para = preg_replace('/\(([^()]*)\)/', '$1', $paraTest);
} while ($para != $paraTest);
$paraOpen = strpos($para, '(');
$paraClose = strpos($para, ')');
// Check: If the remaining text contains a close parenthesis
// ')' AND it occurs before any open parenthesis, then we
// are almost certain to be inside a (string)
if (0 < $paraClose && (false === $paraOpen || $paraClose < $paraOpen)) {
// Bump the search offset forward and match again
$offsetBI = (int) $text[1][1];
continue;
}
// Step 3: Double check that this is actually inline image
// data by parsing the alleged image characteristics as a
// dictionary
$dict = $this->parseDictionary('<<'.$text[1][0].'>>');
// Check if an image Width and Height are set in the dict
if ((isset($dict['W']) || isset($dict['Width']))
&& (isset($dict['H']) || isset($dict['Height']))) {
$id = uniqid('IMAGE_', true);
$pdfInlineImages[$id] = [
preg_replace(['/\r\n/', '/\r/', '/\n/'], ' ', $text[1][0]),
preg_replace(['/\r\n/', '/\r/', '/\n/'], '', $text[2][0]),
];
$content = preg_replace(
'/'.preg_quote($text[0][0], '/').'/',
'^^^'.$id.'^^^',
$content,
1
);
} else {
// If there was no valid dictionary, or a height and width
// weren't specified, then we don't know what this is, so
// just leave it alone; bump the search offset forward and
// match again
$offsetBI = (int) $text[1][1];
}
}
// Find all strings () and replace them so they aren't affected
// by the next steps
$pdfstrings = [];
$attempt = '(';
while (preg_match('/'.preg_quote($attempt, '/').'.*?\)/s', $content, $text)) {
// Remove all escaped slashes and parentheses from the target text
$para = str_replace(['\\\\', '\\(', '\\)'], '', $text[0]);
// PDF strings can contain unescaped parentheses as long as
// they're balanced, so check for balanced parentheses
$left = preg_match_all('/\(/', $para);
$right = preg_match_all('/\)/', $para);
if (')' == $para[-1] && $left == $right) {
// Replace the string with a unique placeholder
$id = uniqid('STRING_', true);
$pdfstrings[$id] = $text[0];
$content = preg_replace(
'/'.preg_quote($text[0], '/').'/',
'@@@'.$id.'@@@',
$content,
1
);
// Reset to search for the next string
$attempt = '(';
} else {
// We had unbalanced parentheses, so use the current
// match as a base to find a longer string
$attempt = $text[0];
}
}
// Remove all carriage returns and line-feeds from the document stream
$content = str_replace(["\r", "\n"], ' ', trim($content));
// Find all dictionary << >> commands and replace them so they
// aren't affected by the next steps
$dictstore = [];
while (preg_match('/(<<.*?>> *)(BDC|BMC|DP|MP)/s', $content, $dicttext)) {
$dictid = uniqid('DICT_', true);
$dictstore[$dictid] = $dicttext[1];
$content = preg_replace(
'/'.preg_quote($dicttext[0], '/').'/',
' ###'.$dictid.'###'.$dicttext[2],
$content,
1
);
}
// Normalize white-space in the document stream
$content = preg_replace('/\s{2,}/', ' ', $content);
// Find all valid PDF operators and add \r\n after each; this
// ensures there is just one command on every line
// Source: https://ia801001.us.archive.org/1/items/pdf1.7/pdf_reference_1-7.pdf - Appendix A
// Source: https://archive.org/download/pdf320002008/PDF32000_2008.pdf - Annex A
// Note: PDF Reference 1.7 lists 'I' and 'rI' as valid commands, while
// PDF 32000:2008 lists them as 'i' and 'ri' respectively. Both versions
// appear here in the list for completeness.
$operators = [
'b*', 'b', 'BDC', 'BMC', 'B*', 'BI', 'BT', 'BX', 'B', 'cm', 'cs', 'c', 'CS',
'd0', 'd1', 'd', 'Do', 'DP', 'EMC', 'EI', 'ET', 'EX', 'f*', 'f', 'F', 'gs',
'g', 'G', 'h', 'i', 'ID', 'I', 'j', 'J', 'k', 'K', 'l', 'm', 'MP', 'M', 'n',
'q', 'Q', 're', 'rg', 'ri', 'rI', 'RG', 'scn', 'sc', 'sh', 's', 'SCN', 'SC',
'S', 'T*', 'Tc', 'Td', 'TD', 'Tf', 'TJ', 'Tj', 'TL', 'Tm', 'Tr', 'Ts', 'Tw',
'Tz', 'v', 'w', 'W*', 'W', 'y', '\'', '"',
];
foreach ($operators as $operator) {
$content = preg_replace(
'/(?> commands
$dictstore = array_reverse($dictstore, true);
foreach ($dictstore as $id => $dict) {
$content = str_replace('###'.$id.'###', $dict, $content);
}
// Restore the original string content
$pdfstrings = array_reverse($pdfstrings, true);
foreach ($pdfstrings as $id => $text) {
// Strings may contain escaped newlines, or literal newlines
// and we should clean these up before replacing the string
// back into the content stream; this ensures no strings are
// split between two lines (every command must be on one line)
$text = str_replace(
["\\\r\n", "\\\r", "\\\n", "\r", "\n"],
['', '', '', '\r', '\n'],
$text
);
$content = str_replace('@@@'.$id.'@@@', $text, $content);
}
// Restore the original content of any inline images
$pdfInlineImages = array_reverse($pdfInlineImages, true);
foreach ($pdfInlineImages as $id => $image) {
$content = str_replace(
'^^^'.$id.'^^^',
"\r\nBI\r\n".$image[0]." ID\r\n".$image[1]." EI\r\n",
$content
);
}
$content = trim(preg_replace(['/(\r\n){2,}/', '/\r\n +/'], "\r\n", $content));
return $content;
}
/**
* getSectionsText() now takes an entire, unformatted
* document stream as a string, cleans it, then filters out
* commands that aren't needed for text positioning/extraction. It
* returns an array of unprocessed PDF commands, one command per
* element.
*
* @internal
*/
public function getSectionsText(?string $content): array
{
$sections = [];
// A cleaned stream has one command on every line, so split the
// cleaned stream content on \r\n into an array
$textCleaned = preg_split(
'/(\r\n|\n|\r)/',
$this->formatContent($content),
-1,
\PREG_SPLIT_NO_EMPTY
);
$inTextBlock = false;
foreach ($textCleaned as $line) {
$line = trim($line);
// Skip empty lines
if ('' === $line) {
continue;
}
// If a 'BT' is encountered, set the $inTextBlock flag
if (preg_match('/BT$/', $line)) {
$inTextBlock = true;
$sections[] = $line;
// If an 'ET' is encountered, unset the $inTextBlock flag
} elseif ('ET' == $line) {
$inTextBlock = false;
$sections[] = $line;
} elseif ($inTextBlock) {
// If we are inside a BT ... ET text block, save all lines
$sections[] = trim($line);
} else {
// Otherwise, if we are outside of a text block, only
// save specific, necessary lines. Care should be taken
// to ensure a command being checked for *only* matches
// that command. For instance, a simple search for 'c'
// may also match the 'sc' command. See the command
// list in the formatContent() method above.
// Add more commands to save here as you find them in
// weird PDFs!
if ('q' == $line[-1] || 'Q' == $line[-1]) {
// Save and restore graphics state commands
$sections[] = $line;
} elseif (preg_match('/(?getFonts();
}
$firstFont = $this->document->getFirstFont();
if (null !== $firstFont) {
$fonts[] = $firstFont;
}
if (\count($fonts) > 0) {
return reset($fonts);
}
return new Font($this->document, null, null, $this->config);
}
/**
* Decode a '[]TJ' command and attempt to use alternate
* fonts if the current font results in output that contains
* Unicode control characters.
*
* @internal
*
* @param array> $command
*/
private function getTJUsingFontFallback(Font $font, array $command, ?Page $page = null, float $fontFactor = 4): string
{
$orig_text = $font->decodeText($command, $fontFactor);
$text = $orig_text;
// If we make this a Config option, we can add a check if it's
// enabled here.
if (null !== $page) {
$font_ids = array_keys($page->getFonts());
// If the decoded text contains UTF-8 control characters
// then the font page being used is probably the wrong one.
// Loop through the rest of the fonts to see if we can get
// a good decode. Allow x09 to x0d which are whitespace.
while (preg_match('/[\x00-\x08\x0e-\x1f\x7f]/u', $text) || false !== strpos(bin2hex($text), '00')) {
// If we're out of font IDs, then give up and use the
// original string
if (0 == \count($font_ids)) {
return $orig_text;
}
// Try the next font ID
$font = $page->getFont(array_shift($font_ids));
$text = $font->decodeText($command, $fontFactor);
}
}
return $text;
}
/**
* Expects a string that is a full PDF dictionary object,
* including the outer enclosing << >> angle brackets
*
* @internal
*
* @throws InvalidDictionaryObjectException
*/
public function parseDictionary(string $dictionary): array
{
// Normalize whitespace
$dictionary = preg_replace(['/\r/', '/\n/', '/\s{2,}/'], ' ', trim($dictionary));
if ('<<' != substr($dictionary, 0, 2)) {
throw new InvalidDictionaryObjectException('Not a valid dictionary object.');
}
$parsed = [];
$stack = [];
$currentName = '';
$arrayTypeNumeric = false;
// Remove outer layer of dictionary, and split on tokens
$split = preg_split(
'/(<<|>>|\[|\]|\/[^\s\/\[\]\(\)<>]*)/',
trim(preg_replace('/^<<|>>$/', '', $dictionary)),
-1,
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
);
foreach ($split as $token) {
$token = trim($token);
switch ($token) {
case '':
break;
// Open numeric array
case '[':
$parsed[$currentName] = [];
$arrayTypeNumeric = true;
// Move up one level in the stack
$stack[\count($stack)] = &$parsed;
$parsed = &$parsed[$currentName];
$currentName = '';
break;
// Open hashed array
case '<<':
$parsed[$currentName] = [];
$arrayTypeNumeric = false;
// Move up one level in the stack
$stack[\count($stack)] = &$parsed;
$parsed = &$parsed[$currentName];
$currentName = '';
break;
// Close numeric array
case ']':
// Revert string type arrays back to a single element
if (\is_array($parsed) && 1 == \count($parsed)
&& isset($parsed[0]) && \is_string($parsed[0])
&& '' !== $parsed[0] && '/' != $parsed[0][0]) {
$parsed = '['.$parsed[0].']';
}
// Close hashed array
// no break
case '>>':
$arrayTypeNumeric = false;
// Move down one level in the stack
$parsed = &$stack[\count($stack) - 1];
unset($stack[\count($stack) - 1]);
break;
default:
// If value begins with a slash, then this is a name
// Add it to the appropriate array
if ('/' == substr($token, 0, 1)) {
$currentName = substr($token, 1);
if (true == $arrayTypeNumeric) {
$parsed[] = $currentName;
$currentName = '';
}
} elseif ('' != $currentName) {
if (false == $arrayTypeNumeric) {
$parsed[$currentName] = $token;
}
$currentName = '';
} elseif ('' == $currentName) {
$parsed[] = $token;
}
}
}
return $parsed;
}
/**
* Returns the text content of a PDF as a string. Attempts to add
* whitespace for spacing and line-breaks where appropriate.
*
* getText() leverages getTextArray() to get the content
* of the document, setting the addPositionWhitespace flag to true
* so whitespace is inserted in a logical way for reading by
* humans.
*/
public function getText(?Page $page = null): string
{
$this->addPositionWhitespace = true;
$result = $this->getTextArray($page);
$this->addPositionWhitespace = false;
return implode('', $result).' ';
}
/**
* Returns the text content of a PDF as an array of strings. No
* extra whitespace is inserted besides what is actually encoded in
* the PDF text.
*
* @throws \Exception
*/
public function getTextArray(?Page $page = null): array
{
$result = [];
$text = [];
$marked_stack = [];
$last_written_position = false;
$sections = $this->getSectionsText($this->content);
$current_font = $this->getDefaultFont($page);
$current_font_size = 1;
$current_text_leading = 0;
$current_position = ['x' => false, 'y' => false];
$current_position_tm = [
'a' => 1, 'b' => 0, 'c' => 0,
'i' => 0, 'j' => 1, 'k' => 0,
'x' => 0, 'y' => 0, 'z' => 1,
];
$current_position_td = ['x' => 0, 'y' => 0];
$current_position_cm = [
'a' => 1, 'b' => 0, 'c' => 0,
'i' => 0, 'j' => 1, 'k' => 0,
'x' => 0, 'y' => 0, 'z' => 1,
];
$clipped_font = [];
$clipped_position_cm = [];
self::$recursionStack[] = $this->getUniqueId();
foreach ($sections as $section) {
$commands = $this->getCommandsText($section);
foreach ($commands as $command) {
switch ($command[self::OPERATOR]) {
// Begin text object
case 'BT':
// Reset text positioning matrices
$current_position_tm = [
'a' => 1, 'b' => 0, 'c' => 0,
'i' => 0, 'j' => 1, 'k' => 0,
'x' => 0, 'y' => 0, 'z' => 1,
];
$current_position_td = ['x' => 0, 'y' => 0];
$current_text_leading = 0;
break;
// Begin marked content sequence with property list
case 'BDC':
if (preg_match('/(<<.*>>)$/', $command[self::COMMAND], $match)) {
$dict = $this->parseDictionary($match[1]);
// Check for ActualText block
if (isset($dict['ActualText']) && \is_string($dict['ActualText']) && '' !== $dict['ActualText']) {
if ('[' == $dict['ActualText'][0]) {
// Simulate a 'TJ' command on the stack
$marked_stack[] = [
'ActualText' => $this->getCommandsText($dict['ActualText'].'TJ')[0],
];
} elseif ('<' == $dict['ActualText'][0] || '(' == $dict['ActualText'][0]) {
// Simulate a 'Tj' command on the stack
$marked_stack[] = [
'ActualText' => $this->getCommandsText($dict['ActualText'].'Tj')[0],
];
}
}
}
break;
// Begin marked content sequence
case 'BMC':
if ('ReversedChars' == $command[self::COMMAND]) {
// Upon encountering a ReversedChars command,
// add the characters we've built up so far to
// the result array
$result = array_merge($result, $text);
// Start a fresh $text array that will contain
// reversed characters
$text = [];
// Add the reversed text flag to the stack
$marked_stack[] = ['ReversedChars' => true];
}
break;
// set graphics position matrix
case 'cm':
$args = preg_split('/\s+/s', $command[self::COMMAND]);
$current_position_cm = [
'a' => (float) $args[0], 'b' => (float) $args[1], 'c' => 0,
'i' => (float) $args[2], 'j' => (float) $args[3], 'k' => 0,
'x' => (float) $args[4], 'y' => (float) $args[5], 'z' => 1,
];
break;
case 'Do':
if (is_null($page)) {
break;
}
$args = preg_split('/\s/s', $command[self::COMMAND]);
$id = trim(array_pop($args), '/ ');
$xobject = $page->getXObject($id);
// Check we got a PDFObject back.
if (!$xobject instanceof self) {
break;
}
// If the PDFObject is an image, do nothing, as images aren't text.
if ($xobject instanceof Image) {
break;
}
// Check this is not a circular reference.
if (!\in_array($xobject->getUniqueId(), self::$recursionStack, true)) {
$text[] = $xobject->getText($page);
}
break;
// Marked content point with (DP) & without (MP) property list
case 'DP':
case 'MP':
break;
// End text object
case 'ET':
break;
// Store current selected font and graphics matrix
case 'q':
$clipped_font[] = [$current_font, $current_font_size];
$clipped_position_cm[] = $current_position_cm;
break;
// Restore previous selected font and graphics matrix
case 'Q':
list($current_font, $current_font_size) = array_pop($clipped_font);
$current_position_cm = array_pop($clipped_position_cm);
break;
// End marked content sequence
case 'EMC':
$data = false;
if (\count($marked_stack)) {
$marked = array_pop($marked_stack);
$action = key($marked);
$data = $marked[$action];
switch ($action) {
// If we are in ReversedChars mode...
case 'ReversedChars':
// Reverse the characters we've built up so far
foreach ($text as $key => $t) {
$text[$key] = implode('', array_reverse(
mb_str_split($t, 1, mb_internal_encoding())
));
}
// Add these characters to the result array
$result = array_merge($result, $text);
// Start a fresh $text array that will contain
// non-reversed characters
$text = [];
break;
case 'ActualText':
// Use the content of the ActualText as a command
$command = $data;
break;
}
}
// If this EMC command has been transformed into a 'Tj'
// or 'TJ' command because of being ActualText, then bypass
// the break to proceed to the writing section below.
if ('Tj' != $command[self::OPERATOR] && 'TJ' != $command[self::OPERATOR]) {
break;
}
// no break
case "'":
case '"':
if ("'" == $command[self::OPERATOR] || '"' == $command[self::OPERATOR]) {
// Move to next line and write text
$current_position['x'] = 0;
$current_position_td['x'] = 0;
$current_position_td['y'] += $current_text_leading;
}
// no break
case 'Tj':
$command[self::COMMAND] = [$command];
// no break
case 'TJ':
// Check the marked content stack for flags
$actual_text = false;
$reverse_text = false;
foreach ($marked_stack as $marked) {
if (isset($marked['ActualText'])) {
$actual_text = true;
}
if (isset($marked['ReversedChars'])) {
$reverse_text = true;
}
}
// Account for text position ONLY just before we write text
if (false === $actual_text && \is_array($last_written_position)) {
// If $last_written_position is an array, that
// means we have stored text position coordinates
// for placing an ActualText
$currentX = $last_written_position[0];
$currentY = $last_written_position[1];
$last_written_position = false;
} else {
$currentX = $current_position_cm['x'] + $current_position_tm['x'] + $current_position_td['x'];
$currentY = $current_position_cm['y'] + $current_position_tm['y'] + $current_position_td['y'];
}
$whiteSpace = '';
$factorX = -$current_font_size * $current_position_tm['a'] - $current_font_size * $current_position_tm['i'];
$factorY = $current_font_size * $current_position_tm['b'] + $current_font_size * $current_position_tm['j'];
if (true === $this->addPositionWhitespace && false !== $current_position['x']) {
$curY = $currentY - $current_position['y'];
if (abs($curY) >= abs($factorY) / 4) {
$whiteSpace = "\n";
} else {
if (true === $reverse_text) {
$curX = $current_position['x'] - $currentX;
} else {
$curX = $currentX - $current_position['x'];
}
// In abs($factorX * 7) below, the 7 is chosen arbitrarily
// as the number of apparent "spaces" in a document we
// would need before considering them a "tab". In the
// future, we might offer this value to users as a config
// option.
if ($curX >= abs($factorX * 7)) {
$whiteSpace = "\t";
} elseif ($curX >= abs($factorX * 2)) {
$whiteSpace = ' ';
}
}
}
$newtext = $this->getTJUsingFontFallback(
$current_font,
$command[self::COMMAND],
$page,
$factorX
);
// If there is no ActualText pending then write
if (false === $actual_text) {
$newtext = str_replace(["\r", "\n"], '', $newtext);
if (false !== $reverse_text) {
// If we are in ReversedChars mode, add the whitespace last
$text[] = preg_replace('/ $/', ' ', $newtext.$whiteSpace);
} else {
// Otherwise add the whitespace first
if (' ' === $whiteSpace && isset($text[\count($text) - 1])) {
$text[\count($text) - 1] = preg_replace('/ $/', '', $text[\count($text) - 1]);
}
$text[] = preg_replace('/^[ \t]{2}/', ' ', $whiteSpace.$newtext);
}
// Record the position of this inserted text for comparison
// with the next text block.
// Provide a 'fudge' factor guess on how wide this text block
// is based on the number of characters. This helps limit the
// number of tabs inserted, but isn't perfect.
$factor = $factorX / 2;
$current_position = [
'x' => $currentX - mb_strlen($newtext) * $factor,
'y' => $currentY,
];
} elseif (false === $last_written_position) {
// If there is an ActualText in the pipeline
// store the position this undisplayed text
// *would* have been written to, so the
// ActualText is displayed in the right spot
$last_written_position = [$currentX, $currentY];
$current_position['x'] = $currentX;
}
break;
// move to start of next line
case 'T*':
$current_position['x'] = 0;
$current_position_td['x'] = 0;
$current_position_td['y'] += $current_text_leading;
break;
// set character spacing
case 'Tc':
break;
// move text current point and set leading
case 'Td':
case 'TD':
// move text current point
$args = preg_split('/\s+/s', $command[self::COMMAND]);
$y = (float) array_pop($args);
$x = (float) array_pop($args);
if ('TD' == $command[self::OPERATOR]) {
$current_text_leading = -$y * $current_position_tm['b'] - $y * $current_position_tm['j'];
}
$current_position_td = [
'x' => $current_position_td['x'] + $x * $current_position_tm['a'] + $x * $current_position_tm['i'],
'y' => $current_position_td['y'] + $y * $current_position_tm['b'] + $y * $current_position_tm['j'],
];
break;
case 'Tf':
$args = preg_split('/\s/s', $command[self::COMMAND]);
$size = (float) array_pop($args);
$id = trim(array_pop($args), '/');
if (null !== $page) {
$new_font = $page->getFont($id);
// If an invalid font ID is given, do not update the font.
// This should theoretically never happen, as the PDF spec states for the Tf operator:
// "The specified font value shall match a resource name in the Font entry of the default resource dictionary"
// (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 435)
// But we want to make sure that malformed PDFs do not simply crash.
if (null !== $new_font) {
$current_font = $new_font;
$current_font_size = $size;
}
}
break;
// set leading
case 'TL':
$y = (float) $command[self::COMMAND];
$current_text_leading = -$y * $current_position_tm['b'] + -$y * $current_position_tm['j'];
break;
// set text position matrix
case 'Tm':
$args = preg_split('/\s+/s', $command[self::COMMAND]);
$current_position_tm = [
'a' => (float) $args[0], 'b' => (float) $args[1], 'c' => 0,
'i' => (float) $args[2], 'j' => (float) $args[3], 'k' => 0,
'x' => (float) $args[4], 'y' => (float) $args[5], 'z' => 1,
];
break;
// set text rendering mode
case 'Ts':
break;
// set super/subscripting text rise
case 'Ts':
break;
// set word spacing
case 'Tw':
break;
// set horizontal scaling
case 'Tz':
break;
default:
}
}
}
$result = array_merge($result, $text);
return $result;
}
/**
* getCommandsText() expects the content of $text_part to be an
* already formatted, single-line command from a document stream.
* The companion function getSectionsText() returns a document
* stream as an array of single commands for just this purpose.
* Because of this, the argument $offset is no longer used, and
* may be removed in a future PdfParser release.
*
* A better name for this function would be getCommandText()
* since it now always works on just one command.
*/
public function getCommandsText(string $text_part, int &$offset = 0): array
{
$commands = $matches = [];
preg_match('/^(([\/\[\(<])?.*)(? '(',
self::OPERATOR => 'TJ',
self::COMMAND => $tjmatch[1],
];
if (isset($tjmatch[2]) && trim($tjmatch[2])) {
$subcommand[] = [
self::TYPE => 'n',
self::OPERATOR => '',
self::COMMAND => $tjmatch[2],
];
}
$command = substr($command, \strlen($tjmatch[0]));
}
// Search for hexadecimal <> format
if (preg_match('/^ *<([0-9a-f\s]*)> *(-?[\d.]+)?/i', $command, $tjmatch)) {
$tjmatch[1] = preg_replace('/\s/', '', $tjmatch[1]);
$subcommand[] = [
self::TYPE => '<',
self::OPERATOR => 'TJ',
self::COMMAND => $tjmatch[1],
];
if (isset($tjmatch[2]) && trim($tjmatch[2])) {
$subcommand[] = [
self::TYPE => 'n',
self::OPERATOR => '',
self::COMMAND => $tjmatch[2],
];
}
$command = substr($command, \strlen($tjmatch[0]));
}
} while ($command != $oldCommand);
$command = $subcommand;
} elseif ('Tj' == $operator || "'" == $operator || '"' == $operator) {
// Depending on the string type, trim the data of the
// appropriate delimiters
if ('(' == $type) {
// Don't use trim() here since a () string may end with
// a balanced or escaped right parentheses, and trim()
// will delete both. Both strings below are valid:
// eg. (String())
// eg. (String\))
$command = preg_replace('/^\(|\)$/', '', $command);
} elseif ('<' == $type) {
$command = trim($command, '<>');
}
} elseif ('/' == $type) {
$command = substr($command, 1);
}
$commands[] = [
self::TYPE => $type,
self::OPERATOR => $operator,
self::COMMAND => $command,
];
return $commands;
}
public static function factory(
Document $document,
Header $header,
?string $content,
?Config $config = null
): self {
switch ($header->get('Type')->getContent()) {
case 'XObject':
switch ($header->get('Subtype')->getContent()) {
case 'Image':
return new Image($document, $header, $config->getRetainImageContent() ? $content : null, $config);
case 'Form':
return new Form($document, $header, $content, $config);
}
return new self($document, $header, $content, $config);
case 'Pages':
return new Pages($document, $header, $content, $config);
case 'Page':
return new Page($document, $header, $content, $config);
case 'Encoding':
return new Encoding($document, $header, $content, $config);
case 'Font':
$subtype = $header->get('Subtype')->getContent();
$classname = '\Smalot\PdfParser\Font\Font'.$subtype;
if (class_exists($classname)) {
return new $classname($document, $header, $content, $config);
}
return new Font($document, $header, $content, $config);
default:
return new self($document, $header, $content, $config);
}
}
/**
* Returns unique id identifying the object.
*/
protected function getUniqueId(): string
{
return spl_object_hash($this);
}
}
pdfparser/src/Smalot/PdfParser/Config.php 0000644 00000010435 15266563345 0014406 0 ustar 00
*
* @date 2020-11-22
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
/**
* This class contains configurations used in various classes. You can override them
* manually, in case default values aren't working.
*
* @see https://github.com/smalot/pdfparser/issues/305
*/
class Config
{
private $fontSpaceLimit = -50;
/**
* @var string
*/
private $horizontalOffset = ' ';
/**
* Represents: (NUL, HT, LF, FF, CR, SP)
*
* @var string
*/
private $pdfWhitespaces = "\0\t\n\f\r ";
/**
* Represents: (NUL, HT, LF, FF, CR, SP)
*
* @var string
*/
private $pdfWhitespacesRegex = '[\0\t\n\f\r ]';
/**
* Whether to retain raw image data as content or discard it to save memory
*
* @var bool
*/
private $retainImageContent = true;
/**
* Memory limit to use when de-compressing files, in bytes.
*
* @var int
*/
private $decodeMemoryLimit = 0;
/**
* Whether to include font id and size in dataTm array
*
* @var bool
*/
private $dataTmFontInfoHasToBeIncluded = false;
/**
* Whether to attempt to read PDFs even if they are marked as encrypted.
*
* @var bool
*/
private $ignoreEncryption = false;
public function getFontSpaceLimit()
{
return $this->fontSpaceLimit;
}
public function setFontSpaceLimit($value)
{
$this->fontSpaceLimit = $value;
}
public function getHorizontalOffset(): string
{
return $this->horizontalOffset;
}
public function setHorizontalOffset($value): void
{
$this->horizontalOffset = $value;
}
public function getPdfWhitespaces(): string
{
return $this->pdfWhitespaces;
}
public function setPdfWhitespaces(string $pdfWhitespaces): void
{
$this->pdfWhitespaces = $pdfWhitespaces;
}
public function getPdfWhitespacesRegex(): string
{
return $this->pdfWhitespacesRegex;
}
public function setPdfWhitespacesRegex(string $pdfWhitespacesRegex): void
{
$this->pdfWhitespacesRegex = $pdfWhitespacesRegex;
}
public function getRetainImageContent(): bool
{
return $this->retainImageContent;
}
public function setRetainImageContent(bool $retainImageContent): void
{
$this->retainImageContent = $retainImageContent;
}
public function getDecodeMemoryLimit(): int
{
return $this->decodeMemoryLimit;
}
public function setDecodeMemoryLimit(int $decodeMemoryLimit): void
{
$this->decodeMemoryLimit = $decodeMemoryLimit;
}
public function getDataTmFontInfoHasToBeIncluded(): bool
{
return $this->dataTmFontInfoHasToBeIncluded;
}
public function setDataTmFontInfoHasToBeIncluded(bool $dataTmFontInfoHasToBeIncluded): void
{
$this->dataTmFontInfoHasToBeIncluded = $dataTmFontInfoHasToBeIncluded;
}
public function getIgnoreEncryption(): bool
{
return $this->ignoreEncryption;
}
/**
* @deprecated this is a temporary workaround, don't rely on it
* @see https://github.com/smalot/pdfparser/pull/653
*/
public function setIgnoreEncryption(bool $ignoreEncryption): void
{
$this->ignoreEncryption = $ignoreEncryption;
}
}
pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php 0000644 00000116621 15266563345 0017230 0 ustar 00
*
* @date 2020-01-06
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\RawData;
use Smalot\PdfParser\Config;
use Smalot\PdfParser\Exception\EmptyPdfException;
use Smalot\PdfParser\Exception\MissingPdfHeaderException;
class RawDataParser
{
/**
* @var Config
*/
private $config;
/**
* Configuration array.
*
* @var array
*/
protected $cfg = [
// if `true` ignore filter decoding errors
'ignore_filter_decoding_errors' => true,
// if `true` ignore missing filter decoding errors
'ignore_missing_filter_decoders' => true,
];
protected $filterHelper;
protected $objects;
/**
* @param array $cfg Configuration array, default is []
*/
public function __construct($cfg = [], ?Config $config = null)
{
// merge given array with default values
$this->cfg = array_merge($this->cfg, $cfg);
$this->filterHelper = new FilterHelper();
$this->config = $config ?: new Config();
}
/**
* Decode the specified stream.
*
* @param string $pdfData PDF data
* @param array $sdic Stream's dictionary array
* @param string $stream Stream to decode
*
* @return array containing decoded stream data and remaining filters
*
* @throws \Exception
*/
protected function decodeStream(string $pdfData, array $xref, array $sdic, string $stream): array
{
// get stream length and filters
$slength = \strlen($stream);
if ($slength <= 0) {
return ['', []];
}
$filters = [];
foreach ($sdic as $k => $v) {
if ('/' == $v[0]) {
if (('Length' == $v[1]) && (isset($sdic[$k + 1])) && ('numeric' == $sdic[$k + 1][0])) {
// get declared stream length
$declength = (int) $sdic[$k + 1][1];
if ($declength < $slength) {
$stream = substr($stream, 0, $declength);
$slength = $declength;
}
} elseif (('Filter' == $v[1]) && (isset($sdic[$k + 1]))) {
// resolve indirect object
$objval = $this->getObjectVal($pdfData, $xref, $sdic[$k + 1]);
if ('/' == $objval[0]) {
// single filter
$filters[] = $objval[1];
} elseif ('[' == $objval[0]) {
// array of filters
foreach ($objval[1] as $flt) {
if ('/' == $flt[0]) {
$filters[] = $flt[1];
}
}
}
}
}
}
// decode the stream
$remaining_filters = [];
foreach ($filters as $filter) {
if (\in_array($filter, $this->filterHelper->getAvailableFilters(), true)) {
try {
$stream = $this->filterHelper->decodeFilter($filter, $stream, $this->config->getDecodeMemoryLimit());
} catch (\Exception $e) {
$emsg = $e->getMessage();
if ((('~' == $emsg[0]) && !$this->cfg['ignore_missing_filter_decoders'])
|| (('~' != $emsg[0]) && !$this->cfg['ignore_filter_decoding_errors'])
) {
throw new \Exception($e->getMessage());
}
}
} else {
// add missing filter to array
$remaining_filters[] = $filter;
}
}
return [$stream, $remaining_filters];
}
/**
* Decode the Cross-Reference section
*
* @param string $pdfData PDF data
* @param int $startxref Offset at which the xref section starts (position of the 'xref' keyword)
* @param array $xref Previous xref array (if any)
*
* @return array containing xref and trailer data
*
* @throws \Exception
*/
protected function decodeXref(string $pdfData, int $startxref, array $xref = []): array
{
$startxref += 4; // 4 is the length of the word 'xref'
// skip initial white space chars
$offset = $startxref + strspn($pdfData, $this->config->getPdfWhitespaces(), $startxref);
// initialize object number
$obj_num = 0;
// search for cross-reference entries or subsection
while (preg_match('/([0-9]+)[\x20]([0-9]+)[\x20]?([nf]?)(\r\n|[\x20]?[\r\n])/', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset) > 0) {
if ($matches[0][1] != $offset) {
// we are on another section
break;
}
$offset += \strlen($matches[0][0]);
if ('n' == $matches[3][0]) {
// create unique object index: [object number]_[generation number]
$index = $obj_num.'_'.(int) $matches[2][0];
// check if object already exist
if (!isset($xref['xref'][$index])) {
// store object offset position
$xref['xref'][$index] = (int) $matches[1][0];
}
++$obj_num;
} elseif ('f' == $matches[3][0]) {
++$obj_num;
} else {
// object number (index)
$obj_num = (int) $matches[1][0];
}
}
// get trailer data
if (preg_match('/trailer[\s]*<<(.*)>>/isU', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset) > 0) {
$trailer_data = $matches[1][0];
if (!isset($xref['trailer']) || empty($xref['trailer'])) {
// get only the last updated version
$xref['trailer'] = [];
// parse trailer_data
if (preg_match('/Size[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) {
$xref['trailer']['size'] = (int) $matches[1];
}
if (preg_match('/Root[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
$xref['trailer']['root'] = (int) $matches[1].'_'.(int) $matches[2];
}
if (preg_match('/Encrypt[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
$xref['trailer']['encrypt'] = (int) $matches[1].'_'.(int) $matches[2];
}
if (preg_match('/Info[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
$xref['trailer']['info'] = (int) $matches[1].'_'.(int) $matches[2];
}
if (preg_match('/ID[\s]*[\[][\s]*[<]([^>]*)[>][\s]*[<]([^>]*)[>]/i', $trailer_data, $matches) > 0) {
$xref['trailer']['id'] = [];
$xref['trailer']['id'][0] = $matches[1];
$xref['trailer']['id'][1] = $matches[2];
}
}
if (preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) {
$offset = (int) $matches[1];
if (0 != $offset) {
// get previous xref
$xref = $this->getXrefData($pdfData, $offset, $xref);
}
}
} else {
throw new \Exception('Unable to find trailer');
}
return $xref;
}
/**
* Decode the Cross-Reference Stream section
*
* @param string $pdfData PDF data
* @param int $startxref Offset at which the xref section starts
* @param array $xref Previous xref array (if any)
*
* @return array containing xref and trailer data
*
* @throws \Exception if unknown PNG predictor detected
*/
protected function decodeXrefStream(string $pdfData, int $startxref, array $xref = []): array
{
// try to read Cross-Reference Stream
$xrefobj = $this->getRawObject($pdfData, $startxref);
$xrefcrs = $this->getIndirectObject($pdfData, $xref, $xrefobj[1], $startxref, true);
if (!isset($xref['trailer']) || empty($xref['trailer'])) {
// get only the last updated version
$xref['trailer'] = [];
$filltrailer = true;
} else {
$filltrailer = false;
}
if (!isset($xref['xref'])) {
$xref['xref'] = [];
}
$valid_crs = false;
$columns = 0;
$predictor = null;
$sarr = $xrefcrs[0][1];
if (!\is_array($sarr)) {
$sarr = [];
}
$wb = [];
foreach ($sarr as $k => $v) {
if (
('/' == $v[0])
&& ('Type' == $v[1])
&& (
isset($sarr[$k + 1])
&& '/' == $sarr[$k + 1][0]
&& 'XRef' == $sarr[$k + 1][1]
)
) {
$valid_crs = true;
} elseif (('/' == $v[0]) && ('Index' == $v[1]) && (isset($sarr[$k + 1]))) {
// initialize list for: first object number in the subsection / number of objects
$index_blocks = [];
for ($m = 0; $m < \count($sarr[$k + 1][1]); $m += 2) {
$index_blocks[] = [$sarr[$k + 1][1][$m][1], $sarr[$k + 1][1][$m + 1][1]];
}
} elseif (('/' == $v[0]) && ('Prev' == $v[1]) && (isset($sarr[$k + 1]) && ('numeric' == $sarr[$k + 1][0]))) {
// get previous xref offset
$prevxref = (int) $sarr[$k + 1][1];
} elseif (('/' == $v[0]) && ('W' == $v[1]) && (isset($sarr[$k + 1]))) {
// number of bytes (in the decoded stream) of the corresponding field
$wb[0] = (int) $sarr[$k + 1][1][0][1];
$wb[1] = (int) $sarr[$k + 1][1][1][1];
$wb[2] = (int) $sarr[$k + 1][1][2][1];
} elseif (('/' == $v[0]) && ('DecodeParms' == $v[1]) && (isset($sarr[$k + 1][1]))) {
$decpar = $sarr[$k + 1][1];
foreach ($decpar as $kdc => $vdc) {
if (
'/' == $vdc[0]
&& 'Columns' == $vdc[1]
&& (
isset($decpar[$kdc + 1])
&& 'numeric' == $decpar[$kdc + 1][0]
)
) {
$columns = (int) $decpar[$kdc + 1][1];
} elseif (
'/' == $vdc[0]
&& 'Predictor' == $vdc[1]
&& (
isset($decpar[$kdc + 1])
&& 'numeric' == $decpar[$kdc + 1][0]
)
) {
$predictor = (int) $decpar[$kdc + 1][1];
}
}
} elseif ($filltrailer) {
if (('/' == $v[0]) && ('Size' == $v[1]) && (isset($sarr[$k + 1]) && ('numeric' == $sarr[$k + 1][0]))) {
$xref['trailer']['size'] = $sarr[$k + 1][1];
} elseif (('/' == $v[0]) && ('Root' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) {
$xref['trailer']['root'] = $sarr[$k + 1][1];
} elseif (('/' == $v[0]) && ('Info' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) {
$xref['trailer']['info'] = $sarr[$k + 1][1];
} elseif (('/' == $v[0]) && ('Encrypt' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) {
$xref['trailer']['encrypt'] = $sarr[$k + 1][1];
} elseif (('/' == $v[0]) && ('ID' == $v[1]) && (isset($sarr[$k + 1]))) {
$xref['trailer']['id'] = [];
$xref['trailer']['id'][0] = $sarr[$k + 1][1][0][1];
$xref['trailer']['id'][1] = $sarr[$k + 1][1][1][1];
}
}
}
// decode data
if ($valid_crs && isset($xrefcrs[1][3][0])) {
if (null !== $predictor) {
// number of bytes in a row
$rowlen = ($columns + 1);
// convert the stream into an array of integers
/** @var array */
$sdata = unpack('C*', $xrefcrs[1][3][0]);
// TODO: Handle the case when unpack returns false
// split the rows
$sdata = array_chunk($sdata, $rowlen);
// initialize decoded array
$ddata = [];
// initialize first row with zeros
$prev_row = array_fill(0, $rowlen, 0);
// for each row apply PNG unpredictor
foreach ($sdata as $k => $row) {
// initialize new row
$ddata[$k] = [];
// get PNG predictor value
$predictor = (10 + $row[0]);
// for each byte on the row
for ($i = 1; $i <= $columns; ++$i) {
// new index
$j = ($i - 1);
$row_up = $prev_row[$j];
if (1 == $i) {
$row_left = 0;
$row_upleft = 0;
} else {
$row_left = $row[$i - 1];
$row_upleft = $prev_row[$j - 1];
}
switch ($predictor) {
case 10: // PNG prediction (on encoding, PNG None on all rows)
$ddata[$k][$j] = $row[$i];
break;
case 11: // PNG prediction (on encoding, PNG Sub on all rows)
$ddata[$k][$j] = (($row[$i] + $row_left) & 0xFF);
break;
case 12: // PNG prediction (on encoding, PNG Up on all rows)
$ddata[$k][$j] = (($row[$i] + $row_up) & 0xFF);
break;
case 13: // PNG prediction (on encoding, PNG Average on all rows)
$ddata[$k][$j] = (($row[$i] + (($row_left + $row_up) / 2)) & 0xFF);
break;
case 14: // PNG prediction (on encoding, PNG Paeth on all rows)
// initial estimate
$p = ($row_left + $row_up - $row_upleft);
// distances
$pa = abs($p - $row_left);
$pb = abs($p - $row_up);
$pc = abs($p - $row_upleft);
$pmin = min($pa, $pb, $pc);
// return minimum distance
switch ($pmin) {
case $pa:
$ddata[$k][$j] = (($row[$i] + $row_left) & 0xFF);
break;
case $pb:
$ddata[$k][$j] = (($row[$i] + $row_up) & 0xFF);
break;
case $pc:
$ddata[$k][$j] = (($row[$i] + $row_upleft) & 0xFF);
break;
}
break;
default: // PNG prediction (on encoding, PNG optimum)
throw new \Exception('Unknown PNG predictor: '.$predictor);
}
}
$prev_row = $ddata[$k];
} // end for each row
// complete decoding
} else {
// number of bytes in a row
$rowlen = array_sum($wb);
if (0 < $rowlen) {
// convert the stream into an array of integers
$sdata = unpack('C*', $xrefcrs[1][3][0]);
// split the rows
$ddata = array_chunk($sdata, $rowlen);
} else {
// if the row length is zero, $ddata should be an empty array as well
$ddata = [];
}
}
$sdata = [];
// for every row
foreach ($ddata as $k => $row) {
// initialize new row
$sdata[$k] = [0, 0, 0];
if (0 == $wb[0]) {
// default type field
$sdata[$k][0] = 1;
}
$i = 0; // count bytes in the row
// for every column
for ($c = 0; $c < 3; ++$c) {
// for every byte on the column
for ($b = 0; $b < $wb[$c]; ++$b) {
if (isset($row[$i])) {
$sdata[$k][$c] += ($row[$i] << (($wb[$c] - 1 - $b) * 8));
}
++$i;
}
}
}
// fill xref
if (isset($index_blocks)) {
// load the first object number of the first /Index entry
$obj_num = $index_blocks[0][0];
} else {
$obj_num = 0;
}
foreach ($sdata as $k => $row) {
switch ($row[0]) {
case 0: // (f) linked list of free objects
break;
case 1: // (n) objects that are in use but are not compressed
// create unique object index: [object number]_[generation number]
$index = $obj_num.'_'.$row[2];
// check if object already exist
if (!isset($xref['xref'][$index])) {
// store object offset position
$xref['xref'][$index] = $row[1];
}
break;
case 2: // compressed objects
// $row[1] = object number of the object stream in which this object is stored
// $row[2] = index of this object within the object stream
$index = $row[1].'_0_'.$row[2];
$xref['xref'][$index] = -1;
break;
default: // null objects
break;
}
++$obj_num;
if (isset($index_blocks)) {
// reduce the number of remaining objects
--$index_blocks[0][1];
if (0 == $index_blocks[0][1]) {
// remove the actual used /Index entry
array_shift($index_blocks);
if (0 < \count($index_blocks)) {
// load the first object number of the following /Index entry
$obj_num = $index_blocks[0][0];
} else {
// if there are no more entries, remove $index_blocks to avoid actions on an empty array
unset($index_blocks);
}
}
}
}
} // end decoding data
if (isset($prevxref)) {
// get previous xref
$xref = $this->getXrefData($pdfData, $prevxref, $xref);
}
return $xref;
}
protected function getObjectHeaderPattern(array $objRefs): string
{
// consider all whitespace character (PDF specifications)
return '/'.$objRefs[0].$this->config->getPdfWhitespacesRegex().$objRefs[1].$this->config->getPdfWhitespacesRegex().'obj/';
}
protected function getObjectHeaderLen(array $objRefs): int
{
// "4 0 obj"
// 2 whitespaces + strlen("obj") = 5
return 5 + \strlen($objRefs[0]) + \strlen($objRefs[1]);
}
/**
* Get content of indirect object.
*
* @param string $pdfData PDF data
* @param string $objRef Object number and generation number separated by underscore character
* @param int $offset Object offset
* @param bool $decoding If true decode streams
*
* @return array containing object data
*
* @throws \Exception if invalid object reference found
*/
protected function getIndirectObject(string $pdfData, array $xref, string $objRef, int $offset = 0, bool $decoding = true): array
{
/*
* build indirect object header
*/
// $objHeader = "[object number] [generation number] obj"
$objRefArr = explode('_', $objRef);
if (2 !== \count($objRefArr)) {
throw new \Exception('Invalid object reference for $obj.');
}
$objHeaderLen = $this->getObjectHeaderLen($objRefArr);
/*
* check if we are in position
*/
// ignore whitespace characters at offset
$offset += strspn($pdfData, $this->config->getPdfWhitespaces(), $offset);
// ignore leading zeros for object number
$offset += strspn($pdfData, '0', $offset);
if (0 == preg_match($this->getObjectHeaderPattern($objRefArr), substr($pdfData, $offset, $objHeaderLen))) {
// an indirect reference to an undefined object shall be considered a reference to the null object
return ['null', 'null', $offset];
}
/*
* get content
*/
// starting position of object content
$offset += $objHeaderLen;
$objContentArr = [];
$i = 0; // object main index
$header = null;
do {
$oldOffset = $offset;
// get element
$element = $this->getRawObject($pdfData, $offset, null != $header ? $header[1] : null);
$offset = $element[2];
// decode stream using stream's dictionary information
if ($decoding && ('stream' === $element[0]) && null != $header) {
$element[3] = $this->decodeStream($pdfData, $xref, $header[1], $element[1]);
}
$objContentArr[$i] = $element;
$header = isset($element[0]) && '<<' === $element[0] ? $element : null;
++$i;
} while (('endobj' !== $element[0]) && ($offset !== $oldOffset));
// remove closing delimiter
array_pop($objContentArr);
/*
* return raw object content
*/
return $objContentArr;
}
/**
* Get the content of object, resolving indirect object reference if necessary.
*
* @param string $pdfData PDF data
* @param array $obj Object value
*
* @return array containing object data
*
* @throws \Exception
*/
protected function getObjectVal(string $pdfData, $xref, array $obj): array
{
if ('objref' == $obj[0]) {
// reference to indirect object
if (isset($this->objects[$obj[1]])) {
// this object has been already parsed
return $this->objects[$obj[1]];
} elseif (isset($xref[$obj[1]])) {
// parse new object
$this->objects[$obj[1]] = $this->getIndirectObject($pdfData, $xref, $obj[1], $xref[$obj[1]], false);
return $this->objects[$obj[1]];
}
}
return $obj;
}
/**
* Get object type, raw value and offset to next object
*
* @param int $offset Object offset
* @param array|null $headerDic obj header's dictionary, parsed by getRawObject. Used for stream parsing optimization
*
* @return array containing object type, raw value and offset to next object
*/
protected function getRawObject(string $pdfData, int $offset = 0, ?array $headerDic = null): array
{
$objtype = ''; // object type to be returned
$objval = ''; // object value to be returned
// skip initial white space chars
$offset += strspn($pdfData, $this->config->getPdfWhitespaces(), $offset);
// get first char
$char = $pdfData[$offset];
// get object type
switch ($char) {
case '%': // \x25 PERCENT SIGN
// skip comment and search for next token
$next = strcspn($pdfData, "\r\n", $offset);
if ($next > 0) {
$offset += $next;
return $this->getRawObject($pdfData, $offset);
}
break;
case '/': // \x2F SOLIDUS
// name object
$objtype = $char;
++$offset;
$span = strcspn($pdfData, "\x00\x09\x0a\x0c\x0d\x20\n\t\r\v\f\x28\x29\x3c\x3e\x5b\x5d\x7b\x7d\x2f\x25", $offset, 256);
if ($span > 0) {
$objval = substr($pdfData, $offset, $span); // unescaped value
$offset += $span;
}
break;
case '(': // \x28 LEFT PARENTHESIS
case ')': // \x29 RIGHT PARENTHESIS
// literal string object
$objtype = $char;
++$offset;
$strpos = $offset;
if ('(' == $char) {
$open_bracket = 1;
while ($open_bracket > 0) {
if (!isset($pdfData[$strpos])) {
break;
}
$ch = $pdfData[$strpos];
switch ($ch) {
case '\\': // REVERSE SOLIDUS (5Ch) (Backslash)
// skip next character
++$strpos;
break;
case '(': // LEFT PARENHESIS (28h)
++$open_bracket;
break;
case ')': // RIGHT PARENTHESIS (29h)
--$open_bracket;
break;
}
++$strpos;
}
$objval = substr($pdfData, $offset, $strpos - $offset - 1);
$offset = $strpos;
}
break;
case '[': // \x5B LEFT SQUARE BRACKET
case ']': // \x5D RIGHT SQUARE BRACKET
// array object
$objtype = $char;
++$offset;
if ('[' == $char) {
// get array content
$objval = [];
do {
$oldOffset = $offset;
// get element
$element = $this->getRawObject($pdfData, $offset);
$offset = $element[2];
$objval[] = $element;
} while ((']' != $element[0]) && ($offset != $oldOffset));
// remove closing delimiter
array_pop($objval);
}
break;
case '<': // \x3C LESS-THAN SIGN
case '>': // \x3E GREATER-THAN SIGN
if (isset($pdfData[$offset + 1]) && ($pdfData[$offset + 1] == $char)) {
// dictionary object
$objtype = $char.$char;
$offset += 2;
if ('<' == $char) {
// get array content
$objval = [];
do {
$oldOffset = $offset;
// get element
$element = $this->getRawObject($pdfData, $offset);
$offset = $element[2];
$objval[] = $element;
} while (('>>' != $element[0]) && ($offset != $oldOffset));
// remove closing delimiter
array_pop($objval);
}
} else {
// hexadecimal string object
$objtype = $char;
++$offset;
$span = strspn($pdfData, "0123456789abcdefABCDEF\x09\x0a\x0c\x0d\x20", $offset);
$dataToCheck = $pdfData[$offset + $span] ?? null;
if ('<' == $char && $span > 0 && '>' == $dataToCheck) {
// remove white space characters
$objval = strtr(substr($pdfData, $offset, $span), $this->config->getPdfWhitespaces(), '');
$offset += $span + 1;
} elseif (false !== ($endpos = strpos($pdfData, '>', $offset))) {
$offset = $endpos + 1;
}
}
break;
default:
if ('endobj' == substr($pdfData, $offset, 6)) {
// indirect object
$objtype = 'endobj';
$offset += 6;
} elseif ('null' == substr($pdfData, $offset, 4)) {
// null object
$objtype = 'null';
$offset += 4;
$objval = 'null';
} elseif ('true' == substr($pdfData, $offset, 4)) {
// boolean true object
$objtype = 'boolean';
$offset += 4;
$objval = 'true';
} elseif ('false' == substr($pdfData, $offset, 5)) {
// boolean false object
$objtype = 'boolean';
$offset += 5;
$objval = 'false';
} elseif ('stream' == substr($pdfData, $offset, 6)) {
// start stream object
$objtype = 'stream';
$offset += 6;
if (1 == preg_match('/^( *[\r]?[\n])/isU', substr($pdfData, $offset, 4), $matches)) {
$offset += \strlen($matches[0]);
// we get stream length here to later help preg_match test less data
$streamLen = (int) $this->getHeaderValue($headerDic, 'Length', 'numeric', 0);
$skip = false === $this->config->getRetainImageContent() && 'XObject' == $this->getHeaderValue($headerDic, 'Type', '/') && 'Image' == $this->getHeaderValue($headerDic, 'Subtype', '/');
$pregResult = preg_match(
'/(endstream)[\x09\x0a\x0c\x0d\x20]/isU',
$pdfData,
$matches,
\PREG_OFFSET_CAPTURE,
$offset + $streamLen
);
if (1 == $pregResult) {
$objval = $skip ? '' : substr($pdfData, $offset, $matches[0][1] - $offset);
$offset = $matches[1][1];
}
}
} elseif ('endstream' == substr($pdfData, $offset, 9)) {
// end stream object
$objtype = 'endstream';
$offset += 9;
} elseif (1 == preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+R/iU', substr($pdfData, $offset, 33), $matches)) {
// indirect object reference
$objtype = 'objref';
$offset += \strlen($matches[0]);
$objval = (int) $matches[1].'_'.(int) $matches[2];
} elseif (1 == preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+obj/iU', substr($pdfData, $offset, 33), $matches)) {
// object start
$objtype = 'obj';
$objval = (int) $matches[1].'_'.(int) $matches[2];
$offset += \strlen($matches[0]);
} elseif (($numlen = strspn($pdfData, '+-.0123456789', $offset)) > 0) {
// numeric object
$objtype = 'numeric';
$objval = substr($pdfData, $offset, $numlen);
$offset += $numlen;
}
break;
}
return [$objtype, $objval, $offset];
}
/**
* Get value of an object header's section (obj << YYY >> part ).
*
* It is similar to Header::get('...')->getContent(), the only difference is it can be used during the parsing process,
* when no Smalot\PdfParser\Header objects are created yet.
*
* @param string $key header's section name
* @param string $type type of the section (i.e. 'numeric', '/', '<<', etc.)
* @param string|array|null $default default value for header's section
*
* @return string|array|null value of obj header's section, or default value if none found, or its type doesn't match $type param
*/
private function getHeaderValue(?array $headerDic, string $key, string $type, $default = '')
{
if (false === \is_array($headerDic)) {
return $default;
}
/*
* It recieves dictionary of header fields, as it is returned by RawDataParser::getRawObject,
* iterates over it, searching for section of type '/' whith requested key.
* If such a section is found, it tries to receive it's value (next object in dictionary),
* returning it, if it matches requested type, or default value otherwise.
*/
foreach ($headerDic as $i => $val) {
$isSectionName = \is_array($val) && 3 == \count($val) && '/' == $val[0];
if (
$isSectionName
&& $val[1] == $key
&& isset($headerDic[$i + 1])
) {
$isSectionValue = \is_array($headerDic[$i + 1]) && 1 < \count($headerDic[$i + 1]);
return $isSectionValue && $type == $headerDic[$i + 1][0]
? $headerDic[$i + 1][1]
: $default;
}
}
return $default;
}
/**
* Get Cross-Reference (xref) table and trailer data from PDF document data.
*
* @param int $offset xref offset (if known)
* @param array $xref previous xref array (if any)
*
* @return array containing xref and trailer data
*
* @throws \Exception if it was unable to find startxref
* @throws \Exception if it was unable to find xref
*/
protected function getXrefData(string $pdfData, int $offset = 0, array $xref = []): array
{
// If the $offset is currently pointed at whitespace, bump it
// forward until it isn't; affects loosely targetted offsets
// for the 'xref' keyword
// See: https://github.com/smalot/pdfparser/issues/673
$bumpOffset = $offset;
while (preg_match('/\s/', substr($pdfData, $bumpOffset, 1))) {
++$bumpOffset;
}
// Find all startxref tables from this $offset forward
$startxrefPreg = preg_match_all(
'/(?<=[\r\n])startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i',
$pdfData,
$startxrefMatches,
\PREG_SET_ORDER,
$offset
);
if (0 == $startxrefPreg) {
// No startxref tables were found
throw new \Exception('Unable to find startxref');
} elseif (0 == $offset) {
// Use the last startxref in the document
$startxref = (int) $startxrefMatches[\count($startxrefMatches) - 1][1];
} elseif (strpos($pdfData, 'xref', $bumpOffset) == $bumpOffset) {
// Already pointing at the xref table
$startxref = $bumpOffset;
} elseif (preg_match('/([0-9]+[\s][0-9]+[\s]obj)/i', $pdfData, $matches, 0, $bumpOffset)) {
// Cross-Reference Stream object
$startxref = $bumpOffset;
} else {
// Use the next startxref from this $offset
$startxref = (int) $startxrefMatches[0][1];
}
if ($startxref > \strlen($pdfData)) {
throw new \Exception('Unable to find xref (PDF corrupted?)');
}
// check xref position
if (strpos($pdfData, 'xref', $startxref) == $startxref) {
// Cross-Reference
$xref = $this->decodeXref($pdfData, $startxref, $xref);
} else {
// Check if the $pdfData might have the wrong line-endings
$pdfDataUnix = str_replace("\r\n", "\n", $pdfData);
if ($startxref < \strlen($pdfDataUnix) && strpos($pdfDataUnix, 'xref', $startxref) == $startxref) {
// Return Unix-line-ending flag
$xref = ['Unix' => true];
} else {
// Cross-Reference Stream
$xref = $this->decodeXrefStream($pdfData, $startxref, $xref);
}
}
if (empty($xref)) {
throw new \Exception('Unable to find xref');
}
return $xref;
}
/**
* Parses PDF data and returns extracted data as array.
*
* @param string $data PDF data to parse
*
* @return array array of parsed PDF document objects
*
* @throws EmptyPdfException if empty PDF data given
* @throws MissingPdfHeaderException if PDF data missing `%PDF-` header
*/
public function parseData(string $data): array
{
if (empty($data)) {
throw new EmptyPdfException('Empty PDF data given.');
}
// find the pdf header starting position
if (false === ($trimpos = strpos($data, '%PDF-'))) {
throw new MissingPdfHeaderException('Invalid PDF data: Missing `%PDF-` header.');
}
// get PDF content string
$pdfData = $trimpos > 0 ? substr($data, $trimpos) : $data;
// get xref and trailer data
$xref = $this->getXrefData($pdfData);
// If we found Unix line-endings
if (isset($xref['Unix'])) {
$pdfData = str_replace("\r\n", "\n", $pdfData);
$xref = $this->getXrefData($pdfData);
}
// parse all document objects
$objects = [];
foreach ($xref['xref'] as $obj => $offset) {
if (!isset($objects[$obj]) && ($offset > 0)) {
// decode objects with positive offset
$objects[$obj] = $this->getIndirectObject($pdfData, $xref, $obj, $offset, true);
}
}
return [$xref, $objects];
}
}
pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php 0000644 00000033617 15266563345 0017120 0 ustar 00
*
* @date 2020-01-06
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\RawData;
use Smalot\PdfParser\Exception\NotImplementedException;
class FilterHelper
{
protected $availableFilters = ['ASCIIHexDecode', 'ASCII85Decode', 'LZWDecode', 'FlateDecode', 'RunLengthDecode'];
/**
* Decode data using the specified filter type.
*
* @param string $filter Filter name
* @param string $data Data to decode
*
* @return string Decoded data string
*
* @throws \Exception
* @throws \Smalot\PdfParser\Exception\NotImplementedException if a certain decode function is not implemented yet
*/
public function decodeFilter(string $filter, string $data, int $decodeMemoryLimit = 0): string
{
switch ($filter) {
case 'ASCIIHexDecode':
return $this->decodeFilterASCIIHexDecode($data);
case 'ASCII85Decode':
return $this->decodeFilterASCII85Decode($data);
case 'LZWDecode':
return $this->decodeFilterLZWDecode($data);
case 'FlateDecode':
return $this->decodeFilterFlateDecode($data, $decodeMemoryLimit);
case 'RunLengthDecode':
return $this->decodeFilterRunLengthDecode($data);
case 'CCITTFaxDecode':
throw new NotImplementedException('Decode CCITTFaxDecode not implemented yet.');
case 'JBIG2Decode':
throw new NotImplementedException('Decode JBIG2Decode not implemented yet.');
case 'DCTDecode':
throw new NotImplementedException('Decode DCTDecode not implemented yet.');
case 'JPXDecode':
throw new NotImplementedException('Decode JPXDecode not implemented yet.');
case 'Crypt':
throw new NotImplementedException('Decode Crypt not implemented yet.');
default:
return $data;
}
}
/**
* ASCIIHexDecode
*
* Decodes data encoded in an ASCII hexadecimal representation, reproducing the original binary data.
*
* @param string $data Data to decode
*
* @return string data string
*
* @throws \Exception
*/
protected function decodeFilterASCIIHexDecode(string $data): string
{
// all white-space characters shall be ignored
$data = preg_replace('/[\s]/', '', $data);
// check for EOD character: GREATER-THAN SIGN (3Eh)
$eod = strpos($data, '>');
if (false !== $eod) {
// remove EOD and extra data (if any)
$data = substr($data, 0, $eod);
$eod = true;
}
// get data length
$data_length = \strlen($data);
if (0 != ($data_length % 2)) {
// odd number of hexadecimal digits
if ($eod) {
// EOD shall behave as if a 0 (zero) followed the last digit
$data = substr($data, 0, -1).'0'.substr($data, -1);
} else {
throw new \Exception('decodeFilterASCIIHexDecode: invalid code');
}
}
// check for invalid characters
if (preg_match('/[^a-fA-F\d]/', $data) > 0) {
throw new \Exception('decodeFilterASCIIHexDecode: invalid code');
}
// get one byte of binary data for each pair of ASCII hexadecimal digits
$decoded = pack('H*', $data);
return $decoded;
}
/**
* ASCII85Decode
*
* Decodes data encoded in an ASCII base-85 representation, reproducing the original binary data.
*
* @param string $data Data to decode
*
* @return string data string
*
* @throws \Exception
*/
protected function decodeFilterASCII85Decode(string $data): string
{
// initialize string to return
$decoded = '';
// all white-space characters shall be ignored
$data = preg_replace('/[\s]/', '', $data);
// remove start sequence 2-character sequence <~ (3Ch)(7Eh)
if (0 === strpos($data, '<~')) {
// remove EOD and extra data (if any)
$data = substr($data, 2);
}
// check for EOD: 2-character sequence ~> (7Eh)(3Eh)
$eod = strpos($data, '~>');
if (\strlen($data) - 2 === $eod) {
// remove EOD and extra data (if any)
$data = substr($data, 0, $eod);
}
// data length
$data_length = \strlen($data);
// check for invalid characters
if (preg_match('/[^\x21-\x75,\x74]/', $data) > 0) {
throw new \Exception('decodeFilterASCII85Decode: invalid code');
}
// z sequence
$zseq = \chr(0).\chr(0).\chr(0).\chr(0);
// position inside a group of 4 bytes (0-3)
$group_pos = 0;
$tuple = 0;
$pow85 = [85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1];
// for each byte
for ($i = 0; $i < $data_length; ++$i) {
// get char value
$char = \ord($data[$i]);
if (122 == $char) { // 'z'
if (0 == $group_pos) {
$decoded .= $zseq;
} else {
throw new \Exception('decodeFilterASCII85Decode: invalid code');
}
} else {
// the value represented by a group of 5 characters should never be greater than 2^32 - 1
$tuple += (($char - 33) * $pow85[$group_pos]);
if (4 == $group_pos) {
$decoded .= \chr($tuple >> 24).\chr($tuple >> 16).\chr($tuple >> 8).\chr($tuple);
$tuple = 0;
$group_pos = 0;
} else {
++$group_pos;
}
}
}
if ($group_pos > 1) {
$tuple += $pow85[$group_pos - 1];
}
// last tuple (if any)
switch ($group_pos) {
case 4:
$decoded .= \chr($tuple >> 24).\chr($tuple >> 16).\chr($tuple >> 8);
break;
case 3:
$decoded .= \chr($tuple >> 24).\chr($tuple >> 16);
break;
case 2:
$decoded .= \chr($tuple >> 24);
break;
case 1:
throw new \Exception('decodeFilterASCII85Decode: invalid code');
}
return $decoded;
}
/**
* FlateDecode
*
* Decompresses data encoded using the zlib/deflate compression method, reproducing the original text or binary data.
*
* @param string $data Data to decode
* @param int $decodeMemoryLimit Memory limit on deflation
*
* @return string data string
*
* @throws \Exception
*/
protected function decodeFilterFlateDecode(string $data, int $decodeMemoryLimit): ?string
{
// Uncatchable E_WARNING for "data error" is @ suppressed
// so execution may proceed with an alternate decompression
// method.
$decoded = @gzuncompress($data, $decodeMemoryLimit);
if (false === $decoded) {
// If gzuncompress() failed, try again using the compress.zlib://
// wrapper to decode it in a file-based context.
// See: https://www.php.net/manual/en/function.gzuncompress.php#79042
// Issue: https://github.com/smalot/pdfparser/issues/592
$ztmp = tmpfile();
if (false != $ztmp) {
fwrite($ztmp, "\x1f\x8b\x08\x00\x00\x00\x00\x00".$data);
$file = stream_get_meta_data($ztmp)['uri'];
if (0 === $decodeMemoryLimit) {
$decoded = file_get_contents('compress.zlib://'.$file);
} else {
$decoded = file_get_contents('compress.zlib://'.$file, false, null, 0, $decodeMemoryLimit);
}
fclose($ztmp);
}
}
if (false === \is_string($decoded) || '' === $decoded) {
// If the decoded string is empty, that means decoding failed.
throw new \Exception('decodeFilterFlateDecode: invalid data');
}
return $decoded;
}
/**
* LZWDecode
*
* Decompresses data encoded using the LZW (Lempel-Ziv-Welch) adaptive compression method, reproducing the original text or binary data.
*
* @param string $data Data to decode
*
* @return string Data string
*/
protected function decodeFilterLZWDecode(string $data): string
{
// initialize string to return
$decoded = '';
// data length
$data_length = \strlen($data);
// convert string to binary string
$bitstring = '';
for ($i = 0; $i < $data_length; ++$i) {
$bitstring .= \sprintf('%08b', \ord($data[$i]));
}
// get the number of bits
$data_length = \strlen($bitstring);
// initialize code length in bits
$bitlen = 9;
// initialize dictionary index
$dix = 258;
// initialize the dictionary (with the first 256 entries).
$dictionary = [];
for ($i = 0; $i < 256; ++$i) {
$dictionary[$i] = \chr($i);
}
// previous val
$prev_index = 0;
// while we encounter EOD marker (257), read code_length bits
while (($data_length > 0) && (257 != ($index = bindec(substr($bitstring, 0, $bitlen))))) {
// remove read bits from string
$bitstring = substr($bitstring, $bitlen);
// update number of bits
$data_length -= $bitlen;
if (256 == $index) { // clear-table marker
// reset code length in bits
$bitlen = 9;
// reset dictionary index
$dix = 258;
$prev_index = 256;
// reset the dictionary (with the first 256 entries).
$dictionary = [];
for ($i = 0; $i < 256; ++$i) {
$dictionary[$i] = \chr($i);
}
} elseif (256 == $prev_index) {
// first entry
$decoded .= $dictionary[$index];
$prev_index = $index;
} else {
// check if index exist in the dictionary
if ($index < $dix) {
// index exist on dictionary
$decoded .= $dictionary[$index];
$dic_val = $dictionary[$prev_index].$dictionary[$index][0];
// store current index
$prev_index = $index;
} else {
// index do not exist on dictionary
$dic_val = $dictionary[$prev_index].$dictionary[$prev_index][0];
$decoded .= $dic_val;
}
// update dictionary
$dictionary[$dix] = $dic_val;
++$dix;
// change bit length by case
if (2047 == $dix) {
$bitlen = 12;
} elseif (1023 == $dix) {
$bitlen = 11;
} elseif (511 == $dix) {
$bitlen = 10;
}
}
}
return $decoded;
}
/**
* RunLengthDecode
*
* Decompresses data encoded using a byte-oriented run-length encoding algorithm.
*
* @param string $data Data to decode
*/
protected function decodeFilterRunLengthDecode(string $data): string
{
// initialize string to return
$decoded = '';
// data length
$data_length = \strlen($data);
$i = 0;
while ($i < $data_length) {
// get current byte value
$byte = \ord($data[$i]);
if (128 == $byte) {
// a length value of 128 denote EOD
break;
} elseif ($byte < 128) {
// if the length byte is in the range 0 to 127
// the following length + 1 (1 to 128) bytes shall be copied literally during decompression
$decoded .= substr($data, $i + 1, $byte + 1);
// move to next block
$i += ($byte + 2);
} else {
// if length is in the range 129 to 255,
// the following single byte shall be copied 257 - length (2 to 128) times during decompression
$decoded .= str_repeat($data[$i + 1], 257 - $byte);
// move to next block
$i += 2;
}
}
return $decoded;
}
/**
* @return array list of available filters
*/
public function getAvailableFilters(): array
{
return $this->availableFilters;
}
}
pdfparser/src/Smalot/PdfParser/XObject/Form.php 0000644 00000002740 15266563345 0015442 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\XObject;
use Smalot\PdfParser\Header;
use Smalot\PdfParser\Page;
use Smalot\PdfParser\PDFObject;
/**
* Class Form
*/
class Form extends Page
{
public function getText(?Page $page = null): string
{
$header = new Header([], $this->document);
$contents = new PDFObject($this->document, $header, $this->content, $this->config);
return $contents->getText($this);
}
}
pdfparser/src/Smalot/PdfParser/XObject/Image.php 0000644 00000002443 15266563345 0015561 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\XObject;
use Smalot\PdfParser\Page;
use Smalot\PdfParser\PDFObject;
/**
* Class Image
*/
class Image extends PDFObject
{
public function getText(?Page $page = null): string
{
return '';
}
}
pdfparser/src/Smalot/PdfParser/Font.php 0000644 00000060564 15266563345 0014117 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Encoding\WinAnsiEncoding;
use Smalot\PdfParser\Exception\EncodingNotFoundException;
/**
* Class Font
*/
class Font extends PDFObject
{
public const MISSING = '?';
/**
* @var array
*/
protected $table;
/**
* @var array
*/
protected $tableSizes;
/**
* Caches results from uchr.
*
* @var array
*/
private static $uchrCache = [];
/**
* In some PDF-files encoding could be referenced by object id but object itself does not contain
* `/Type /Encoding` in its dictionary. These objects wouldn't be initialized as Encoding in
* \Smalot\PdfParser\PDFObject::factory() during file parsing (they would be just PDFObject).
*
* Therefore, we create an instance of Encoding from them during decoding and cache this value in this property.
*
* @var Encoding
*
* @see https://github.com/smalot/pdfparser/pull/500
*/
private $initializedEncodingByPdfObject;
public function init()
{
// Load translate table.
$this->loadTranslateTable();
}
public function getName(): string
{
return $this->has('BaseFont') ? (string) $this->get('BaseFont') : '[Unknown]';
}
public function getType(): string
{
return (string) $this->header->get('Subtype');
}
public function getDetails(bool $deep = true): array
{
$details = [];
$details['Name'] = $this->getName();
$details['Type'] = $this->getType();
$details['Encoding'] = ($this->has('Encoding') ? (string) $this->get('Encoding') : 'Ansi');
$details += parent::getDetails($deep);
return $details;
}
/**
* @return string|bool
*/
public function translateChar(string $char, bool $use_default = true)
{
$dec = hexdec(bin2hex($char));
if (\array_key_exists($dec, $this->table)) {
return $this->table[$dec];
}
// fallback for decoding single-byte ANSI characters that are not in the lookup table
$fallbackDecoded = $char;
if (
\strlen($char) < 2
&& $this->has('Encoding')
&& $this->get('Encoding') instanceof Encoding
) {
try {
if (WinAnsiEncoding::class === $this->get('Encoding')->__toString()) {
$fallbackDecoded = self::uchr($dec);
}
} catch (EncodingNotFoundException $e) {
// Encoding->getEncodingClass() throws EncodingNotFoundException when BaseEncoding doesn't exists
// See table 5.11 on PDF 1.5 specs for more info
}
}
return $use_default ? self::MISSING : $fallbackDecoded;
}
/**
* Convert unicode character code to "utf-8" encoded string.
*
* @param int|float $code Unicode character code. Will be casted to int internally!
*/
public static function uchr($code): string
{
// note:
// $code was typed as int before, but changed in https://github.com/smalot/pdfparser/pull/623
// because in some cases uchr was called with a float instead of an integer.
$code = (int) $code;
if (!isset(self::$uchrCache[$code])) {
// html_entity_decode() will not work with UTF-16 or UTF-32 char entities,
// therefore, we use mb_convert_encoding() instead
self::$uchrCache[$code] = mb_convert_encoding("{$code};", 'UTF-8', 'HTML-ENTITIES');
}
return self::$uchrCache[$code];
}
/**
* Init internal chars translation table by ToUnicode CMap.
*/
public function loadTranslateTable(): array
{
if (null !== $this->table) {
return $this->table;
}
$this->table = [];
$this->tableSizes = [
'from' => 1,
'to' => 1,
];
if ($this->has('ToUnicode')) {
$content = $this->get('ToUnicode')->getContent();
$matches = [];
// Support for multiple spacerange sections
if (preg_match_all('/begincodespacerange(?P.*?)endcodespacerange/s', $content, $matches)) {
foreach ($matches['sections'] as $section) {
$regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)>[ \r\n]+/is';
preg_match_all($regexp, $section, $matches);
$this->tableSizes = [
'from' => max(1, \strlen(current($matches['from'])) / 2),
'to' => max(1, \strlen(current($matches['to'])) / 2),
];
break;
}
}
// Support for multiple bfchar sections
if (preg_match_all('/beginbfchar(?P.*?)endbfchar/s', $content, $matches)) {
foreach ($matches['sections'] as $section) {
$regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)>[ \r\n]+/is';
preg_match_all($regexp, $section, $matches);
$this->tableSizes['from'] = max(1, \strlen(current($matches['from'])) / 2);
foreach ($matches['from'] as $key => $from) {
$parts = preg_split(
'/([0-9A-F]{4})/i',
$matches['to'][$key],
0,
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
);
$text = '';
foreach ($parts as $part) {
$text .= self::uchr(hexdec($part));
}
$this->table[hexdec($from)] = $text;
}
}
}
// Support for multiple bfrange sections
if (preg_match_all('/beginbfrange(?P.*?)endbfrange/s', $content, $matches)) {
foreach ($matches['sections'] as $section) {
/**
* Regexp to capture , , and either or [...] items.
* - (?P...) Source range's start
* - (?P...) Source range's end
* - (?P...) Destination range's offset or each char code
* Some PDF file has 2-byte Unicode values on new lines > added \r\n
*/
$regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)> *(?P<[0-9A-F]+>|\[[\r\n<>0-9A-F ]+\])[ \r\n]+/is';
preg_match_all($regexp, $section, $matches);
foreach ($matches['from'] as $key => $from) {
$char_from = hexdec($from);
$char_to = hexdec($matches['to'][$key]);
$dest = $matches['dest'][$key];
if (1 === preg_match('/^<(?P[0-9A-F]+)>$/i', $dest, $offset_matches)) {
// Support for :
$offset = hexdec($offset_matches['offset']);
for ($char = $char_from; $char <= $char_to; ++$char) {
$this->table[$char] = self::uchr($char - $char_from + $offset);
}
} else {
// Support for : [ ... ]
$strings = [];
$matched = preg_match_all('/<(?P[0-9A-F]+)> */is', $dest, $strings);
if (false === $matched || 0 === $matched) {
continue;
}
foreach ($strings['string'] as $position => $string) {
$parts = preg_split(
'/([0-9A-F]{4})/i',
$string,
0,
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
);
if (false === $parts) {
continue;
}
$text = '';
foreach ($parts as $part) {
$text .= self::uchr(hexdec($part));
}
$this->table[$char_from + $position] = $text;
}
}
}
}
}
}
return $this->table;
}
/**
* Set custom char translation table where:
* - key - integer character code;
* - value - "utf-8" encoded value;
*
* @return void
*/
public function setTable(array $table)
{
$this->table = $table;
}
/**
* Calculate text width with data from header 'Widths'. If width of character is not found then character is added to missing array.
*/
public function calculateTextWidth(string $text, ?array &$missing = null): ?float
{
$index_map = array_flip($this->table);
$details = $this->getDetails();
// Usually, Widths key is set in $details array, but if it isn't use an empty array instead.
$widths = $details['Widths'] ?? [];
/*
* Widths array is zero indexed but table is not. We must map them based on FirstChar and LastChar
*
* Note: Without the change you would see warnings in PHP 8.4 because the values of FirstChar or LastChar
* can be null sometimes.
*/
$width_map = array_flip(range((int) $details['FirstChar'], (int) $details['LastChar']));
$width = null;
$missing = [];
$textLength = mb_strlen($text);
for ($i = 0; $i < $textLength; ++$i) {
$char = mb_substr($text, $i, 1);
if (
!\array_key_exists($char, $index_map)
|| !\array_key_exists($index_map[$char], $width_map)
|| !\array_key_exists($width_map[$index_map[$char]], $widths)
) {
$missing[] = $char;
continue;
}
$width_index = $width_map[$index_map[$char]];
$width += $widths[$width_index];
}
return $width;
}
/**
* Decode hexadecimal encoded string. If $add_braces is true result value would be wrapped by parentheses.
*/
public static function decodeHexadecimal(string $hexa, bool $add_braces = false): string
{
// Special shortcut for XML content.
if (false !== stripos($hexa, ')/si', $hexa, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $part) {
if (preg_match('/^<[a-f0-9\s]+>$/si', $part)) {
// strip whitespace
$part = preg_replace("/\s/", '', $part);
$part = trim($part, '<>');
if ($add_braces) {
$text .= '(';
}
$part = pack('H*', $part);
$text .= ($add_braces ? preg_replace('/\\\/s', '\\\\\\', $part) : $part);
if ($add_braces) {
$text .= ')';
}
} else {
$text .= $part;
}
}
return $text;
}
/**
* Decode string with octal-decoded chunks.
*/
public static function decodeOctal(string $text): string
{
// Replace all double backslashes \\ with a special string
$text = strtr($text, ['\\\\' => '[**pdfparserdblslsh**]']);
// Now we can replace all octal codes without worrying about
// escaped backslashes
$text = preg_replace_callback('/\\\\([0-7]{1,3})/', function ($m) {
return \chr(octdec($m[1]));
}, $text);
// Unescape any parentheses
$text = str_replace(['\\(', '\\)'], ['(', ')'], $text);
// Replace instances of the special string with a single backslash
return str_replace('[**pdfparserdblslsh**]', '\\', $text);
}
/**
* Decode string with html entity encoded chars.
*/
public static function decodeEntities(string $text): string
{
return preg_replace_callback('/#([0-9a-f]{2})/i', function ($m) {
return \chr(hexdec($m[1]));
}, $text);
}
/**
* Check if given string is Unicode text (by BOM);
* If true - decode to "utf-8" encoded string.
* Otherwise - return text as is.
*
* @todo Rename in next major release to make the name correspond to reality (for ex. decodeIfUnicode())
*/
public static function decodeUnicode(string $text): string
{
if ("\xFE\xFF" === substr($text, 0, 2)) {
// Strip U+FEFF byte order marker.
$decode = substr($text, 2);
$text = '';
$length = \strlen($decode);
for ($i = 0; $i < $length; $i += 2) {
$text .= self::uchr(hexdec(bin2hex(substr($decode, $i, 2))));
}
}
return $text;
}
/**
* @todo Deprecated, use $this->config->getFontSpaceLimit() instead.
*/
protected function getFontSpaceLimit(): int
{
return $this->config->getFontSpaceLimit();
}
/**
* Decode text by commands array.
*/
public function decodeText(array $commands, float $fontFactor = 4): string
{
$word_position = 0;
$words = [];
$font_space = $this->getFontSpaceLimit() * abs($fontFactor) / 4;
foreach ($commands as $command) {
switch ($command[PDFObject::TYPE]) {
case 'n':
$offset = (float) trim($command[PDFObject::COMMAND]);
if ($offset - (float) $font_space < 0) {
$word_position = \count($words);
}
continue 2;
case '<':
// Decode hexadecimal.
$text = self::decodeHexadecimal('<'.$command[PDFObject::COMMAND].'>');
break;
default:
// Decode octal (if necessary).
$text = self::decodeOctal($command[PDFObject::COMMAND]);
}
// replace escaped chars
$text = str_replace(
['\\\\', '\(', '\)', '\n', '\r', '\t', '\f', '\ ', '\b'],
[\chr(92), \chr(40), \chr(41), \chr(10), \chr(13), \chr(9), \chr(12), \chr(32), \chr(8)],
$text
);
// add content to result string
if (isset($words[$word_position])) {
$words[$word_position] .= $text;
} else {
$words[$word_position] = $text;
}
}
foreach ($words as &$word) {
$word = $this->decodeContent($word);
$word = str_replace("\t", ' ', $word);
}
// Remove internal "words" that are just spaces, but leave them
// if they are at either end of the array of words. This fixes,
// for example, lines that are justified to fill
// a whole row.
for ($x = \count($words) - 2; $x >= 1; --$x) {
if ('' === trim($words[$x], ' ')) {
unset($words[$x]);
}
}
$words = array_values($words);
// Cut down on the number of unnecessary internal spaces by
// imploding the string on the null byte, and checking if the
// text includes extra spaces on either side. If so, merge
// where appropriate.
$words = implode("\x00\x00", $words);
$words = str_replace(
[" \x00\x00 ", "\x00\x00 ", " \x00\x00", "\x00\x00"],
[' ', ' ', ' ', ' '],
$words
);
return $words;
}
/**
* Decode given $text to "utf-8" encoded string.
*
* @param bool $unicode This parameter is deprecated and might be removed in a future release
*/
public function decodeContent(string $text, ?bool &$unicode = null): string
{
// If this string begins with a UTF-16BE BOM, then decode it
// directly as Unicode
if ("\xFE\xFF" === substr($text, 0, 2)) {
return $this->decodeUnicode($text);
}
if ($this->has('ToUnicode')) {
return $this->decodeContentByToUnicodeCMapOrDescendantFonts($text);
}
if ($this->has('Encoding')) {
$result = $this->decodeContentByEncoding($text);
if (null !== $result) {
return $result;
}
}
return $this->decodeContentByAutodetectIfNecessary($text);
}
/**
* First try to decode $text by ToUnicode CMap.
* If char translation not found in ToUnicode CMap tries:
* - If DescendantFonts exists tries to decode char by one of that fonts.
* - If have no success to decode by DescendantFonts interpret $text as a string with "Windows-1252" encoding.
* - If DescendantFonts does not exist just return "?" as decoded char.
*
* @todo Seems this is invalid algorithm that do not follow pdf-format specification. Must be rewritten.
*/
private function decodeContentByToUnicodeCMapOrDescendantFonts(string $text): string
{
$bytes = $this->tableSizes['from'];
if ($bytes) {
$result = '';
$length = \strlen($text);
for ($i = 0; $i < $length; $i += $bytes) {
$char = substr($text, $i, $bytes);
if (false !== ($decoded = $this->translateChar($char, false))) {
$char = $decoded;
} elseif ($this->has('DescendantFonts')) {
if ($this->get('DescendantFonts') instanceof PDFObject) {
$fonts = $this->get('DescendantFonts')->getHeader()->getElements();
} else {
$fonts = $this->get('DescendantFonts')->getContent();
}
$decoded = false;
foreach ($fonts as $font) {
if ($font instanceof self) {
if (false !== ($decoded = $font->translateChar($char, false))) {
$decoded = mb_convert_encoding($decoded, 'UTF-8', 'Windows-1252');
break;
}
}
}
if (false !== $decoded) {
$char = $decoded;
} else {
$char = mb_convert_encoding($char, 'UTF-8', 'Windows-1252');
}
} else {
$char = self::MISSING;
}
$result .= $char;
}
$text = $result;
}
return $text;
}
/**
* Decode content by any type of Encoding (dictionary's item) instance.
*/
private function decodeContentByEncoding(string $text): ?string
{
$encoding = $this->get('Encoding');
// When Encoding referenced by object id (/Encoding 520 0 R) but object itself does not contain `/Type /Encoding` in it's dictionary.
if ($encoding instanceof PDFObject) {
$encoding = $this->getInitializedEncodingByPdfObject($encoding);
}
// When Encoding referenced by object id (/Encoding 520 0 R) but object itself contains `/Type /Encoding` in it's dictionary.
if ($encoding instanceof Encoding) {
return $this->decodeContentByEncodingEncoding($text, $encoding);
}
// When Encoding is just string (/Encoding /WinAnsiEncoding)
if ($encoding instanceof Element) { // todo: ElementString class must by used?
return $this->decodeContentByEncodingElement($text, $encoding);
}
// don't double-encode strings already in UTF-8
if (!mb_check_encoding($text, 'UTF-8')) {
return mb_convert_encoding($text, 'UTF-8', 'Windows-1252');
}
return $text;
}
/**
* Returns already created or create a new one if not created before Encoding instance by PDFObject instance.
*/
private function getInitializedEncodingByPdfObject(PDFObject $PDFObject): Encoding
{
if (!$this->initializedEncodingByPdfObject) {
$this->initializedEncodingByPdfObject = $this->createInitializedEncodingByPdfObject($PDFObject);
}
return $this->initializedEncodingByPdfObject;
}
/**
* Decode content when $encoding (given by $this->get('Encoding')) is instance of Encoding.
*/
private function decodeContentByEncodingEncoding(string $text, Encoding $encoding): string
{
$result = '';
$length = \strlen($text);
for ($i = 0; $i < $length; ++$i) {
$dec_av = hexdec(bin2hex($text[$i]));
$dec_ap = $encoding->translateChar($dec_av);
$result .= self::uchr($dec_ap ?? $dec_av);
}
return $result;
}
/**
* Decode content when $encoding (given by $this->get('Encoding')) is instance of Element.
*/
private function decodeContentByEncodingElement(string $text, Element $encoding): ?string
{
$pdfEncodingName = $encoding->getContent();
// mb_convert_encoding does not support MacRoman/macintosh,
// so we use iconv() here
$iconvEncodingName = $this->getIconvEncodingNameOrNullByPdfEncodingName($pdfEncodingName);
return $iconvEncodingName ? iconv($iconvEncodingName, 'UTF-8//TRANSLIT//IGNORE', $text) : null;
}
/**
* Convert PDF encoding name to iconv-known encoding name.
*/
private function getIconvEncodingNameOrNullByPdfEncodingName(string $pdfEncodingName): ?string
{
$pdfToIconvEncodingNameMap = [
'StandardEncoding' => 'ISO-8859-1',
'MacRomanEncoding' => 'MACINTOSH',
'WinAnsiEncoding' => 'CP1252',
];
return \array_key_exists($pdfEncodingName, $pdfToIconvEncodingNameMap)
? $pdfToIconvEncodingNameMap[$pdfEncodingName]
: null;
}
/**
* If string seems like "utf-8" encoded string do nothing and just return given string as is.
* Otherwise, interpret string as "Window-1252" encoded string.
*
* @return string|false
*/
private function decodeContentByAutodetectIfNecessary(string $text)
{
if (mb_check_encoding($text, 'UTF-8')) {
return $text;
}
return mb_convert_encoding($text, 'UTF-8', 'Windows-1252');
// todo: Why exactly `Windows-1252` used?
}
/**
* Create Encoding instance by PDFObject instance and init it.
*/
private function createInitializedEncodingByPdfObject(PDFObject $PDFObject): Encoding
{
$encoding = $this->createEncodingByPdfObject($PDFObject);
$encoding->init();
return $encoding;
}
/**
* Create Encoding instance by PDFObject instance (without init).
*/
private function createEncodingByPdfObject(PDFObject $PDFObject): Encoding
{
$document = $PDFObject->getDocument();
$header = $PDFObject->getHeader();
$content = $PDFObject->getContent();
$config = $PDFObject->getConfig();
return new Encoding($document, $header, $content, $config);
}
}
pdfparser/src/Smalot/PdfParser/Element.php 0000644 00000011676 15266563345 0014602 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Element\ElementArray;
use Smalot\PdfParser\Element\ElementBoolean;
use Smalot\PdfParser\Element\ElementDate;
use Smalot\PdfParser\Element\ElementHexa;
use Smalot\PdfParser\Element\ElementName;
use Smalot\PdfParser\Element\ElementNull;
use Smalot\PdfParser\Element\ElementNumeric;
use Smalot\PdfParser\Element\ElementString;
use Smalot\PdfParser\Element\ElementStruct;
use Smalot\PdfParser\Element\ElementXRef;
/**
* Class Element
*/
class Element
{
/**
* @var Document|null
*/
protected $document;
protected $value;
public function __construct($value, ?Document $document = null)
{
$this->value = $value;
$this->document = $document;
}
public function init()
{
}
public function equals($value): bool
{
return $value == $this->value;
}
public function contains($value): bool
{
if (\is_array($this->value)) {
/** @var Element $val */
foreach ($this->value as $val) {
if ($val->equals($value)) {
return true;
}
}
return false;
}
return $this->equals($value);
}
public function getContent()
{
return $this->value;
}
public function __toString(): string
{
return (string) $this->value;
}
public static function parse(string $content, ?Document $document = null, int &$position = 0)
{
$args = \func_get_args();
$only_values = isset($args[3]) ? $args[3] : false;
$content = trim($content);
$values = [];
do {
$old_position = $position;
if (!$only_values) {
if (!preg_match('/\G\s*(?P\/[A-Z#0-9\._]+)(?P.*)/si', $content, $match, 0, $position)) {
break;
} else {
$name = preg_replace_callback(
'/#([0-9a-f]{2})/i',
function ($m) {
return \chr(base_convert($m[1], 16, 10));
},
ltrim($match['name'], '/')
);
$value = $match['value'];
$position = strpos($content, $value, $position + \strlen($match['name']));
}
} else {
$name = \count($values);
$value = substr($content, $position);
}
if ($element = ElementName::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementXRef::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementNumeric::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementStruct::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementBoolean::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementNull::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementDate::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementString::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementHexa::parse($value, $document, $position)) {
$values[$name] = $element;
} elseif ($element = ElementArray::parse($value, $document, $position)) {
$values[$name] = $element;
} else {
$position = $old_position;
break;
}
} while ($position < \strlen($content));
return $values;
}
}
pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php 0000644 00000002262 15266563345 0016516 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Font;
use Smalot\PdfParser\Font;
/**
* Class FontTrueType
*/
class FontTrueType extends Font
{
}
pdfparser/src/Smalot/PdfParser/Font/FontType1.php 0000644 00000002254 15266563345 0015740 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Font;
use Smalot\PdfParser\Font;
/**
* Class FontType1
*/
class FontType1 extends Font
{
}
pdfparser/src/Smalot/PdfParser/Font/FontType0.php 0000644 00000002254 15266563345 0015737 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Font;
use Smalot\PdfParser\Font;
/**
* Class FontType0
*/
class FontType0 extends Font
{
}
pdfparser/src/Smalot/PdfParser/Font/FontType3.php 0000644 00000002254 15266563345 0015742 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Font;
use Smalot\PdfParser\Font;
/**
* Class FontType3
*/
class FontType3 extends Font
{
}
pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php 0000644 00000002272 15266563345 0017110 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Font;
use Smalot\PdfParser\Font;
/**
* Class FontCIDFontType2
*/
class FontCIDFontType2 extends Font
{
}
pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php 0000644 00000002272 15266563345 0017106 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Font;
use Smalot\PdfParser\Font;
/**
* Class FontCIDFontType0
*/
class FontCIDFontType0 extends Font
{
}
pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php 0000644 00000115175 15266563345 0020237 0 ustar 00
*
* @date 2019-09-17
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser\Encoding;
/**
* Class PostScriptGlyphs
*/
class PostScriptGlyphs
{
/**
* The mapping tables have been converted from https://github.com/OpenPrinting/cups-filters/blob/master/fontembed/aglfn13.c,
* part of the OpenPrinting/cups-filters package, which itself is licensed under the MIT license and lists this specific code part as:
* Copyright 2008,2012 Tobias Hoffmann under the Expat license (https://www.gnu.org/licenses/license-list.html#Expat)
*/
public static function getGlyphs(): array
{
return [
'space' => '0x00a0',
'exclam' => '0x0021',
'quotedbl' => '0x0022',
'numbersign' => '0x0023',
'dollar' => '0x0024',
'percent' => '0x0025',
'ampersand' => '0x0026',
'quotesingle' => '0x0027',
'parenleft' => '0x0028',
'parenright' => '0x0029',
'asterisk' => '0x002a',
'plus' => '0x002b',
'comma' => '0x002c',
'hyphen' => '0x002d',
'period' => '0x002e',
'slash' => '0x002f',
'zero' => '0x0030',
'one' => '0x0031',
'two' => '0x0032',
'three' => '0x0033',
'four' => '0x0034',
'five' => '0x0035',
'six' => '0x0036',
'seven' => '0x0037',
'eight' => '0x0038',
'nine' => '0x0039',
'colon' => '0x003a',
'semicolon' => '0x003b',
'less' => '0x003c',
'equal' => '0x003d',
'greater' => '0x003e',
'question' => '0x003f',
'at' => '0x0040',
'A' => '0x0041',
'B' => '0x0042',
'C' => '0x0043',
'D' => '0x0044',
'E' => '0x0045',
'F' => '0x0046',
'G' => '0x0047',
'H' => '0x0048',
'I' => '0x0049',
'J' => '0x004a',
'K' => '0x004b',
'L' => '0x004c',
'M' => '0x004d',
'N' => '0x004e',
'O' => '0x004f',
'P' => '0x0050',
'Q' => '0x0051',
'R' => '0x0052',
'S' => '0x0053',
'T' => '0x0054',
'U' => '0x0055',
'V' => '0x0056',
'W' => '0x0057',
'X' => '0x0058',
'Y' => '0x0059',
'Z' => '0x005a',
'bracketleft' => '0x005b',
'backslash' => '0x005c',
'bracketright' => '0x005d',
'asciicircum' => '0x005e',
'underscore' => '0x005f',
'grave' => '0x0060',
'a' => '0x0061',
'b' => '0x0062',
'c' => '0x0063',
'd' => '0x0064',
'e' => '0x0065',
'f' => '0x0066',
'g' => '0x0067',
'h' => '0x0068',
'i' => '0x0069',
'j' => '0x006a',
'k' => '0x006b',
'l' => '0x006c',
'm' => '0x006d',
'n' => '0x006e',
'o' => '0x006f',
'p' => '0x0070',
'q' => '0x0071',
'r' => '0x0072',
's' => '0x0073',
't' => '0x0074',
'u' => '0x0075',
'v' => '0x0076',
'w' => '0x0077',
'x' => '0x0078',
'y' => '0x0079',
'z' => '0x007a',
'braceleft' => '0x007b',
'bar' => '0x007c',
'braceright' => '0x007d',
'asciitilde' => '0x007e',
'exclamdown' => '0x00a1',
'cent' => '0x00a2',
'sterling' => '0x00a3',
'currency' => '0x00a4',
'yen' => '0x00a5',
'brokenbar' => '0x00a6',
'section' => '0x00a7',
'dieresis' => '0x00a8',
'copyright' => '0x00a9',
'ordfeminine' => '0x00aa',
'guillemotleft' => '0x00ab',
'logicalnot' => '0x00ac',
'minus' => '0x2212',
'registered' => '0x00ae',
'macron' => '0x02c9',
'degree' => '0x00b0',
'plusminus' => '0x00b1',
'twosuperior' => '0x00b2',
'threesuperior' => '0x00b3',
'acute' => '0x00b4',
'mu' => '0x03bc',
'paragraph' => '0x00b6',
'periodcentered' => '0x2219',
'cedilla' => '0x00b8',
'onesuperior' => '0x00b9',
'ordmasculine' => '0x00ba',
'guillemotright' => '0x00bb',
'onequarter' => '0x00bc',
'onehalf' => '0x00bd',
'threequarters' => '0x00be',
'questiondown' => '0x00bf',
'Agrave' => '0x00c0',
'Aacute' => '0x00c1',
'Acircumflex' => '0x00c2',
'Atilde' => '0x00c3',
'Adieresis' => '0x00c4',
'Aring' => '0x00c5',
'AE' => '0x00c6',
'Ccedilla' => '0x00c7',
'Egrave' => '0x00c8',
'Eacute' => '0x00c9',
'Ecircumflex' => '0x00ca',
'Edieresis' => '0x00cb',
'Igrave' => '0x00cc',
'Iacute' => '0x00cd',
'Icircumflex' => '0x00ce',
'Idieresis' => '0x00cf',
'Eth' => '0x00d0',
'Ntilde' => '0x00d1',
'Ograve' => '0x00d2',
'Oacute' => '0x00d3',
'Ocircumflex' => '0x00d4',
'Otilde' => '0x00d5',
'Odieresis' => '0x00d6',
'multiply' => '0x00d7',
'Oslash' => '0x00d8',
'Ugrave' => '0x00d9',
'Uacute' => '0x00da',
'Ucircumflex' => '0x00db',
'Udieresis' => '0x00dc',
'Yacute' => '0x00dd',
'Thorn' => '0x00de',
'germandbls' => '0x00df',
'agrave' => '0x00e0',
'aacute' => '0x00e1',
'acircumflex' => '0x00e2',
'atilde' => '0x00e3',
'adieresis' => '0x00e4',
'aring' => '0x00e5',
'ae' => '0x00e6',
'ccedilla' => '0x00e7',
'egrave' => '0x00e8',
'eacute' => '0x00e9',
'ecircumflex' => '0x00ea',
'edieresis' => '0x00eb',
'igrave' => '0x00ec',
'iacute' => '0x00ed',
'icircumflex' => '0x00ee',
'idieresis' => '0x00ef',
'eth' => '0x00f0',
'ntilde' => '0x00f1',
'ograve' => '0x00f2',
'oacute' => '0x00f3',
'ocircumflex' => '0x00f4',
'otilde' => '0x00f5',
'odieresis' => '0x00f6',
'divide' => '0x00f7',
'oslash' => '0x00f8',
'ugrave' => '0x00f9',
'uacute' => '0x00fa',
'ucircumflex' => '0x00fb',
'udieresis' => '0x00fc',
'yacute' => '0x00fd',
'thorn' => '0x00fe',
'ydieresis' => '0x00ff',
'Amacron' => '0x0100',
'amacron' => '0x0101',
'Abreve' => '0x0102',
'abreve' => '0x0103',
'Aogonek' => '0x0104',
'aogonek' => '0x0105',
'Cacute' => '0x0106',
'cacute' => '0x0107',
'Ccircumflex' => '0x0108',
'ccircumflex' => '0x0109',
'Cdotaccent' => '0x010a',
'cdotaccent' => '0x010b',
'Ccaron' => '0x010c',
'ccaron' => '0x010d',
'Dcaron' => '0x010e',
'dcaron' => '0x010f',
'Dcroat' => '0x0110',
'dcroat' => '0x0111',
'Emacron' => '0x0112',
'emacron' => '0x0113',
'Ebreve' => '0x0114',
'ebreve' => '0x0115',
'Edotaccent' => '0x0116',
'edotaccent' => '0x0117',
'Eogonek' => '0x0118',
'eogonek' => '0x0119',
'Ecaron' => '0x011a',
'ecaron' => '0x011b',
'Gcircumflex' => '0x011c',
'gcircumflex' => '0x011d',
'Gbreve' => '0x011e',
'gbreve' => '0x011f',
'Gdotaccent' => '0x0120',
'gdotaccent' => '0x0121',
'Gcommaaccent' => '0x0122',
'gcommaaccent' => '0x0123',
'Hcircumflex' => '0x0124',
'hcircumflex' => '0x0125',
'Hbar' => '0x0126',
'hbar' => '0x0127',
'Itilde' => '0x0128',
'itilde' => '0x0129',
'Imacron' => '0x012a',
'imacron' => '0x012b',
'Ibreve' => '0x012c',
'ibreve' => '0x012d',
'Iogonek' => '0x012e',
'iogonek' => '0x012f',
'Idotaccent' => '0x0130',
'dotlessi' => '0x0131',
'IJ' => '0x0132',
'ij' => '0x0133',
'Jcircumflex' => '0x0134',
'jcircumflex' => '0x0135',
'Kcommaaccent' => '0x0136',
'kcommaaccent' => '0x0137',
'kgreenlandic' => '0x0138',
'Lacute' => '0x0139',
'lacute' => '0x013a',
'Lcommaaccent' => '0x013b',
'lcommaaccent' => '0x013c',
'Lcaron' => '0x013d',
'lcaron' => '0x013e',
'Ldot' => '0x013f',
'ldot' => '0x0140',
'Lslash' => '0x0141',
'lslash' => '0x0142',
'Nacute' => '0x0143',
'nacute' => '0x0144',
'Ncommaaccent' => '0x0145',
'ncommaaccent' => '0x0146',
'Ncaron' => '0x0147',
'ncaron' => '0x0148',
'napostrophe' => '0x0149',
'Eng' => '0x014a',
'eng' => '0x014b',
'Omacron' => '0x014c',
'omacron' => '0x014d',
'Obreve' => '0x014e',
'obreve' => '0x014f',
'Ohungarumlaut' => '0x0150',
'ohungarumlaut' => '0x0151',
'OE' => '0x0152',
'oe' => '0x0153',
'Racute' => '0x0154',
'racute' => '0x0155',
'Rcommaaccent' => '0x0156',
'rcommaaccent' => '0x0157',
'Rcaron' => '0x0158',
'rcaron' => '0x0159',
'Sacute' => '0x015a',
'sacute' => '0x015b',
'Scircumflex' => '0x015c',
'scircumflex' => '0x015d',
'Scedilla' => '0xf6c1',
'scedilla' => '0xf6c2',
'Scaron' => '0x0160',
'scaron' => '0x0161',
'Tcommaaccent' => '0x021a',
'tcommaaccent' => '0x021b',
'Tcaron' => '0x0164',
'tcaron' => '0x0165',
'Tbar' => '0x0166',
'tbar' => '0x0167',
'Utilde' => '0x0168',
'utilde' => '0x0169',
'Umacron' => '0x016a',
'umacron' => '0x016b',
'Ubreve' => '0x016c',
'ubreve' => '0x016d',
'Uring' => '0x016e',
'uring' => '0x016f',
'Uhungarumlaut' => '0x0170',
'uhungarumlaut' => '0x0171',
'Uogonek' => '0x0172',
'uogonek' => '0x0173',
'Wcircumflex' => '0x0174',
'wcircumflex' => '0x0175',
'Ycircumflex' => '0x0176',
'ycircumflex' => '0x0177',
'Ydieresis' => '0x0178',
'Zacute' => '0x0179',
'zacute' => '0x017a',
'Zdotaccent' => '0x017b',
'zdotaccent' => '0x017c',
'Zcaron' => '0x017d',
'zcaron' => '0x017e',
'longs' => '0x017f',
'florin' => '0x0192',
'Ohorn' => '0x01a0',
'ohorn' => '0x01a1',
'Uhorn' => '0x01af',
'uhorn' => '0x01b0',
'Gcaron' => '0x01e6',
'gcaron' => '0x01e7',
'Aringacute' => '0x01fa',
'aringacute' => '0x01fb',
'AEacute' => '0x01fc',
'aeacute' => '0x01fd',
'Oslashacute' => '0x01fe',
'oslashacute' => '0x01ff',
'Scommaaccent' => '0x0218',
'scommaaccent' => '0x0219',
'afii57929' => '0x02bc',
'afii64937' => '0x02bd',
'circumflex' => '0x02c6',
'caron' => '0x02c7',
'breve' => '0x02d8',
'dotaccent' => '0x02d9',
'ring' => '0x02da',
'ogonek' => '0x02db',
'tilde' => '0x02dc',
'hungarumlaut' => '0x02dd',
'gravecomb' => '0x0300',
'acutecomb' => '0x0301',
'tildecomb' => '0x0303',
'hookabovecomb' => '0x0309',
'dotbelowcomb' => '0x0323',
'tonos' => '0x0384',
'dieresistonos' => '0x0385',
'Alphatonos' => '0x0386',
'anoteleia' => '0x0387',
'Epsilontonos' => '0x0388',
'Etatonos' => '0x0389',
'Iotatonos' => '0x038a',
'Omicrontonos' => '0x038c',
'Upsilontonos' => '0x038e',
'Omegatonos' => '0x038f',
'iotadieresistonos' => '0x0390',
'Alpha' => '0x0391',
'Beta' => '0x0392',
'Gamma' => '0x0393',
'Delta' => '0x2206',
'Epsilon' => '0x0395',
'Zeta' => '0x0396',
'Eta' => '0x0397',
'Theta' => '0x0398',
'Iota' => '0x0399',
'Kappa' => '0x039a',
'Lambda' => '0x039b',
'Mu' => '0x039c',
'Nu' => '0x039d',
'Xi' => '0x039e',
'Omicron' => '0x039f',
'Pi' => '0x03a0',
'Rho' => '0x03a1',
'Sigma' => '0x03a3',
'Tau' => '0x03a4',
'Upsilon' => '0x03a5',
'Phi' => '0x03a6',
'Chi' => '0x03a7',
'Psi' => '0x03a8',
'Omega' => '0x2126',
'Iotadieresis' => '0x03aa',
'Upsilondieresis' => '0x03ab',
'alphatonos' => '0x03ac',
'epsilontonos' => '0x03ad',
'etatonos' => '0x03ae',
'iotatonos' => '0x03af',
'upsilondieresistonos' => '0x03b0',
'alpha' => '0x03b1',
'beta' => '0x03b2',
'gamma' => '0x03b3',
'delta' => '0x03b4',
'epsilon' => '0x03b5',
'zeta' => '0x03b6',
'eta' => '0x03b7',
'theta' => '0x03b8',
'iota' => '0x03b9',
'kappa' => '0x03ba',
'lambda' => '0x03bb',
'nu' => '0x03bd',
'xi' => '0x03be',
'omicron' => '0x03bf',
'pi' => '0x03c0',
'rho' => '0x03c1',
'sigma1' => '0x03c2',
'sigma' => '0x03c3',
'tau' => '0x03c4',
'upsilon' => '0x03c5',
'phi' => '0x03c6',
'chi' => '0x03c7',
'psi' => '0x03c8',
'omega' => '0x03c9',
'iotadieresis' => '0x03ca',
'upsilondieresis' => '0x03cb',
'omicrontonos' => '0x03cc',
'upsilontonos' => '0x03cd',
'omegatonos' => '0x03ce',
'theta1' => '0x03d1',
'Upsilon1' => '0x03d2',
'phi1' => '0x03d5',
'omega1' => '0x03d6',
'afii10023' => '0x0401',
'afii10051' => '0x0402',
'afii10052' => '0x0403',
'afii10053' => '0x0404',
'afii10054' => '0x0405',
'afii10055' => '0x0406',
'afii10056' => '0x0407',
'afii10057' => '0x0408',
'afii10058' => '0x0409',
'afii10059' => '0x040a',
'afii10060' => '0x040b',
'afii10061' => '0x040c',
'afii10062' => '0x040e',
'afii10145' => '0x040f',
'afii10017' => '0x0410',
'afii10018' => '0x0411',
'afii10019' => '0x0412',
'afii10020' => '0x0413',
'afii10021' => '0x0414',
'afii10022' => '0x0415',
'afii10024' => '0x0416',
'afii10025' => '0x0417',
'afii10026' => '0x0418',
'afii10027' => '0x0419',
'afii10028' => '0x041a',
'afii10029' => '0x041b',
'afii10030' => '0x041c',
'afii10031' => '0x041d',
'afii10032' => '0x041e',
'afii10033' => '0x041f',
'afii10034' => '0x0420',
'afii10035' => '0x0421',
'afii10036' => '0x0422',
'afii10037' => '0x0423',
'afii10038' => '0x0424',
'afii10039' => '0x0425',
'afii10040' => '0x0426',
'afii10041' => '0x0427',
'afii10042' => '0x0428',
'afii10043' => '0x0429',
'afii10044' => '0x042a',
'afii10045' => '0x042b',
'afii10046' => '0x042c',
'afii10047' => '0x042d',
'afii10048' => '0x042e',
'afii10049' => '0x042f',
'afii10065' => '0x0430',
'afii10066' => '0x0431',
'afii10067' => '0x0432',
'afii10068' => '0x0433',
'afii10069' => '0x0434',
'afii10070' => '0x0435',
'afii10072' => '0x0436',
'afii10073' => '0x0437',
'afii10074' => '0x0438',
'afii10075' => '0x0439',
'afii10076' => '0x043a',
'afii10077' => '0x043b',
'afii10078' => '0x043c',
'afii10079' => '0x043d',
'afii10080' => '0x043e',
'afii10081' => '0x043f',
'afii10082' => '0x0440',
'afii10083' => '0x0441',
'afii10084' => '0x0442',
'afii10085' => '0x0443',
'afii10086' => '0x0444',
'afii10087' => '0x0445',
'afii10088' => '0x0446',
'afii10089' => '0x0447',
'afii10090' => '0x0448',
'afii10091' => '0x0449',
'afii10092' => '0x044a',
'afii10093' => '0x044b',
'afii10094' => '0x044c',
'afii10095' => '0x044d',
'afii10096' => '0x044e',
'afii10097' => '0x044f',
'afii10071' => '0x0451',
'afii10099' => '0x0452',
'afii10100' => '0x0453',
'afii10101' => '0x0454',
'afii10102' => '0x0455',
'afii10103' => '0x0456',
'afii10104' => '0x0457',
'afii10105' => '0x0458',
'afii10106' => '0x0459',
'afii10107' => '0x045a',
'afii10108' => '0x045b',
'afii10109' => '0x045c',
'afii10110' => '0x045e',
'afii10193' => '0x045f',
'afii10146' => '0x0462',
'afii10194' => '0x0463',
'afii10147' => '0x0472',
'afii10195' => '0x0473',
'afii10148' => '0x0474',
'afii10196' => '0x0475',
'afii10050' => '0x0490',
'afii10098' => '0x0491',
'afii10846' => '0x04d9',
'afii57799' => '0x05b0',
'afii57801' => '0x05b1',
'afii57800' => '0x05b2',
'afii57802' => '0x05b3',
'afii57793' => '0x05b4',
'afii57794' => '0x05b5',
'afii57795' => '0x05b6',
'afii57798' => '0x05b7',
'afii57797' => '0x05b8',
'afii57806' => '0x05b9',
'afii57796' => '0x05bb',
'afii57807' => '0x05bc',
'afii57839' => '0x05bd',
'afii57645' => '0x05be',
'afii57841' => '0x05bf',
'afii57842' => '0x05c0',
'afii57804' => '0x05c1',
'afii57803' => '0x05c2',
'afii57658' => '0x05c3',
'afii57664' => '0x05d0',
'afii57665' => '0x05d1',
'afii57666' => '0x05d2',
'afii57667' => '0x05d3',
'afii57668' => '0x05d4',
'afii57669' => '0x05d5',
'afii57670' => '0x05d6',
'afii57671' => '0x05d7',
'afii57672' => '0x05d8',
'afii57673' => '0x05d9',
'afii57674' => '0x05da',
'afii57675' => '0x05db',
'afii57676' => '0x05dc',
'afii57677' => '0x05dd',
'afii57678' => '0x05de',
'afii57679' => '0x05df',
'afii57680' => '0x05e0',
'afii57681' => '0x05e1',
'afii57682' => '0x05e2',
'afii57683' => '0x05e3',
'afii57684' => '0x05e4',
'afii57685' => '0x05e5',
'afii57686' => '0x05e6',
'afii57687' => '0x05e7',
'afii57688' => '0x05e8',
'afii57689' => '0x05e9',
'afii57690' => '0x05ea',
'afii57716' => '0x05f0',
'afii57717' => '0x05f1',
'afii57718' => '0x05f2',
'afii57388' => '0x060c',
'afii57403' => '0x061b',
'afii57407' => '0x061f',
'afii57409' => '0x0621',
'afii57410' => '0x0622',
'afii57411' => '0x0623',
'afii57412' => '0x0624',
'afii57413' => '0x0625',
'afii57414' => '0x0626',
'afii57415' => '0x0627',
'afii57416' => '0x0628',
'afii57417' => '0x0629',
'afii57418' => '0x062a',
'afii57419' => '0x062b',
'afii57420' => '0x062c',
'afii57421' => '0x062d',
'afii57422' => '0x062e',
'afii57423' => '0x062f',
'afii57424' => '0x0630',
'afii57425' => '0x0631',
'afii57426' => '0x0632',
'afii57427' => '0x0633',
'afii57428' => '0x0634',
'afii57429' => '0x0635',
'afii57430' => '0x0636',
'afii57431' => '0x0637',
'afii57432' => '0x0638',
'afii57433' => '0x0639',
'afii57434' => '0x063a',
'afii57440' => '0x0640',
'afii57441' => '0x0641',
'afii57442' => '0x0642',
'afii57443' => '0x0643',
'afii57444' => '0x0644',
'afii57445' => '0x0645',
'afii57446' => '0x0646',
'afii57470' => '0x0647',
'afii57448' => '0x0648',
'afii57449' => '0x0649',
'afii57450' => '0x064a',
'afii57451' => '0x064b',
'afii57452' => '0x064c',
'afii57453' => '0x064d',
'afii57454' => '0x064e',
'afii57455' => '0x064f',
'afii57456' => '0x0650',
'afii57457' => '0x0651',
'afii57458' => '0x0652',
'afii57392' => '0x0660',
'afii57393' => '0x0661',
'afii57394' => '0x0662',
'afii57395' => '0x0663',
'afii57396' => '0x0664',
'afii57397' => '0x0665',
'afii57398' => '0x0666',
'afii57399' => '0x0667',
'afii57400' => '0x0668',
'afii57401' => '0x0669',
'afii57381' => '0x066a',
'afii63167' => '0x066d',
'afii57511' => '0x0679',
'afii57506' => '0x067e',
'afii57507' => '0x0686',
'afii57512' => '0x0688',
'afii57513' => '0x0691',
'afii57508' => '0x0698',
'afii57505' => '0x06a4',
'afii57509' => '0x06af',
'afii57514' => '0x06ba',
'afii57519' => '0x06d2',
'afii57534' => '0x06d5',
'Wgrave' => '0x1e80',
'wgrave' => '0x1e81',
'Wacute' => '0x1e82',
'wacute' => '0x1e83',
'Wdieresis' => '0x1e84',
'wdieresis' => '0x1e85',
'Ygrave' => '0x1ef2',
'ygrave' => '0x1ef3',
'afii61664' => '0x200c',
'afii301' => '0x200d',
'afii299' => '0x200e',
'afii300' => '0x200f',
'figuredash' => '0x2012',
'endash' => '0x2013',
'emdash' => '0x2014',
'afii00208' => '0x2015',
'underscoredbl' => '0x2017',
'quoteleft' => '0x2018',
'quoteright' => '0x2019',
'quotesinglbase' => '0x201a',
'quotereversed' => '0x201b',
'quotedblleft' => '0x201c',
'quotedblright' => '0x201d',
'quotedblbase' => '0x201e',
'dagger' => '0x2020',
'daggerdbl' => '0x2021',
'bullet' => '0x2022',
'onedotenleader' => '0x2024',
'twodotenleader' => '0x2025',
'ellipsis' => '0x2026',
'afii61573' => '0x202c',
'afii61574' => '0x202d',
'afii61575' => '0x202e',
'perthousand' => '0x2030',
'minute' => '0x2032',
'second' => '0x2033',
'guilsinglleft' => '0x2039',
'guilsinglright' => '0x203a',
'exclamdbl' => '0x203c',
'fraction' => '0x2215',
'zerosuperior' => '0x2070',
'foursuperior' => '0x2074',
'fivesuperior' => '0x2075',
'sixsuperior' => '0x2076',
'sevensuperior' => '0x2077',
'eightsuperior' => '0x2078',
'ninesuperior' => '0x2079',
'parenleftsuperior' => '0x207d',
'parenrightsuperior' => '0x207e',
'nsuperior' => '0x207f',
'zeroinferior' => '0x2080',
'oneinferior' => '0x2081',
'twoinferior' => '0x2082',
'threeinferior' => '0x2083',
'fourinferior' => '0x2084',
'fiveinferior' => '0x2085',
'sixinferior' => '0x2086',
'seveninferior' => '0x2087',
'eightinferior' => '0x2088',
'nineinferior' => '0x2089',
'parenleftinferior' => '0x208d',
'parenrightinferior' => '0x208e',
'colonmonetary' => '0x20a1',
'franc' => '0x20a3',
'lira' => '0x20a4',
'peseta' => '0x20a7',
'afii57636' => '0x20aa',
'dong' => '0x20ab',
'Euro' => '0x20ac',
'afii61248' => '0x2105',
'Ifraktur' => '0x2111',
'afii61289' => '0x2113',
'afii61352' => '0x2116',
'weierstrass' => '0x2118',
'Rfraktur' => '0x211c',
'prescription' => '0x211e',
'trademark' => '0x2122',
'estimated' => '0x212e',
'aleph' => '0x2135',
'onethird' => '0x2153',
'twothirds' => '0x2154',
'oneeighth' => '0x215b',
'threeeighths' => '0x215c',
'fiveeighths' => '0x215d',
'seveneighths' => '0x215e',
'arrowleft' => '0x2190',
'arrowup' => '0x2191',
'arrowright' => '0x2192',
'arrowdown' => '0x2193',
'arrowboth' => '0x2194',
'arrowupdn' => '0x2195',
'arrowupdnbse' => '0x21a8',
'carriagereturn' => '0x21b5',
'arrowdblleft' => '0x21d0',
'arrowdblup' => '0x21d1',
'arrowdblright' => '0x21d2',
'arrowdbldown' => '0x21d3',
'arrowdblboth' => '0x21d4',
'universal' => '0x2200',
'partialdiff' => '0x2202',
'existential' => '0x2203',
'emptyset' => '0x2205',
'gradient' => '0x2207',
'element' => '0x2208',
'notelement' => '0x2209',
'suchthat' => '0x220b',
'product' => '0x220f',
'summation' => '0x2211',
'asteriskmath' => '0x2217',
'radical' => '0x221a',
'proportional' => '0x221d',
'infinity' => '0x221e',
'orthogonal' => '0x221f',
'angle' => '0x2220',
'logicaland' => '0x2227',
'logicalor' => '0x2228',
'intersection' => '0x2229',
'union' => '0x222a',
'integral' => '0x222b',
'therefore' => '0x2234',
'similar' => '0x223c',
'congruent' => '0x2245',
'approxequal' => '0x2248',
'notequal' => '0x2260',
'equivalence' => '0x2261',
'lessequal' => '0x2264',
'greaterequal' => '0x2265',
'propersubset' => '0x2282',
'propersuperset' => '0x2283',
'notsubset' => '0x2284',
'reflexsubset' => '0x2286',
'reflexsuperset' => '0x2287',
'circleplus' => '0x2295',
'circlemultiply' => '0x2297',
'perpendicular' => '0x22a5',
'dotmath' => '0x22c5',
'house' => '0x2302',
'revlogicalnot' => '0x2310',
'integraltp' => '0x2320',
'integralbt' => '0x2321',
'angleleft' => '0x2329',
'angleright' => '0x232a',
'SF100000' => '0x2500',
'SF110000' => '0x2502',
'SF010000' => '0x250c',
'SF030000' => '0x2510',
'SF020000' => '0x2514',
'SF040000' => '0x2518',
'SF080000' => '0x251c',
'SF090000' => '0x2524',
'SF060000' => '0x252c',
'SF070000' => '0x2534',
'SF050000' => '0x253c',
'SF430000' => '0x2550',
'SF240000' => '0x2551',
'SF510000' => '0x2552',
'SF520000' => '0x2553',
'SF390000' => '0x2554',
'SF220000' => '0x2555',
'SF210000' => '0x2556',
'SF250000' => '0x2557',
'SF500000' => '0x2558',
'SF490000' => '0x2559',
'SF380000' => '0x255a',
'SF280000' => '0x255b',
'SF270000' => '0x255c',
'SF260000' => '0x255d',
'SF360000' => '0x255e',
'SF370000' => '0x255f',
'SF420000' => '0x2560',
'SF190000' => '0x2561',
'SF200000' => '0x2562',
'SF230000' => '0x2563',
'SF470000' => '0x2564',
'SF480000' => '0x2565',
'SF410000' => '0x2566',
'SF450000' => '0x2567',
'SF460000' => '0x2568',
'SF400000' => '0x2569',
'SF540000' => '0x256a',
'SF530000' => '0x256b',
'SF440000' => '0x256c',
'upblock' => '0x2580',
'dnblock' => '0x2584',
'block' => '0x2588',
'lfblock' => '0x258c',
'rtblock' => '0x2590',
'ltshade' => '0x2591',
'shade' => '0x2592',
'dkshade' => '0x2593',
'filledbox' => '0x25a0',
'H22073' => '0x25a1',
'H18543' => '0x25aa',
'H18551' => '0x25ab',
'filledrect' => '0x25ac',
'triagup' => '0x25b2',
'triagrt' => '0x25ba',
'triagdn' => '0x25bc',
'triaglf' => '0x25c4',
'lozenge' => '0x25ca',
'circle' => '0x25cb',
'H18533' => '0x25cf',
'invbullet' => '0x25d8',
'invcircle' => '0x25d9',
'openbullet' => '0x25e6',
'smileface' => '0x263a',
'invsmileface' => '0x263b',
'sun' => '0x263c',
'female' => '0x2640',
'male' => '0x2642',
'spade' => '0x2660',
'club' => '0x2663',
'heart' => '0x2665',
'diamond' => '0x2666',
'musicalnote' => '0x266a',
'musicalnotedbl' => '0x266b',
'dotlessj' => '0xf6be',
'LL' => '0xf6bf',
'll' => '0xf6c0',
'commaaccent' => '0xf6c3',
'afii10063' => '0xf6c4',
'afii10064' => '0xf6c5',
'afii10192' => '0xf6c6',
'afii10831' => '0xf6c7',
'afii10832' => '0xf6c8',
'Acute' => '0xf6c9',
'Caron' => '0xf6ca',
'Dieresis' => '0xf6cb',
'DieresisAcute' => '0xf6cc',
'DieresisGrave' => '0xf6cd',
'Grave' => '0xf6ce',
'Hungarumlaut' => '0xf6cf',
'Macron' => '0xf6d0',
'cyrBreve' => '0xf6d1',
'cyrFlex' => '0xf6d2',
'dblGrave' => '0xf6d3',
'cyrbreve' => '0xf6d4',
'cyrflex' => '0xf6d5',
'dblgrave' => '0xf6d6',
'dieresisacute' => '0xf6d7',
'dieresisgrave' => '0xf6d8',
'copyrightserif' => '0xf6d9',
'registerserif' => '0xf6da',
'trademarkserif' => '0xf6db',
'onefitted' => '0xf6dc',
'rupiah' => '0xf6dd',
'threequartersemdash' => '0xf6de',
'centinferior' => '0xf6df',
'centsuperior' => '0xf6e0',
'commainferior' => '0xf6e1',
'commasuperior' => '0xf6e2',
'dollarinferior' => '0xf6e3',
'dollarsuperior' => '0xf6e4',
'hypheninferior' => '0xf6e5',
'hyphensuperior' => '0xf6e6',
'periodinferior' => '0xf6e7',
'periodsuperior' => '0xf6e8',
'asuperior' => '0xf6e9',
'bsuperior' => '0xf6ea',
'dsuperior' => '0xf6eb',
'esuperior' => '0xf6ec',
'isuperior' => '0xf6ed',
'lsuperior' => '0xf6ee',
'msuperior' => '0xf6ef',
'osuperior' => '0xf6f0',
'rsuperior' => '0xf6f1',
'ssuperior' => '0xf6f2',
'tsuperior' => '0xf6f3',
'Brevesmall' => '0xf6f4',
'Caronsmall' => '0xf6f5',
'Circumflexsmall' => '0xf6f6',
'Dotaccentsmall' => '0xf6f7',
'Hungarumlautsmall' => '0xf6f8',
'Lslashsmall' => '0xf6f9',
'OEsmall' => '0xf6fa',
'Ogoneksmall' => '0xf6fb',
'Ringsmall' => '0xf6fc',
'Scaronsmall' => '0xf6fd',
'Tildesmall' => '0xf6fe',
'Zcaronsmall' => '0xf6ff',
'exclamsmall' => '0xf721',
'dollaroldstyle' => '0xf724',
'ampersandsmall' => '0xf726',
'zerooldstyle' => '0xf730',
'oneoldstyle' => '0xf731',
'twooldstyle' => '0xf732',
'threeoldstyle' => '0xf733',
'fouroldstyle' => '0xf734',
'fiveoldstyle' => '0xf735',
'sixoldstyle' => '0xf736',
'sevenoldstyle' => '0xf737',
'eightoldstyle' => '0xf738',
'nineoldstyle' => '0xf739',
'questionsmall' => '0xf73f',
'Gravesmall' => '0xf760',
'Asmall' => '0xf761',
'Bsmall' => '0xf762',
'Csmall' => '0xf763',
'Dsmall' => '0xf764',
'Esmall' => '0xf765',
'Fsmall' => '0xf766',
'Gsmall' => '0xf767',
'Hsmall' => '0xf768',
'Ismall' => '0xf769',
'Jsmall' => '0xf76a',
'Ksmall' => '0xf76b',
'Lsmall' => '0xf76c',
'Msmall' => '0xf76d',
'Nsmall' => '0xf76e',
'Osmall' => '0xf76f',
'Psmall' => '0xf770',
'Qsmall' => '0xf771',
'Rsmall' => '0xf772',
'Ssmall' => '0xf773',
'Tsmall' => '0xf774',
'Usmall' => '0xf775',
'Vsmall' => '0xf776',
'Wsmall' => '0xf777',
'Xsmall' => '0xf778',
'Ysmall' => '0xf779',
'Zsmall' => '0xf77a',
'exclamdownsmall' => '0xf7a1',
'centoldstyle' => '0xf7a2',
'Dieresissmall' => '0xf7a8',
'Macronsmall' => '0xf7af',
'Acutesmall' => '0xf7b4',
'Cedillasmall' => '0xf7b8',
'questiondownsmall' => '0xf7bf',
'Agravesmall' => '0xf7e0',
'Aacutesmall' => '0xf7e1',
'Acircumflexsmall' => '0xf7e2',
'Atildesmall' => '0xf7e3',
'Adieresissmall' => '0xf7e4',
'Aringsmall' => '0xf7e5',
'AEsmall' => '0xf7e6',
'Ccedillasmall' => '0xf7e7',
'Egravesmall' => '0xf7e8',
'Eacutesmall' => '0xf7e9',
'Ecircumflexsmall' => '0xf7ea',
'Edieresissmall' => '0xf7eb',
'Igravesmall' => '0xf7ec',
'Iacutesmall' => '0xf7ed',
'Icircumflexsmall' => '0xf7ee',
'Idieresissmall' => '0xf7ef',
'Ethsmall' => '0xf7f0',
'Ntildesmall' => '0xf7f1',
'Ogravesmall' => '0xf7f2',
'Oacutesmall' => '0xf7f3',
'Ocircumflexsmall' => '0xf7f4',
'Otildesmall' => '0xf7f5',
'Odieresissmall' => '0xf7f6',
'Oslashsmall' => '0xf7f8',
'Ugravesmall' => '0xf7f9',
'Uacutesmall' => '0xf7fa',
'Ucircumflexsmall' => '0xf7fb',
'Udieresissmall' => '0xf7fc',
'Yacutesmall' => '0xf7fd',
'Thornsmall' => '0xf7fe',
'Ydieresissmall' => '0xf7ff',
'radicalex' => '0xf8e5',
'arrowvertex' => '0xf8e6',
'arrowhorizex' => '0xf8e7',
'registersans' => '0xf8e8',
'copyrightsans' => '0xf8e9',
'trademarksans' => '0xf8ea',
'parenlefttp' => '0xf8eb',
'parenleftex' => '0xf8ec',
'parenleftbt' => '0xf8ed',
'bracketlefttp' => '0xf8ee',
'bracketleftex' => '0xf8ef',
'bracketleftbt' => '0xf8f0',
'bracelefttp' => '0xf8f1',
'braceleftmid' => '0xf8f2',
'braceleftbt' => '0xf8f3',
'braceex' => '0xf8f4',
'integralex' => '0xf8f5',
'parenrighttp' => '0xf8f6',
'parenrightex' => '0xf8f7',
'parenrightbt' => '0xf8f8',
'bracketrighttp' => '0xf8f9',
'bracketrightex' => '0xf8fa',
'bracketrightbt' => '0xf8fb',
'bracerighttp' => '0xf8fc',
'bracerightmid' => '0xf8fd',
'bracerightbt' => '0xf8fe',
'ff' => '0xfb00',
'fi' => '0xfb01',
'fl' => '0xfb02',
'ffi' => '0xfb03',
'ffl' => '0xfb04',
'afii57705' => '0xfb1f',
'afii57694' => '0xfb2a',
'afii57695' => '0xfb2b',
'afii57723' => '0xfb35',
'afii57700' => '0xfb4b',
];
}
public static function getCodePoint($glyph): ?int
{
$glyphsMap = static::getGlyphs();
if (isset($glyphsMap[$glyph])) {
return hexdec($glyphsMap[$glyph]);
}
return null;
}
}
pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php 0000644 00000007123 15266563345 0020073 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
// Source : http://www.opensource.apple.com/source/vim/vim-34/vim/runtime/print/mac-roman.ps
namespace Smalot\PdfParser\Encoding;
/**
* Class MacRomanEncoding
*/
class MacRomanEncoding extends AbstractEncoding
{
public function getTranslations(): array
{
$encoding =
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'space exclam quotedbl numbersign dollar percent ampersand quotesingle '.
'parenleft parenright asterisk plus comma minus period slash '.
'zero one two three four five six seven '.
'eight nine colon semicolon less equal greater question '.
'at A B C D E F G '.
'H I J K L M N O '.
'P Q R S T U V W '.
'X Y Z bracketleft backslash bracketright asciicircum underscore '.
'grave a b c d e f g '.
'h i j k l m n o '.
'p q r s t u v w '.
'x y z braceleft bar braceright asciitilde .notdef '.
'Adieresis Aring Ccedilla Eacute Ntilde Odieresis Udieresis aacute '.
'agrave acircumflex adieresis atilde aring ccedilla eacute egrave '.
'ecircumflex edieresis iacute igrave icircumflex idieresis ntilde oacute '.
'ograve ocircumflex odieresis otilde uacute ugrave ucircumflex udieresis '.
'dagger degree cent sterling section bullet paragraph germandbls '.
'registered copyright trademark acute dieresis notequal AE Oslash '.
'infinity plusminus lessequal greaterequal yen mu partialdiff summation '.
'Pi pi integral ordfeminine ordmasculine Omega ae oslash '.
'questiondown exclamdown logicalnot radical florin approxequal delta guillemotleft '.
'guillemotright ellipsis space Agrave Atilde Otilde OE oe '.
'endash emdash quotedblleft quotedblright quoteleft quoteright divide lozenge '.
'ydieresis Ydieresis fraction currency guilsinglleft guilsinglright fi fl '.
'daggerdbl periodcentered quotesinglbase quotedblbase perthousand Acircumflex Ecircumflex Aacute '.
'Edieresis Egrave Iacute Icircumflex Idieresis Igrave Oacute Ocircumflex '.
'heart Ograve Uacute Ucircumflex Ugrave dotlessi circumflex tilde '.
'macron breve dotaccent ring cedilla hungarumlaut ogonek caron';
return explode(' ', $encoding);
}
}
pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php 0000644 00000006772 15266563345 0020122 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin9Encoding.pm
namespace Smalot\PdfParser\Encoding;
/**
* Class ISOLatin9Encoding
*/
class ISOLatin9Encoding extends AbstractEncoding
{
public function getTranslations(): array
{
$encoding =
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'space exclam quotedbl numbersign dollar percent ampersand quoteright '.
'parenleft parenright asterisk plus comma minus period slash zero one '.
'two three four five six seven eight nine colon semicolon less equal '.
'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '.
'Y Z bracketleft backslash bracketright asciicircum underscore '.
'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '.
'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef dotlessi grave acute '.
'circumflex tilde macron breve dotaccent dieresis .notdef ring '.
'cedilla .notdef hungarumlaut ogonek caron space exclamdown cent '.
'sterling Euro yen Scaron section scaron copyright '.
'ordfeminine guillemotleft logicalnot hyphen registered macron degree '.
'plusminus twosuperior threesuperior Zcaron mu paragraph '.
'periodcentered zcaron onesuperior ordmasculine guillemotright '.
'OE oe Ydieresis questiondown Agrave Aacute '.
'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '.
'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '.
'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '.
'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '.
'acircumflex atilde adieresis aring ae ccedilla egrave eacute '.
'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '.
'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '.
'uacute ucircumflex udieresis yacute thorn ydieresis';
return explode(' ', $encoding);
}
}
pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php 0000644 00000007050 15266563345 0017745 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/WinANSIEncoding.pm
namespace Smalot\PdfParser\Encoding;
/**
* Class WinAnsiEncoding
*/
class WinAnsiEncoding extends AbstractEncoding
{
public function getTranslations(): array
{
$encoding =
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'space exclam quotedbl numbersign dollar percent ampersand quotesingle '.
'parenleft parenright asterisk plus comma hyphen period slash zero one '.
'two three four five six seven eight nine colon semicolon less equal '.
'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '.
'Y Z bracketleft backslash bracketright asciicircum underscore '.
'grave a b c d e f g h i j k l m n o p q r s t u v w x y z '.
'braceleft bar braceright asciitilde bullet Euro bullet quotesinglbase '.
'florin quotedblbase ellipsis dagger daggerdbl circumflex perthousand '.
'Scaron guilsinglleft OE bullet Zcaron bullet bullet quoteleft quoteright '.
'quotedblleft quotedblright bullet endash emdash tilde trademark scaron '.
'guilsinglright oe bullet zcaron Ydieresis space exclamdown cent '.
'sterling currency yen brokenbar section dieresis copyright '.
'ordfeminine guillemotleft logicalnot hyphen registered macron degree '.
'plusminus twosuperior threesuperior acute mu paragraph '.
'periodcentered cedilla onesuperior ordmasculine guillemotright '.
'onequarter onehalf threequarters questiondown Agrave Aacute '.
'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '.
'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '.
'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '.
'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '.
'acircumflex atilde adieresis aring ae ccedilla egrave eacute '.
'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '.
'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '.
'uacute ucircumflex udieresis yacute thorn ydieresis';
return explode(' ', $encoding);
}
}
pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php 0000644 00000006773 15266563345 0020150 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/StandardEncoding.pm
namespace Smalot\PdfParser\Encoding;
/**
* Class StandardEncoding
*/
class StandardEncoding extends AbstractEncoding
{
public function getTranslations(): array
{
$encoding =
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'space exclam quotedbl numbersign dollar percent ampersand quoteright '.
'parenleft parenright asterisk plus comma hyphen period slash zero '.
'one two three four five six seven eight nine colon semicolon less '.
'equal greater question at A B C D E F G H I J K L M N O P Q R S T U '.
'V W X Y Z bracketleft backslash bracketright asciicircum underscore '.
'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '.
'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef exclamdown cent '.
'sterling fraction yen florin section currency quotesingle '.
'quotedblleft guillemotleft guilsinglleft guilsinglright fi fl '.
'.notdef endash dagger daggerdbl periodcentered .notdef paragraph '.
'bullet quotesinglbase quotedblbase quotedblright guillemotright '.
'ellipsis perthousand .notdef questiondown .notdef grave acute '.
'circumflex tilde macron breve dotaccent dieresis .notdef ring '.
'cedilla .notdef hungarumlaut ogonek caron emdash .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef AE .notdef '.
'ordfeminine .notdef .notdef .notdef .notdef Lslash Oslash OE '.
'ordmasculine .notdef .notdef .notdef .notdef .notdef ae .notdef '.
'.notdef .notdef dotlessi .notdef .notdef lslash oslash oe germandbls '.
'.notdef .notdef .notdef .notdef';
return explode(' ', $encoding);
}
}
pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin1Encoding.php 0000644 00000007024 15266563345 0020101 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin1Encoding.pm
namespace Smalot\PdfParser\Encoding;
/**
* Class ISOLatin1Encoding
*/
class ISOLatin1Encoding extends AbstractEncoding
{
public function getTranslations(): array
{
$encoding =
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'space exclam quotedbl numbersign dollar percent ampersand quoteright '.
'parenleft parenright asterisk plus comma minus period slash zero one '.
'two three four five six seven eight nine colon semicolon less equal '.
'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '.
'Y Z bracketleft backslash bracketright asciicircum underscore '.
'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '.
'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
'.notdef .notdef .notdef .notdef .notdef dotlessi grave acute '.
'circumflex tilde macron breve dotaccent dieresis .notdef ring '.
'cedilla .notdef hungarumlaut ogonek caron space exclamdown cent '.
'sterling currency yen brokenbar section dieresis copyright '.
'ordfeminine guillemotleft logicalnot hyphen registered macron degree '.
'plusminus twosuperior threesuperior acute mu paragraph '.
'periodcentered cedilla onesuperior ordmasculine guillemotright '.
'onequarter onehalf threequarters questiondown Agrave Aacute '.
'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '.
'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '.
'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '.
'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '.
'acircumflex atilde adieresis aring ae ccedilla egrave eacute '.
'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '.
'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '.
'uacute ucircumflex udieresis yacute thorn ydieresis';
return explode(' ', $encoding);
}
}
pdfparser/src/Smalot/PdfParser/Encoding/PDFDocEncoding.php 0000644 00000017122 15266563345 0017435 0 ustar 00
*
* @date 2023-06-28
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
// Source : https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.2.pdf
// Source : https://ia801001.us.archive.org/1/items/pdf1.7/pdf_reference_1-7.pdf
namespace Smalot\PdfParser\Encoding;
/**
* Class PDFDocEncoding
*/
class PDFDocEncoding
{
public static function getCodePage(): array
{
return [
"\x18" => "\u{02d8}", // breve
"\x19" => "\u{02c7}", // caron
"\x1a" => "\u{02c6}", // circumflex
"\x1b" => "\u{02d9}", // dotaccent
"\x1c" => "\u{02dd}", // hungarumlaut
"\x1d" => "\u{02db}", // ogonek
"\x1e" => "\u{02de}", // ring
"\x1f" => "\u{02dc}", // tilde
"\x7f" => '',
"\x80" => "\u{2022}", // bullet
"\x81" => "\u{2020}", // dagger
"\x82" => "\u{2021}", // daggerdbl
"\x83" => "\u{2026}", // ellipsis
"\x84" => "\u{2014}", // emdash
"\x85" => "\u{2013}", // endash
"\x86" => "\u{0192}", // florin
"\x87" => "\u{2044}", // fraction
"\x88" => "\u{2039}", // guilsinglleft
"\x89" => "\u{203a}", // guilsinglright
"\x8a" => "\u{2212}", // minus
"\x8b" => "\u{2030}", // perthousand
"\x8c" => "\u{201e}", // quotedblbase
"\x8d" => "\u{201c}", // quotedblleft
"\x8e" => "\u{201d}", // quotedblright
"\x8f" => "\u{2018}", // quoteleft
"\x90" => "\u{2019}", // quoteright
"\x91" => "\u{201a}", // quotesinglbase
"\x92" => "\u{2122}", // trademark
"\x93" => "\u{fb01}", // fi
"\x94" => "\u{fb02}", // fl
"\x95" => "\u{0141}", // Lslash
"\x96" => "\u{0152}", // OE
"\x97" => "\u{0160}", // Scaron
"\x98" => "\u{0178}", // Ydieresis
"\x99" => "\u{017d}", // Zcaron
"\x9a" => "\u{0131}", // dotlessi
"\x9b" => "\u{0142}", // lslash
"\x9c" => "\u{0153}", // oe
"\x9d" => "\u{0161}", // scaron
"\x9e" => "\u{017e}", // zcaron
"\x9f" => '',
"\xa0" => "\u{20ac}", // Euro
"\xa1" => "\u{00a1}", // exclamdown
"\xa2" => "\u{00a2}", // cent
"\xa3" => "\u{00a3}", // sterling
"\xa4" => "\u{00a4}", // currency
"\xa5" => "\u{00a5}", // yen
"\xa6" => "\u{00a6}", // brokenbar
"\xa7" => "\u{00a7}", // section
"\xa8" => "\u{00a8}", // dieresis
"\xa9" => "\u{00a9}", // copyright
"\xaa" => "\u{00aa}", // ordfeminine
"\xab" => "\u{00ab}", // guillemotleft
"\xac" => "\u{00ac}", // logicalnot
"\xad" => '',
"\xae" => "\u{00ae}", // registered
"\xaf" => "\u{00af}", // macron
"\xb0" => "\u{00b0}", // degree
"\xb1" => "\u{00b1}", // plusminus
"\xb2" => "\u{00b2}", // twosuperior
"\xb3" => "\u{00b3}", // threesuperior
"\xb4" => "\u{00b4}", // acute
"\xb5" => "\u{00b5}", // mu
"\xb6" => "\u{00b6}", // paragraph
"\xb7" => "\u{00b7}", // periodcentered
"\xb8" => "\u{00b8}", // cedilla
"\xb9" => "\u{00b9}", // onesuperior
"\xba" => "\u{00ba}", // ordmasculine
"\xbb" => "\u{00bb}", // guillemotright
"\xbc" => "\u{00bc}", // onequarter
"\xbd" => "\u{00bd}", // onehalf
"\xbe" => "\u{00be}", // threequarters
"\xbf" => "\u{00bf}", // questiondown
"\xc0" => "\u{00c0}", // Agrave
"\xc1" => "\u{00c1}", // Aacute
"\xc2" => "\u{00c2}", // Acircumflex
"\xc3" => "\u{00c3}", // Atilde
"\xc4" => "\u{00c4}", // Adieresis
"\xc5" => "\u{00c5}", // Aring
"\xc6" => "\u{00c6}", // AE
"\xc7" => "\u{00c7}", // Ccedill
"\xc8" => "\u{00c8}", // Egrave
"\xc9" => "\u{00c9}", // Eacute
"\xca" => "\u{00ca}", // Ecircumflex
"\xcb" => "\u{00cb}", // Edieresis
"\xcc" => "\u{00cc}", // Igrave
"\xcd" => "\u{00cd}", // Iacute
"\xce" => "\u{00ce}", // Icircumflex
"\xcf" => "\u{00cf}", // Idieresis
"\xd0" => "\u{00d0}", // Eth
"\xd1" => "\u{00d1}", // Ntilde
"\xd2" => "\u{00d2}", // Ograve
"\xd3" => "\u{00d3}", // Oacute
"\xd4" => "\u{00d4}", // Ocircumflex
"\xd5" => "\u{00d5}", // Otilde
"\xd6" => "\u{00d6}", // Odieresis
"\xd7" => "\u{00d7}", // multiply
"\xd8" => "\u{00d8}", // Oslash
"\xd9" => "\u{00d9}", // Ugrave
"\xda" => "\u{00da}", // Uacute
"\xdb" => "\u{00db}", // Ucircumflex
"\xdc" => "\u{00dc}", // Udieresis
"\xdd" => "\u{00dd}", // Yacute
"\xde" => "\u{00de}", // Thorn
"\xdf" => "\u{00df}", // germandbls
"\xe0" => "\u{00e0}", // agrave
"\xe1" => "\u{00e1}", // aacute
"\xe2" => "\u{00e2}", // acircumflex
"\xe3" => "\u{00e3}", // atilde
"\xe4" => "\u{00e4}", // adieresis
"\xe5" => "\u{00e5}", // aring
"\xe6" => "\u{00e6}", // ae
"\xe7" => "\u{00e7}", // ccedilla
"\xe8" => "\u{00e8}", // egrave
"\xe9" => "\u{00e9}", // eacute
"\xea" => "\u{00ea}", // ecircumflex
"\xeb" => "\u{00eb}", // edieresis
"\xec" => "\u{00ec}", // igrave
"\xed" => "\u{00ed}", // iacute
"\xee" => "\u{00ee}", // icircumflex
"\xef" => "\u{00ef}", // idieresis
"\xf0" => "\u{00f0}", // eth
"\xf1" => "\u{00f1}", // ntilde
"\xf2" => "\u{00f2}", // ograve
"\xf3" => "\u{00f3}", // oacute
"\xf4" => "\u{00f4}", // ocircumflex
"\xf5" => "\u{00f5}", // otilde
"\xf6" => "\u{00f6}", // odieresis
"\xf7" => "\u{00f7}", // divide
"\xf8" => "\u{00f8}", // oslash
"\xf9" => "\u{00f9}", // ugrave
"\xfa" => "\u{00fa}", // uacute
"\xfb" => "\u{00fb}", // ucircumflex
"\xfc" => "\u{00fc}", // udieresis
"\xfd" => "\u{00fd}", // yacute
"\xfe" => "\u{00fe}", // thorn
"\xff" => "\u{00ff}", // ydieresis
];
}
public static function convertPDFDoc2UTF8(string $content): string
{
return strtr($content, static::getCodePage());
}
}
pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php 0000644 00000000210 15266563345 0020127 0 ustar 00
*
* @date 2017-01-03
*
* @license LGPLv3
*
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*/
namespace Smalot\PdfParser;
use Smalot\PdfParser\Encoding\PDFDocEncoding;
use Smalot\PdfParser\Exception\MissingCatalogException;
/**
* Technical references :
* - http://www.mactech.com/articles/mactech/Vol.15/15.09/PDFIntro/index.html
* - http://framework.zend.com/issues/secure/attachment/12512/Pdf.php
* - http://www.php.net/manual/en/ref.pdf.php#74211
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin1Encoding.pm
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin9Encoding.pm
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/StandardEncoding.pm
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/WinAnsiEncoding.pm
*
* Class Document
*/
class Document
{
/**
* @var PDFObject[]
*/
protected $objects = [];
/**
* @var array
*/
protected $dictionary = [];
/**
* @var Header
*/
protected $trailer;
/**
* @var array
*/
protected $metadata = [];
/**
* @var array
*/
protected $details;
public function __construct()
{
$this->trailer = new Header([], $this);
}
public function init()
{
$this->buildDictionary();
$this->buildDetails();
// Propagate init to objects.
foreach ($this->objects as $object) {
$object->getHeader()->init();
$object->init();
}
}
/**
* Build dictionary based on type header field.
*/
protected function buildDictionary()
{
// Build dictionary.
$this->dictionary = [];
foreach ($this->objects as $id => $object) {
// Cache objects by type and subtype
$type = $object->getHeader()->get('Type')->getContent();
if (null != $type) {
if (!isset($this->dictionary[$type])) {
$this->dictionary[$type] = [
'all' => [],
'subtype' => [],
];
}
$this->dictionary[$type]['all'][$id] = $object;
$subtype = $object->getHeader()->get('Subtype')->getContent();
if (null != $subtype) {
if (!isset($this->dictionary[$type]['subtype'][$subtype])) {
$this->dictionary[$type]['subtype'][$subtype] = [];
}
$this->dictionary[$type]['subtype'][$subtype][$id] = $object;
}
}
}
}
/**
* Build details array.
*/
protected function buildDetails()
{
// Build details array.
$details = [];
// Extract document info
if ($this->trailer->has('Info')) {
/** @var PDFObject $info */
$info = $this->trailer->get('Info');
// This could be an ElementMissing object, so we need to check for
// the getHeader method first.
if (null !== $info && method_exists($info, 'getHeader')) {
$details = $info->getHeader()->getDetails();
}
}
// Retrieve the page count
try {
$pages = $this->getPages();
$details['Pages'] = \count($pages);
} catch (\Exception $e) {
$details['Pages'] = 0;
}
// Decode and repair encoded document properties
foreach ($details as $key => $value) {
if (\is_string($value)) {
// If the string is already UTF-8 encoded, that means we only
// need to repair Adobe's ham-fisted insertion of line-feeds
// every ~127 characters, which doesn't seem to be multi-byte
// safe
if (mb_check_encoding($value, 'UTF-8')) {
// Remove literal backslash + line-feed "\\r"
$value = str_replace("\x5c\x0d", '', $value);
// Remove backslash plus bytes written into high part of
// multibyte unicode character
while (preg_match("/\x5c\x5c\xe0([\xb4-\xb8])(.)/", $value, $match)) {
$diff = (\ord($match[1]) - 182) * 64;
$newbyte = PDFDocEncoding::convertPDFDoc2UTF8(\chr(\ord($match[2]) + $diff));
$value = preg_replace("/\x5c\x5c\xe0".$match[1].$match[2].'/', $newbyte, $value);
}
// Remove bytes written into low part of multibyte unicode
// character
while (preg_match("/(.)\x9c\xe0([\xb3-\xb7])/", $value, $match)) {
$diff = \ord($match[2]) - 181;
$newbyte = \chr(\ord($match[1]) + $diff);
$value = preg_replace('/'.$match[1]."\x9c\xe0".$match[2].'/', $newbyte, $value);
}
// Remove this byte string that Adobe occasionally adds
// between two single byte characters in a unicode string
$value = str_replace("\xe5\xb0\x8d", '', $value);
$details[$key] = $value;
} else {
// If the string is just PDFDocEncoding, remove any line-feeds
// and decode the whole thing.
$value = str_replace("\\\r", '', $value);
$details[$key] = PDFDocEncoding::convertPDFDoc2UTF8($value);
}
}
}
$details = array_merge($details, $this->metadata);
$this->details = $details;
}
/**
* Extract XMP Metadata
*/
public function extractXMPMetadata(string $content): void
{
$xml = xml_parser_create();
xml_parser_set_option($xml, \XML_OPTION_SKIP_WHITE, 1);
if (1 === xml_parse_into_struct($xml, $content, $values, $index)) {
/*
* short overview about the following code parts:
*
* The output of xml_parse_into_struct is a single dimensional array (= $values), and the $stack is a last-on,
* first-off array of pointers to positions in $metadata, while iterating through it, that potentially turn the
* results into a more intuitive multi-dimensional array. When an "open" XML tag is encountered,
* we save the current $metadata context in the $stack, then create a child array of $metadata and
* make that the current $metadata context. When a "close" XML tag is encountered, the operations are
* reversed: the most recently added $metadata context from $stack (IOW, the parent of the current
* element) is set as the current $metadata context.
*/
$metadata = [];
$stack = [];
foreach ($values as $val) {
// Standardize to lowercase
$val['tag'] = strtolower($val['tag']);
// Ignore structural x: and rdf: XML elements
if (0 === strpos($val['tag'], 'x:')) {
continue;
} elseif (0 === strpos($val['tag'], 'rdf:') && 'rdf:li' != $val['tag']) {
continue;
}
switch ($val['type']) {
case 'open':
// Create an array of list items
if ('rdf:li' == $val['tag']) {
$metadata[] = [];
// Move up one level in the stack
$stack[\count($stack)] = &$metadata;
$metadata = &$metadata[\count($metadata) - 1];
} else {
// Else create an array of named values
$metadata[$val['tag']] = [];
// Move up one level in the stack
$stack[\count($stack)] = &$metadata;
$metadata = &$metadata[$val['tag']];
}
break;
case 'complete':
if (isset($val['value'])) {
// Assign a value to this list item
if ('rdf:li' == $val['tag']) {
$metadata[] = $val['value'];
// Else assign a value to this property
} else {
$metadata[$val['tag']] = $val['value'];
}
}
break;
case 'close':
// If the value of this property is an array
if (\is_array($metadata)) {
// If the value is a single element array
// where the element is of type string, use
// the value of the first list item as the
// value for this property
if (1 == \count($metadata) && isset($metadata[0]) && \is_string($metadata[0])) {
$metadata = $metadata[0];
} elseif (0 == \count($metadata)) {
// if the value is an empty array, set
// the value of this property to the empty
// string
$metadata = '';
}
}
// Move down one level in the stack
$metadata = &$stack[\count($stack) - 1];
unset($stack[\count($stack) - 1]);
break;
}
}
// Only use this metadata if it's referring to a PDF
if (!isset($metadata['dc:format']) || 'application/pdf' == $metadata['dc:format']) {
// According to the XMP specifications: 'Conflict resolution
// for separate packets that describe the same resource is
// beyond the scope of this document.' - Section 6.1
// Source: https://www.adobe.com/devnet/xmp.html
// Source: https://github.com/adobe/XMP-Toolkit-SDK/blob/main/docs/XMPSpecificationPart1.pdf
// So if there are multiple XMP blocks, just merge the values
// of each found block over top of the existing values
$this->metadata = array_merge($this->metadata, $metadata);
}
}
xml_parser_free($xml);
}
public function getDictionary(): array
{
return $this->dictionary;
}
/**
* @param PDFObject[] $objects
*/
public function setObjects($objects = [])
{
$this->objects = (array) $objects;
$this->init();
}
/**
* @return PDFObject[]
*/
public function getObjects()
{
return $this->objects;
}
/**
* @return PDFObject|Font|Page|Element|null
*/
public function getObjectById(string $id)
{
if (isset($this->objects[$id])) {
return $this->objects[$id];
}
return null;
}
public function hasObjectsByType(string $type, ?string $subtype = null): bool
{
return 0 < \count($this->getObjectsByType($type, $subtype));
}
public function getObjectsByType(string $type, ?string $subtype = null): array
{
if (!isset($this->dictionary[$type])) {
return [];
}
if (null != $subtype) {
if (!isset($this->dictionary[$type]['subtype'][$subtype])) {
return [];
}
return $this->dictionary[$type]['subtype'][$subtype];
}
return $this->dictionary[$type]['all'];
}
/**
* @return Font[]
*/
public function getFonts()
{
return $this->getObjectsByType('Font');
}
public function getFirstFont(): ?Font
{
$fonts = $this->getFonts();
if ([] === $fonts) {
return null;
}
return reset($fonts);
}
/**
* @return Page[]
*
* @throws MissingCatalogException
*/
public function getPages()
{
if ($this->hasObjectsByType('Catalog')) {
// Search for catalog to list pages.
$catalogues = $this->getObjectsByType('Catalog');
$catalogue = reset($catalogues);
/** @var Pages $object */
$object = $catalogue->get('Pages');
if (method_exists($object, 'getPages')) {
return $object->getPages(true);
}
}
if ($this->hasObjectsByType('Pages')) {
// Search for pages to list kids.
$pages = [];
/** @var Pages[] $objects */
$objects = $this->getObjectsByType('Pages');
foreach ($objects as $object) {
$pages = array_merge($pages, $object->getPages(true));
}
return $pages;
}
if ($this->hasObjectsByType('Page')) {
// Search for 'page' (unordered pages).
$pages = $this->getObjectsByType('Page');
return array_values($pages);
}
throw new MissingCatalogException('Missing catalog.');
}
public function getText(?int $pageLimit = null): string
{
$texts = [];
$pages = $this->getPages();
// Only use the first X number of pages if $pageLimit is set and numeric.
if (\is_int($pageLimit) && 0 < $pageLimit) {
$pages = \array_slice($pages, 0, $pageLimit);
}
foreach ($pages as $index => $page) {
/**
* In some cases, the $page variable may be null.
*/
if (null === $page) {
continue;
}
if ($text = trim($page->getText())) {
$texts[] = $text;
}
}
return implode("\n\n", $texts);
}
public function getTrailer(): Header
{
return $this->trailer;
}
public function setTrailer(Header $trailer)
{
$this->trailer = $trailer;
}
public function getDetails(): array
{
return $this->details;
}
}
pdfparser/composer.json 0000644 00000001667 15266563345 0011305 0 ustar 00 {
"name": "smalot/pdfparser",
"description": "Pdf parser library. Can read and extract information from pdf file.",
"keywords": ["PDF", "text", "parser", "parse", "extract"],
"type": "library",
"license": "LGPL-3.0",
"authors": [
{
"name": "Sebastien MALOT",
"email": "sebastien@malot.fr"
}
],
"support": {
"issues": "https://github.com/smalot/pdfparser/issues"
},
"homepage": "https://www.pdfparser.org",
"require": {
"php": ">=7.1",
"symfony/polyfill-mbstring": "^1.18",
"ext-zlib": "*",
"ext-iconv": "*"
},
"autoload": {
"psr-0": {
"Smalot\\PdfParser\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PerformanceTests\\": "tests/Performance/",
"PHPUnitTests\\": "tests/PHPUnit/"
}
},
"config": {
"process-timeout": 1200
}
}
pdfparser/.github/ISSUE_TEMPLATE/incorrect-parsing.md 0000644 00000001107 15266563345 0016246 0 ustar 00 ---
name: Incorrect parsing
about: Use this template if a PDF is not parsed as expected
title: ''
labels: ''
assignees: ''
---
- PHP Version:
- PDFParser Version:
### Description:
### PDF input
### Expected output & actual output
### Code
pdfparser/.github/workflows/performance.yml 0000644 00000001041 15266563345 0015166 0 ustar 00 name: "Performance Tests"
on:
pull_request:
push:
branches:
- "master"
env:
fail-fast: true
jobs:
performance-tests:
name: "Tests for the performance testing the PDF parsing"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Run composer for further autoloading"
run: "composer update"
- name: "Run performance tests"
run: "php tests/Performance/runPerformanceTests.php"
pdfparser/.github/workflows/continuous-integration.yml 0000644 00000012334 15266563345 0017423 0 ustar 00 name: "CI"
on: [push, pull_request]
env:
fail-fast: true
jobs:
phpunit:
name: "PHPUnit (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "none"
tools: composer:v2
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
- name: "Install dev tools"
run: "make install-dev-tools"
- name: "Run PHPUnit"
run: "make run-phpunit"
phpunit-lower-php:
name: "PHPUnit (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.1"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "none"
tools: composer:v2
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
- name: "Install dev tools"
run: "make install-dev-tools"
- name: "Run PHPUnit"
run: "make run-phpunit"
phpunit-coverage:
name: "PHPUnit coverage (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Install PHP with PCOV"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "xdebug"
tools: composer:v2
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
- name: "Install dev tools"
run: "make install-dev-tools"
- name: "Run PHPUnit"
run: make run-phpunit ARGS="-v --coverage-clover coverage/clover.xml"
phpunit-composerv2:
name: "PHPUnit Composer v2 (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "none"
tools: composer:v2
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
- name: "Install dev tools"
run: "make install-dev-tools"
- name: "Run PHPUnit"
run: "make run-phpunit"
alt-autoload:
name: "Tests alternative autoloader (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.1"
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "none"
- name: "Test alt-autoload"
run: "php tests/AltAutoloading/AltAutoloadCheck.php"
phpunit-lowest:
name: "PHPUnit lowest deps (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
matrix:
php:
- "7.3"
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "none"
tools: composer:v2
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
with:
dependency-versions: "lowest"
- name: "Install dev tools"
run: "make install-dev-tools"
- name: "Run PHPUnit"
run: "make run-phpunit"
windows-tests:
name: Windows-Tests with PHP ${{ matrix.php-versions }}
runs-on: windows-latest
strategy:
fail-fast: true
matrix:
php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
ini-values: memory_limit=1G
- name: Install Composer dependencies (root)
run: composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader
- name: Install Composer dependencies (dev-tools)
run: composer update --working-dir=dev-tools
- name: Tests
run: dev-tools/vendor/bin/phpunit -c phpunit-windows.xml --exclude-group linux-only
pdfparser/.github/workflows/coding-standards.yml 0000644 00000001506 15266563345 0016117 0 ustar 00 name: "CS"
on:
pull_request:
push:
branches:
- master
jobs:
coding-standards:
name: "CS Fixer & PHPStan"
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
env:
COMPOSER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
name: "Install PHP"
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "7.4"
tools: "composer:v2"
-
name: Install dependencies with Composer
uses: ramsey/composer-install@v2
-
name: Install dev tools
run: make install-dev-tools
-
name: Run PHP CS Fixer
run: 'make run-php-cs-fixer ARGS="--verbose --dry-run"'
-
name: Run PHPStan
run: 'make run-phpstan ARGS="--no-progress"'
pdfparser/.github/pull_request_template.md 0000644 00000000635 15266563345 0015056 0 ustar 00 # Type of pull request
* [ ] Bug fix (involves code and configuration changes)
* [ ] New feature (involves code and configuration changes)
* [ ] Documentation update
* [ ] Something else
# About
# Checklist for code / configuration changes
See [CONTRIBUTING.md](./../CONTRIBUTING.md) for all essential information about contributing.
pdfparser/alt_autoload.php-dist 0000644 00000004775 15266563345 0012710 0 ustar 00
* @date 2021-02-09
*
* @license LGPLv3
* @url
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see .
*
* --------------------------------------------------------------------------------------
*
* About:
* This file provides an alternative to the Composer-approach.
* Include it into your project and all required files of PDFParser will be loaded automatically.
* Please use it only, if Composer is not available.
*
* How to use:
* 1. include this file as it is OR copy and rename it as you like (and then include it)
* 2. afterwards you can use PDFParser classes
* Done.
*/
/**
* Loads all files found in a given folder.
* Calls itself recursively for all sub folders.
*
* @param string $dir
*/
function requireFilesOfFolder($dir)
{
foreach (new DirectoryIterator($dir) as $fileInfo) {
if (!$fileInfo->isDot()) {
if ($fileInfo->isDir()) {
requireFilesOfFolder($fileInfo->getPathname());
} else {
require_once $fileInfo->getPathname();
}
}
}
}
$rootFolder = __DIR__.'/src/Smalot/PdfParser';
// Manually require files, which can't be loaded automatically that easily.
require_once $rootFolder.'/Element.php';
require_once $rootFolder.'/PDFObject.php';
require_once $rootFolder.'/Font.php';
require_once $rootFolder.'/Page.php';
require_once $rootFolder.'/Element/ElementString.php';
require_once $rootFolder.'/Encoding/AbstractEncoding.php';
/*
* Load the rest of PDFParser files from /src/Smalot/PDFParser
* Dont worry, it wont load files multiple times.
*/
requireFilesOfFolder($rootFolder);
pdfparser/CONTRIBUTING.md 0000644 00000004475 15266563345 0011014 0 ustar 00 # Contributing
**Please read the following text before creating a pull request.**
This project is organized and supported by contributions from the community. Maintenance is done in our limited time.
We welcome any pull request that contributes to PDFParser (code, documentation, ...).
However, we would like to point out that you are initially responsible for a contribution.
If you are new to dealing with pull requests, you can find more information at [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests).
Please don't just throw code at us and expect us to handle it.
Nevertheless, we will support you and give you feedback.
To make life easier for you and us, there is a Continuous Integration (CI) system that carries out software tests and performs a number of other tasks.
The following points describe the relevant preparations/inputs for the CI system.
All checks must be green, otherwise a pull request will not be accepted.
* Please create an [issue](https://github.com/smalot/pdfparser/issues) before starting work on any significant changes.
* We only accept code that is bundled with tests, regardless of whether it is a new function or a bug fix. This strengthens the code base and avoids later regressions. :exclamation: **If you don't know how to write a test, tell us upfront when you open the pull request and we might add them ourselves or discuss other ways**. This [Medium article](https://pguso.medium.com/a-beginners-guide-to-phpunit-writing-and-running-unit-tests-in-php-d0b23b96749f) might be a good starting point. Code changes without tests are very likely to be rejected.
* Fix reported issues with the coding style. We use **PHP-CS-Fixer** for this. See [.php-cs-fixer.php](./.php-cs-fixer.php) for more information about our coding styles. [Developer.md](./doc/Developer.md) contains more information about this topic.
* If you are fixing an **existing error**, refer to it in the introduction text of the pull request. For example, if you created a fix for issue `#1234` write the following Markdown: `fixes #1234`.
* In case you have changed **internal behavior/functionality**, check our documentation to make sure these changes are **correctly documented**: https://github.com/smalot/pdfparser/tree/master/doc
pdfparser/Makefile 0000644 00000000356 15266563345 0010215 0 ustar 00 install-dev-tools:
composer update --working-dir=dev-tools
run-php-cs-fixer:
dev-tools/vendor/bin/php-cs-fixer fix $(ARGS)
run-phpstan:
dev-tools/vendor/bin/phpstan analyze $(ARGS)
run-phpunit:
dev-tools/vendor/bin/phpunit $(ARGS)
pdfparser/doc/Developer.md 0000644 00000002274 15266563345 0011572 0 ustar 00 # Developers
Here you will find information about our development tools and how to use them.
## .editorconfig
Please make sure your editor uses our `.editorconfig` file. It contains rules about our coding styles.
## GitHub Action Workflows
We use GitHub Actions to run our continuous integration as well as other tasks after pushing changes.
You will find related files in `.github/workflows/`.
## Development Tools and Tests
Our test related files are located in `tests` folder.
Tests are written using PHPUnit.
To install (and update) development tools like PHPUnit or PHP-CS-Fixer run:
```bash
make install-dev-tools
```
Development tools are getting installed in `dev-tools/vendor`.
Please check `dev-tools/composer.json` for more information about versions etc.
To run a tool manually, you use `dev-tools/vendor/bin`, for instance:
```bash
dev-tools/vendor/bin/php-cs-fixer fix --verbose --dry-run
```
Below are a few shortcuts to improve your developer experience.
### PHPUnit
To run all tests run:
```bash
make run-phpunit
```
### PHP-CS-Fixer
To check coding styles, run:
```bash
make run-php-cs-fixer
```
### PHPStan
To run a static code analysis, use:
```bash
make run-phpstan
```
pdfparser/doc/Usage.md 0000644 00000014362 15266563345 0010712 0 ustar 00 # Usage
First create a parser object and point it to a file.
```php
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile('document.pdf');
// .. or ...
$pdf = $parser->parseContent(file_get_contents('document.pdf'))
```
## Extract text
A common scenario is to extract text.
```php
// extract text of the whole PDF
$text = $pdf->getText();
// or extract the text of a specific page (in this case the first page)
$text = $pdf->getPages()[0]->getText();
// you can also extract text of a limited amount of pages. here, it will only use the first five pages.
$text = $pdf->getText(5);
```
## Extract text positions
You can extract transformation matrix (indexes 0-3) and x,y position of text objects (indexes 4,5).
```php
$data = $pdf->getPages()[0]->getDataTm();
Array
(
[0] => Array
(
[0] => Array
(
[0] => 0.999429
[1] => 0
[2] => 0
[3] => 1
[4] => 201.96
[5] => 720.68
)
[1] => Document title
)
[1] => Array
(
[0] => Array
(
[0] => 0.999402
[1] => 0
[2] => 0
[3] => 1
[4] => 70.8
[5] => 673.64
)
[1] => Calibri : Lorem ipsum dolor sit amet, consectetur a
)
)
```
When activated via Config setting (`Config::setDataTmFontInfoHasToBeIncluded(true)`) font identifier (index 2) and font size (index 3) are added to dataTm.
```php
// create config
$config = new Smalot\PdfParser\Config();
$config->setDataTmFontInfoHasToBeIncluded(true);
// use config and parse file
$parser = new Smalot\PdfParser\Parser([], $config);
$pdf = $parser->parseFile('document.pdf');
$firstpage = $pdf->getPages()[0];
$data = $firstpage->getDataTm();
Array
(
[0] => Array
(
[0] => Array
(
[0] => 0.999429
[1] => 0
[2] => 0
[3] => 1
[4] => 201.96
[5] => 720.68
)
[1] => Document title
[2] => R7
[3] => 27.96
)
[1] => Array
(
[0] => Array
(
[0] => 0.999402
[1] => 0
[2] => 0
[3] => 1
[4] => 70.8
[5] => 673.64
)
[1] => Calibri : Lorem ipsum dolor sit amet, consectetur a
[2] => R9
[3] => 11.04
)
)
```
Text width should be calculated on text from dataTm to make sure all character widths are available.
In next example we are using data from above.
```php
$font_id = $data[0][2]; //R7
$font = $firstpage->getFont($font_id);
$text = $data[0][1];
$width = $font->calculateTextWidth($text, $missing);
```
## Extract metadata
You can also extract metadata. The available data varies from PDF to PDF.
```php
$metaData = $pdf->getDetails();
Array
(
[Producer] => Adobe Acrobat
[CreatedOn] => 2022-01-28T16:36:11+00:00
[Pages] => 35
...
)
```
If the PDF contains Extensible Metadata Platform (XMP) XML metadata, their values, including the XMP namespace, will be appended to the data returned by `getDetails()`. You can read more about what values and namespaces are commonly used in the [XMP Specifications](https://github.com/adobe/XMP-Toolkit-SDK/tree/main/docs).
```php
Array
(
...
[Pages] => 35
[dc:creator] => My Name
[pdf:producer] => Adobe Acrobat
[dc:title] => My Document Title
...
)
```
Some XMP metadata values may have multiple values, or even named children with their own values. In these cases, the value will be an array. The XMP metadata will follow the structure of the XML so it is possible to have multiple levels of nested values.
```php
Array
(
...
[dc:title] => My Document Title
[xmptpg:maxpagesize] => Array
(
[stdim:w] => 21.500000
[stdim:h] => 6.222222
[stdim:unit] => Inches
)
[xmptpg:platenames] => Array
(
[0] => Cyan
[1] => Magenta
[2] => Yellow
[3] => Black
)
...
)
```
## Read Base64 encoded PDFs
If working with [Base64](https://en.wikipedia.org/wiki/Base64) encoded PDFs, you might want to parse the PDF without saving the file to disk.
This sample will parse the Base64 encoded PDF and extract text from each page.
```php
parseContent(base64_decode($base64PDF));
$text = $pdf->getText();
echo $text;
```
## Calculate text width
Try to calculate text width for given font.
Characters without width are added to `$missing` array in second parameter.
```php
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile('document.pdf');
$fonts = $pdf->getFonts();
// get first font (we assume here there is at least one)
$font = reset($fonts);
// get width
$width = $font->calculateTextWidth('Some text', $missing);
```
## Get pages width and height
Ref: [#472](https://github.com/smalot/pdfparser/issues/427#issuecomment-973416786)
```php
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile('document.pdf');
$pages = $pdf->getPages();
// this variable will contain the height and width of each page of the given PDF
$mediaBox = [];
foreach ($pages as $page) {
$details = $page->getDetails();
// If Mediabox is not set in details of current $page instance, get details from the header instead
if (!isset($details['MediaBox'])) {
$pages = $pdf->getObjectsByType('Pages');
$details = reset($pages)->getHeader()->getDetails();
}
$mediaBox[] = [
'width' => $details['MediaBox'][2],
'height' => $details['MediaBox'][3]
];
}
```
## PDF encryption
This library cannot currently read encrypted PDF files, i.e. those with
a read password. Attempting to do so produces this error:
```
Exception: Secured pdf file are currently not supported.
```
See `setIgnoreEncryption` option in [CustomConfig.md](CustomConfig.md)
for how to override the check in specific cases.
pdfparser/doc/CustomConfig.md 0000644 00000010276 15266563345 0012246 0 ustar 00 # Configuring the behavior of the parser
To change the behavior of the parser, create a `Config` object and pass it to the parser.
In this case, we're setting the font space limit.
Changing this value can be helpful when `getText()` returns a text with too many spaces.
```php
$config = new \Smalot\PdfParser\Config();
$config->setFontSpaceLimit(-60);
$parser = new \Smalot\PdfParser\Parser([], $config);
$pdf = $parser->parseFile('document.pdf');
// output extracted text
// echo $pdf->getText();
```
## Config options overview
The `Config` class has the following options:
| Option | Type | Default | Description |
|--------------------------|---------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `setDecodeMemoryLimit` | Integer | `0` | If parsing fails because of memory exhaustion, you can set a lower memory limit for decoding operations. |
| `setFontSpaceLimit` | Integer | `-50` | Changing font space limit can be helpful when `Parser::getText()` returns a text with too many spaces. |
| `setIgnoreEncryption` | Boolean | `false` | Read PDFs that are not encrypted but have the encryption flag set. This is a temporary workaround, don't rely on it. |
| `setHorizontalOffset` | String | ` ` | When words are broken up or when the structure of a table is not preserved, you may get better results when adapting `setHorizontalOffset`. |
| `setPdfWhitespaces` | String | `\0\t\n\f\r ` | |
| `setPdfWhitespacesRegex` | String | `[\0\t\n\f\r ]` | |
| `setRetainImageContent` | Boolean | `true` | If parsing fails due to memory exhaustion, you can set the value to `false`. This will reduce memory usage, although it will no longer retain image content. |
## option setDecodeMemoryLimit + setRetainImageContent (manage memory usage)
If parsing fails because of memory exhaustion, you can use the following options.
```php
$config = new \Smalot\PdfParser\Config();
// Whether to retain raw image data as content or discard it to save memory
$config->setRetainImageContent(false);
// Memory limit to use when de-compressing files, in bytes
$config->setDecodeMemoryLimit(1000000);
$parser = new \Smalot\PdfParser\Parser([], $config);
```
## option setHorizontalOffset
When words are broken up or when the structure of a table is not preserved, you can use `setHorizontalOffset`.
```php
$config = new \Smalot\PdfParser\Config();
// An empty string can prevent words from breaking up
$config->setHorizontalOffset('');
// A tab can help preserve the structure of your document
$config->setHorizontalOffset("\t");
$parser = new \Smalot\PdfParser\Parser([], $config);
```
## option setFontSpaceLimit
Changing font space limit can be helpful when `getText()` returns a text with too many spaces.
```php
$config = new \Smalot\PdfParser\Config();
$config->setFontSpaceLimit(-60);
$parser = new \Smalot\PdfParser\Parser([], $config);
$pdf = $parser->parseFile('document.pdf');
```
## option setIgnoreEncryption
In some cases PDF files may be internally marked as encrypted even though the content is not encrypted and can be read.
This can be caused by the PDF being created by a tool that does not properly set the encryption flag.
If you are sure that the PDF is not encrypted, you can ignore the encryption flag by setting the `ignoreEncryption` flag to `true` in a custom `Config` instance.
```php
$config = new \Smalot\PdfParser\Config();
$config->setIgnoreEncryption(true);
$parser = new \Smalot\PdfParser\Parser([], $config);
$pdf = $parser->parseFile('document.pdf');
```