Completed
Push — master ( a05270...10fb5b )
by Ben
02:15
created

Document::__construct()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 26
rs 8.8571
cc 1
eloc 17
nc 1
nop 0
1
<?php
2
/**
3
 * BaconPdf
4
 *
5
 * @link      http://github.com/Bacon/BaconPdf For the canonical source repository
6
 * @copyright 2015 Ben 'DASPRiD' Scholzen
7
 * @license   http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
8
 */
9
10
namespace Bacon\Pdf;
11
12
use Bacon\Pdf\Exception\UnexpectedValueException;
13
use Bacon\Pdf\Object\ArrayObject;
14
use Bacon\Pdf\Object\DictionaryObject;
15
use Bacon\Pdf\Object\HexadecimalStringObject;
16
use Bacon\Pdf\Object\IndirectObject;
17
use Bacon\Pdf\Object\NameObject;
18
use Bacon\Pdf\Object\NumericObject;
19
use Bacon\Pdf\Object\ObjectInterface;
20
use Bacon\Pdf\Object\ObjectStorage;
21
use Bacon\Pdf\Object\StreamObject;
22
use Bacon\Pdf\Type\DateType;
23
use Bacon\Pdf\Type\TextStringType;
24
use Bacon\Pdf\Utils\EncryptionUtils;
25
use DateTimeImmutable;
26
use SplFileObject;
27
use SplObjectStorage;
28
29
class Document
30
{
31
    /**
32
     * @var string
33
     */
34
    private $version = '1.7';
35
36
    /**
37
     * @var ObjectStorage
38
     */
39
    private $objects;
40
41
    /**
42
     * @var IndirectObject
43
     */
44
    private $pages;
45
46
    /**
47
     * @var IndirectObject
48
     */
49
    private $info;
50
51
    /**
52
     * @var IndirectObject
53
     */
54
    private $root;
55
56
    /**
57
     * @var IndirectObject|null
58
     */
59
    private $encrypt;
60
61
    /**
62
     * @var string|null
63
     */
64
    private $encryptionKey;
65
66
    /**
67
     * @var HexadecimalStringObject
68
     */
69
    private $firstId;
70
71
    /**
72
     * @var HexadecimalStringObject
73
     */
74
    private $secondId;
75
76
    /**
77
     * Creates a new PDF document.
78
     */
79
    public function __construct()
80
    {
81
        $this->objects = new ObjectStorage();
82
83
        $pages = new DictionaryObject([
84
            'Type' => new NameObject('Pages'),
85
            'Count' => new NumericObject(0),
86
            'Kids' => new ArrayObject(),
87
        ]);
88
        $this->pages = $this->objects->addObject($pages);
89
90
        $info = new DictionaryObject([
91
            'Producer' => new TextStringType('BaconPdf'),
92
            'CreationDate' => new DateType(new DateTimeImmutable()),
93
        ]);
94
        $this->info = $this->objects->addObject($info);
95
96
        $root = new DictionaryObject([
0 ignored issues
show
Documentation introduced by
array('Type' => new \Bac...Pages' => $this->pages) is of type array<string,object<Baco...IndirectObject>|null"}>, but the function expects a array<integer,object<Bac...bject\ObjectInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
97
            'Type' => new NameObject('Catalog'),
98
            'Pages' => $this->pages,
99
        ]);
100
        $this->root = $this->objects->addObject($root);
101
102
        $this->firstId  = new HexadecimalStringObject($this->generateFileIdentifier());
103
        $this->secondId = new HexadecimalStringObject($this->generateFileIdentifier());
104
    }
105
106
    /**
107
     * @return Info
108
     */
109
    public function getInfo()
110
    {
111
        return new Info($this->info->getObject());
0 ignored issues
show
Compatibility introduced by
$this->info->getObject() of type object<Bacon\Pdf\Object\ObjectInterface> is not a sub-type of object<Bacon\Pdf\Object\DictionaryObject>. It seems like you assume a concrete implementation of the interface Bacon\Pdf\Object\ObjectInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
112
    }
113
114
    /**
115
     * Enables encryption for the document.
116
     *
117
     * @param string      $userPassword
118
     * @param string|null $ownerPassword
119
     * @param bool        $use128bit
120
     */
121
    public function enableEncryption($userPassword, $ownerPassword = null, $use128bit = true)
122
    {
123
        if (null === $ownerPassword) {
124
            $ownerPassword = $userPassword;
125
        }
126
127
        if ($use128bit) {
128
            $algorithm = 2;
129
            $revision  = 3;
130
            $keyLength = 128 / 8;
131
        } else {
132
            $algorithm = 1;
133
            $revision  = 2;
134
            $keyLength = 40 / 8;
135
        }
136
137
        $permissions = -1;
138
        $ownerEntry  = EncryptionUtils::computeOwnerEntry($ownerPassword, $userPassword, $revision, $keyLength);
139
140
        if (2 === $revision) {
141
            list($userEntry, $key) = EncryptionUtils::computeUserEntryRev2(
142
                $userPassword,
143
                $ownerEntry,
144
                $revision,
145
                $this->firstId->getValue()
146
            );
147
        } else {
148
            list($userEntry, $key) = EncryptionUtils::computeUserEntryRev3OrGreater(
149
                $userPassword,
150
                $revision,
151
                $keyLength,
152
                $ownerEntry,
153
                $permissions,
154
                $this->firstId->getValue()
155
            );
156
        }
157
158
        $encrypt = new DictionaryObject();
0 ignored issues
show
Bug introduced by
The call to DictionaryObject::__construct() misses a required argument $objects.

This check looks for function calls that miss required arguments.

Loading history...
159
        $encrypt['Filter'] = new NameObject('Standard');
160
        $encrypt['V'] = new NumericObject($algorithm);
161
162
        if (2 === $algorithm) {
163
            $encrypt['Length'] = new NumericObject($keyLength * 8);
164
        }
165
166
        $encrypt['R'] = new NumericObject($revision);
167
        $encrypt['O'] = new HexadecimalStringObject($ownerEntry);
168
        $encrypt['U'] = new HexadecimalStringObject($userEntry);
169
        $encrypt['P'] = new NumericObject($permissions);
170
171
        $this->encrypt = $this->objects->addObject($encrypt);
172
        $this->encryptionKey = $key;
173
    }
174
175
    /**
176
     * Writes the document to a file object.
177
     *
178
     * @param SplFileObject $fileObject
179
     */
180
    public function write(SplFileObject $fileObject)
181
    {
182
        $this->resolveCircularReferences();
183
        $this->writeHeader($fileObject);
184
185
        $objectOffsets = $this->writeObjects($fileObject);
186
        $xrefOffset    = $fileObject->ftell();
187
188
        $this->writeXrefTable($fileObject, $objectOffsets);
189
        $this->writeTrailer($fileObject);
190
        $this->writeFooter($fileObject, $xrefOffset);
191
    }
192
193
    /**
194
     * Writes the document to a file.
195
     *
196
     * @param string $filename
197
     */
198
    public function writeToFile($filename)
199
    {
200
        $this->write(new SplFileObject($filename, 'wb'));
201
    }
202
203
    /**
204
     * Outputs the document directly.
205
     */
206
    public function output()
207
    {
208
        $this->write(new SplFileObject('php://stdout', 'wb'));
209
    }
210
211
    /**
212
     * Returns the document as a string.
213
     *
214
     * @return string
215
     */
216
    public function toString()
217
    {
218
        $fileObject = new SplFileObject('php://memory', 'w+b');
219
        $this->write($fileObject);
220
221
        $fileObject->fseek(0, SEEK_END);
222
        $length = $fileObject->ftell();
223
        $fileObject->fseek(0);
224
225
        return $fileObject->fread($length);
226
    }
227
228
    /**
229
     * Resolves circular references in page objects.
230
     */
231
    private function resolveCircularReferences()
232
    {
233
        $externalReferenceMap = new SplObjectStorage();
234
235
        foreach ($this->objects as $id => $object) {
236
            if (!$object instanceof Page || null === $object->getIndirectReference()) {
237
                continue;
238
            }
239
240
            $indirectObject = $object->getIndirectReference();
241
            $objectStorage  = $indirectObject->getObjectStorage();
242
            $generation     = $indirectObject->getGeneration();
243
244
            if (!$externalReferenceMap->contains($objectStorage)) {
245
                $externalReferenceMap->attach($objectStorage, []);
246
            }
247
248
            if (!array_key_exists($generation, $externalReferenceMap[$objectStorage])) {
249
                $externalReferenceMap[$objectStorage][$generation] = [];
250
            }
251
252
            $externalReferenceMap[$objectStorage][$generation][$id] = new IndirectObject($id, 0, $this->objects);
253
        }
254
255
        $stack = [];
256
        $this->sweepIndirectReferences($externalReferenceMap, $this->root, $stack);
257
    }
258
259
    /**
260
     * @param  SplObjectStorage $externalReferenceMap
261
     * @param  ObjectInterface  $object
262
     * @param  array            $stack
263
     * @return ObjectInterface
264
     */
265
    private function sweepIndirectReferences(
266
        SplObjectStorage $externalReferenceMap,
267
        ObjectInterface $object,
268
        array &$stack
269
    ) {
270
        if ($object instanceof DictionaryObject || $object instanceof ArrayObject) {
271
            foreach ($object as $index => $childObject) {
272
                $childObject = $this->sweepIndirectReferences($externalReferenceMap, $childObject, $stack);
273
274
                if ($childObject instanceof StreamObject) {
275
                    $childObject = $this->objects->addObject($childObject);
276
                }
277
278
                $object[$index] = $childObject;
279
            }
280
281
            return $object;
282
        }
283
284
        if ($object instanceof IndirectObject) {
285
            $objectStorage = $object->getObjectStorage();
286
287
            if ($objectStorage === $this->objects) {
288
                if (in_array($object->getId(), $stack)) {
289
                    return $object;
290
                }
291
292
                $stack[] = $object->getId();
293
                $this->sweepIndirectReferences($externalReferenceMap, $this->objects->getObject($object), $stack);
294
295
                return $object;
296
            }
297
298
            $id         = $object->getId();
299
            $generation = $object->getGeneration();
300
301
            if (!$externalReferenceMap->contains($objectStorage)) {
302
                $externalReferenceMap->attach($objectStorage, []);
303
            }
304
305
            $map = $externalReferenceMap[$objectStorage];
306
307
            if (isset($externalReferenceMap[$objectStorage][$generation][$id])) {
308
                return $map[$generation][$id];
309
            }
310
311
            $newObject         = $objectStorage->getObject($object);
312
            $newId             = $this->objects->reserveSlot();
313
            $newIndirectObject = new IndirectObject($newId, 0, $this->objects);
314
315
            if (!array_key_exists($generation, $externalReferenceMap[$objectStorage])) {
316
                $externalReferenceMap[$objectStorage][$generation] = [];
317
            }
318
319
            $externalReferenceMap[$objectStorage][$generation][$id] = $newIndirectObject;
320
            $sweepedIndirectObject = $this->sweepIndirectReferences($externalReferenceMap, $newObject, $stack);
321
322
            if (!$sweepedIndirectObject instanceof IndirectObject) {
323
                throw new UnexpectedValueException(sprintf(
324
                    'Expected sweeped object to be of type %s, got %s',
325
                    IndirectObject::class,
326
                    get_class($sweepedIndirectObject)
327
                ));
328
            }
329
330
            $this->objects->fillSlot($sweepedIndirectObject);
331
332
            return $newIndirectObject;
333
        }
334
335
        return $object;
336
    }
337
338
    /**
339
     * Writes the file header.
340
     *
341
     * @param SplFileObject $fileObject
342
     */
343
    private function writeHeader(SplFileObject $fileObject)
344
    {
345
        $fileObject->fwrite(sprintf("%PDF-%s\n", $this->version));
346
        $fileObject->fwrite("%\xff\xff\xff\xff\n");
347
    }
348
349
    /**
350
     * Writes all objects.
351
     *
352
     * @param  SplFileObject $fileObject
353
     * @return array
354
     */
355
    private function writeObjects(SplFileObject $fileObject)
356
    {
357
        $objectOffsets = [];
358
359
        foreach ($this->objects as $object) {
360
            $indirectObject = $this->objects->getIndirectObject($object);
361
            $id = $indirectObject->getId();
362
            $objectOffsets[] = $fileObject->ftell();
363
            $fileObject->fwrite(sprintf("%d 0 obj\n", $id));
364
365
            $key = null;
366
367
            if (null !== $this->encrypt && $id !== $this->encrypt->getId()) {
368
                $key = substr(hex2bin(md5(
369
                    $this->encryptionKey
370
                    . substr(pack('V', $id), 0, 3)
371
                    . substr(pack('V', $indirectObject->getGeneration()), 0, 2)
372
                )), 0, min(16, strlen($this->encryptionKey) + 5));
373
            }
374
375
            $object->writeToStream($fileObject, $key);
376
            $fileObject->fwrite("\nendobj\n");
377
        }
378
379
        return $objectOffsets;
380
    }
381
382
    /**
383
     * Writes the xref table.
384
     *
385
     * @param SplFileObject $fileObject
386
     * @param array         $objectOffsets
387
     */
388
    private function writeXrefTable(SplFileObject $fileObject, array $objectOffsets)
389
    {
390
        $fileObject->fwrite("xref\n");
391
        $fileObject->fwrite(sprintf("0 %d\n", count($this->objects) + 1));
392
        $fileObject->fwrite(sprintf("%010d %05d f \n", 0, 65535));
393
394
        foreach ($objectOffsets as $offset) {
395
            $fileObject->fwrite(sprintf("%010d %05d n \n", $offset, 0));
396
        }
397
    }
398
399
    /**
400
     * Writes the trailer.
401
     *
402
     * @param SplFileObject $fileObject
403
     */
404
    private function writeTrailer(SplFileObject $fileObject)
405
    {
406
        $fileObject->fwrite("trailer\n");
407
        $trailer = new DictionaryObject();
0 ignored issues
show
Bug introduced by
The call to DictionaryObject::__construct() misses a required argument $objects.

This check looks for function calls that miss required arguments.

Loading history...
408
        $trailer['Size'] = new NumericObject(count($this->objects) + 1);
409
        $trailer['Root'] = $this->root;
410
        $trailer['Info'] = $this->info;
411
        $trailer['Id'] = new ArrayObject([$this->firstId, $this->secondId]);
412
        $trailer->writeToStream($fileObject, null);
413
    }
414
415
    /**
416
     * Writes the file footer.
417
     *
418
     * @param SplFileObject $fileObject
419
     * @param int           $xrefOffset
420
     */
421
    private function writeFooter(SplFileObject $fileObject, $xrefOffset)
422
    {
423
        $fileObject->fwrite("\n");
424
        $fileObject->fwrite("startxref\n");
425
        $fileObject->fwrite(sprintf("%d\n", $xrefOffset));
426
        $fileObject->fwrite("%%%EOF\n");
427
    }
428
429
    /**
430
     * Computes a file identifier.
431
     *
432
     * @return string
433
     */
434
    private function generateFileIdentifier()
435
    {
436
        return hex2bin(md5(microtime()));
437
    }
438
}
439