Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
/**
4
 * This file is part of FPDI
5
 *
6
 * @package   setasign\Fpdi
7
 * @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
8
 * @license   http://opensource.org/licenses/mit-license The MIT License
9
 */
10
 
11
namespace setasign\Fpdi\PdfReader;
12
 
13
use setasign\Fpdi\FpdiException;
14
use setasign\Fpdi\GraphicsState;
15
use setasign\Fpdi\Math\Vector;
16
use setasign\Fpdi\PdfParser\Filter\FilterException;
17
use setasign\Fpdi\PdfParser\PdfParser;
18
use setasign\Fpdi\PdfParser\PdfParserException;
19
use setasign\Fpdi\PdfParser\Type\PdfArray;
20
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
21
use setasign\Fpdi\PdfParser\Type\PdfHexString;
22
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
23
use setasign\Fpdi\PdfParser\Type\PdfName;
24
use setasign\Fpdi\PdfParser\Type\PdfNull;
25
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
26
use setasign\Fpdi\PdfParser\Type\PdfStream;
27
use setasign\Fpdi\PdfParser\Type\PdfString;
28
use setasign\Fpdi\PdfParser\Type\PdfType;
29
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
30
use setasign\Fpdi\PdfReader\DataStructure\Rectangle;
31
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
32
 
33
/**
34
 * Class representing a page of a PDF document
35
 */
36
class Page
37
{
38
    /**
39
     * @var PdfIndirectObject
40
     */
41
    protected $pageObject;
42
 
43
    /**
44
     * @var PdfDictionary
45
     */
46
    protected $pageDictionary;
47
 
48
    /**
49
     * @var PdfParser
50
     */
51
    protected $parser;
52
 
53
    /**
54
     * Inherited attributes
55
     *
56
     * @var null|array
57
     */
58
    protected $inheritedAttributes;
59
 
60
    /**
61
     * Page constructor.
62
     *
63
     * @param PdfIndirectObject $page
64
     * @param PdfParser $parser
65
     */
66
    public function __construct(PdfIndirectObject $page, PdfParser $parser)
67
    {
68
        $this->pageObject = $page;
69
        $this->parser = $parser;
70
    }
71
 
72
    /**
73
     * Get the indirect object of this page.
74
     *
75
     * @return PdfIndirectObject
76
     */
77
    public function getPageObject()
78
    {
79
        return $this->pageObject;
80
    }
81
 
82
    /**
83
     * Get the dictionary of this page.
84
     *
85
     * @return PdfDictionary
86
     * @throws PdfParserException
87
     * @throws PdfTypeException
88
     * @throws CrossReferenceException
89
     */
90
    public function getPageDictionary()
91
    {
92
        if ($this->pageDictionary === null) {
93
            $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser));
94
        }
95
 
96
        return $this->pageDictionary;
97
    }
98
 
99
    /**
100
     * Get a page attribute.
101
     *
102
     * @param string $name
103
     * @param bool $inherited
104
     * @return PdfType|null
105
     * @throws PdfParserException
106
     * @throws PdfTypeException
107
     * @throws CrossReferenceException
108
     */
109
    public function getAttribute($name, $inherited = true)
110
    {
111
        $dict = $this->getPageDictionary();
112
 
113
        if (isset($dict->value[$name])) {
114
            return $dict->value[$name];
115
        }
116
 
117
        $inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate'];
118
        if ($inherited && \in_array($name, $inheritedKeys, true)) {
119
            if ($this->inheritedAttributes === null) {
120
                $this->inheritedAttributes = [];
121
                $inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) {
122
                    return !isset($dict->value[$key]);
123
                });
124
 
125
                if (\count($inheritedKeys) > 0) {
126
                    $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser);
127
                    while ($parentDict instanceof PdfDictionary) {
128
                        foreach ($inheritedKeys as $index => $key) {
129
                            if (isset($parentDict->value[$key])) {
130
                                $this->inheritedAttributes[$key] = $parentDict->value[$key];
131
                                unset($inheritedKeys[$index]);
132
                            }
133
                        }
134
 
135
                        /** @noinspection NotOptimalIfConditionsInspection */
136
                        if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) {
137
                            $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser);
138
                        } else {
139
                            break;
140
                        }
141
                    }
142
                }
143
            }
144
 
145
            if (isset($this->inheritedAttributes[$name])) {
146
                return $this->inheritedAttributes[$name];
147
            }
148
        }
149
 
150
        return null;
151
    }
152
 
153
    /**
154
     * Get the rotation value.
155
     *
156
     * @return int
157
     * @throws PdfParserException
158
     * @throws PdfTypeException
159
     * @throws CrossReferenceException
160
     */
161
    public function getRotation()
162
    {
163
        $rotation = $this->getAttribute('Rotate');
164
        if ($rotation === null) {
165
            return 0;
166
        }
167
 
168
        $rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360;
169
 
170
        if ($rotation < 0) {
171
            $rotation += 360;
172
        }
173
 
174
        return $rotation;
175
    }
176
 
177
    /**
178
     * Get a boundary of this page.
179
     *
180
     * @param string $box
181
     * @param bool $fallback
182
     * @return bool|Rectangle
183
     * @throws PdfParserException
184
     * @throws PdfTypeException
185
     * @throws CrossReferenceException
186
     * @see PageBoundaries
187
     */
188
    public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true)
189
    {
190
        $value = $this->getAttribute($box);
191
 
192
        if ($value !== null) {
193
            return Rectangle::byPdfArray($value, $this->parser);
194
        }
195
 
196
        if ($fallback === false) {
197
            return false;
198
        }
199
 
200
        switch ($box) {
201
            case PageBoundaries::BLEED_BOX:
202
            case PageBoundaries::TRIM_BOX:
203
            case PageBoundaries::ART_BOX:
204
                return $this->getBoundary(PageBoundaries::CROP_BOX, true);
205
            case PageBoundaries::CROP_BOX:
206
                return $this->getBoundary(PageBoundaries::MEDIA_BOX, true);
207
        }
208
 
209
        return false;
210
    }
211
 
212
    /**
213
     * Get the width and height of this page.
214
     *
215
     * @param string $box
216
     * @param bool $fallback
217
     * @return array|bool
218
     * @throws PdfParserException
219
     * @throws PdfTypeException
220
     * @throws CrossReferenceException
221
     */
222
    public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true)
223
    {
224
        $boundary = $this->getBoundary($box, $fallback);
225
        if ($boundary === false) {
226
            return false;
227
        }
228
 
229
        $rotation = $this->getRotation();
230
        $interchange = ($rotation / 90) % 2;
231
 
232
        return [
233
            $interchange ? $boundary->getHeight() : $boundary->getWidth(),
234
            $interchange ? $boundary->getWidth() : $boundary->getHeight()
235
        ];
236
    }
237
 
238
    /**
239
     * Get the raw content stream.
240
     *
241
     * @return string
242
     * @throws PdfReaderException
243
     * @throws PdfTypeException
244
     * @throws FilterException
245
     * @throws PdfParserException
246
     */
247
    public function getContentStream()
248
    {
249
        $dict = $this->getPageDictionary();
250
        $contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser);
251
        if ($contents instanceof PdfNull) {
252
            return '';
253
        }
254
 
255
        if ($contents instanceof PdfArray) {
256
            $result = [];
257
            foreach ($contents->value as $content) {
258
                $content = PdfType::resolve($content, $this->parser);
259
                if (!($content instanceof PdfStream)) {
260
                    continue;
261
                }
262
                $result[] = $content->getUnfilteredStream();
263
            }
264
 
265
            return \implode("\n", $result);
266
        }
267
 
268
        if ($contents instanceof PdfStream) {
269
            return $contents->getUnfilteredStream();
270
        }
271
 
272
        throw new PdfReaderException(
273
            'Array or stream expected.',
274
            PdfReaderException::UNEXPECTED_DATA_TYPE
275
        );
276
    }
277
 
278
    /**
279
     * Get information of all external links on this page.
280
     *
281
     * All coordinates are normalized in view to rotation and translation of the boundary-box, so that their
282
     * origin is lower-left.
283
     *
284
     * The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding.
285
     *
286
     * @return array
287
     */
288
    public function getExternalLinks($box = PageBoundaries::CROP_BOX)
289
    {
290
        try {
291
            $dict = $this->getPageDictionary();
292
            $annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser);
293
        } catch (FpdiException $e) {
294
            return [];
295
        }
296
 
297
        if (!$annotations instanceof PdfArray) {
298
            return [];
299
        }
300
 
301
        $links = [];
302
 
303
        foreach ($annotations->value as $entry) {
304
            try {
305
                $annotation = PdfType::resolve($entry, $this->parser);
306
 
307
                $value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser);
308
                if (!$value instanceof PdfName || $value->value !== 'Link') {
309
                    continue;
310
                }
311
 
312
                $dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser);
313
                if (!$dest instanceof PdfNull) {
314
                    continue;
315
                }
316
 
317
                $action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser);
318
                if (!$action instanceof PdfDictionary) {
319
                    continue;
320
                }
321
 
322
                $actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser);
323
                if (!$actionType instanceof PdfName || $actionType->value !== 'URI') {
324
                    continue;
325
                }
326
 
327
                $uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser);
328
                if ($uri instanceof PdfString) {
329
                    $uriValue = PdfString::unescape($uri->value);
330
                } elseif ($uri instanceof PdfHexString) {
331
                    $uriValue = \hex2bin($uri->value);
332
                } else {
333
                    continue;
334
                }
335
 
336
                $rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser);
337
                if (!$rect instanceof PdfArray || count($rect->value) !== 4) {
338
                    continue;
339
                }
340
 
341
                $rect = Rectangle::byPdfArray($rect, $this->parser);
342
                if ($rect->getWidth() === 0 || $rect->getHeight() === 0) {
343
                    continue;
344
                }
345
 
346
                $bbox = $this->getBoundary($box);
347
                $rotation = $this->getRotation();
348
 
349
                $gs = new GraphicsState();
350
                $gs->translate(-$bbox->getLlx(), -$bbox->getLly());
351
                $gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation);
352
 
353
                switch ($rotation) {
354
                    case 90:
355
                        $gs->translate(-$bbox->getWidth(), 0);
356
                        break;
357
                    case 180:
358
                        $gs->translate(-$bbox->getWidth(), -$bbox->getHeight());
359
                        break;
360
                    case 270:
361
                        $gs->translate(0, -$bbox->getHeight());
362
                        break;
363
                }
364
 
365
                $normalizedRect = Rectangle::byVectors(
366
                    $gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())),
367
                    $gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry()))
368
                );
369
 
370
                $quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser);
371
                $normalizedQuadPoints = [];
372
                if ($quadPoints instanceof PdfArray) {
373
                    $quadPointsCount = count($quadPoints->value);
374
                    if ($quadPointsCount % 8 === 0) {
375
                        for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) {
376
                            $x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser));
377
                            $y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser));
378
 
379
                            $v = $gs->toUserSpace(new Vector($x->value, $y->value));
380
                            $normalizedQuadPoints[] = $v->getX();
381
                            $normalizedQuadPoints[] = $v->getY();
382
                        }
383
                    }
384
                }
385
 
386
                // we remove unsupported/unneeded values here
387
                unset(
388
                    $annotation->value['P'],
389
                    $annotation->value['NM'],
390
                    $annotation->value['AP'],
391
                    $annotation->value['AS'],
392
                    $annotation->value['Type'],
393
                    $annotation->value['Subtype'],
394
                    $annotation->value['Rect'],
395
                    $annotation->value['A'],
396
                    $annotation->value['QuadPoints'],
397
                    $annotation->value['Rotate'],
398
                    $annotation->value['M'],
399
                    $annotation->value['StructParent'],
400
                    $annotation->value['OC']
401
                );
402
 
403
                // ...and flatten the PDF object to eliminate any indirect references.
404
                // Indirect references are a problem when writing the output in FPDF
405
                // because FPDF uses pre-calculated object numbers while FPDI creates
406
                // them at runtime.
407
                $annotation = PdfType::flatten($annotation, $this->parser);
408
 
409
                $links[] = [
410
                    'rect' => $normalizedRect,
411
                    'quadPoints' => $normalizedQuadPoints,
412
                    'uri' => $uriValue,
413
                    'pdfObject' => $annotation
414
                ];
415
            } catch (FpdiException $e) {
416
                continue;
417
            }
418
        }
419
 
420
        return $links;
421
    }
422
}