Completed
Push — master ( 655053...db47c8 )
by Ben
02:17
created

Document::writeObjects()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 26
rs 8.5806
cc 4
eloc 17
nc 3
nop 1
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\Object\ArrayObject;
13
use Bacon\Pdf\Object\DictionaryObject;
14
use Bacon\Pdf\Object\HexadecimalStringObject;
15
use Bacon\Pdf\Object\IndirectObject;
16
use Bacon\Pdf\Object\LiteralStringObject;
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\Utils\EncryptionUtils;
23
use Doctrine\Instantiator\Exception\UnexpectedValueException;
24
use SplFileObject;
25
use SplObjectStorage;
26
27
class Document
28
{
29
    const HEADER = '%PDF-1.7';
30
31
    /**
32
     * @var ObjectStorage
33
     */
34
    private $objects;
35
36
    /**
37
     * @var IndirectObject
38
     */
39
    private $pages;
40
41
    /**
42
     * @var IndirectObject
43
     */
44
    private $info;
45
46
    /**
47
     * @var IndirectObject
48
     */
49
    private $root;
50
51
    /**
52
     * @var IndirectObject|null
53
     */
54
    private $encrypt;
55
56
    /**
57
     * @var string<null
58
     */
59
    private $encryptionKey;
60
61
    /**
62
     * @var HexadecimalStringObject
63
     */
64
    private $firstId;
65
66
    /**
67
     * @var HexadecimalStringObject
68
     */
69
    private $secondId;
70
71
    /**
72
     * Creates a new PDF document.
73
     */
74
    public function __construct()
75
    {
76
        $this->objects = new ObjectStorage();
77
78
        $pages = new DictionaryObject();
79
        $pages['Type'] = new NameObject('Pages');
80
        $pages['Count'] = new NumericObject(0);
81
        $pages['Kids'] = new ArrayObject();
82
        $this->pages = $this->objects->addObject($pages);
83
84
        $info = new DictionaryObject();
85
        $info['Producer'] = new LiteralStringObject('BaconPdf');
86
        $this->info = $this->objects->addObject($info);
87
88
        $root = new DictionaryObject();
89
        $root['Type'] = new NameObject('Catalog');
90
        $root['Pages'] = $this->pages;
91
        $this->root = $this->objects->addObject($root);
92
93
        $this->firstId  = new HexadecimalStringObject($this->generateFileIdentifier());
94
        $this->secondId = new HexadecimalStringObject($this->generateFileIdentifier());
95
    }
96
97
    /**
98
     * Enables encryption for the document.
99
     *
100
     * @param string      $userPassword
101
     * @param string|null $ownerPassword
102
     * @param bool        $use128bit
103
     */
104
    public function enableEncryption($userPassword, $ownerPassword = null, $use128bit = true)
105
    {
106
        if (null === $ownerPassword) {
107
            $ownerPassword = $userPassword;
108
        }
109
110
        if ($use128bit) {
111
            $algorithm = 2;
112
            $revision  = 3;
113
            $keyLength = 128 / 8;
114
        } else {
115
            $algorithm = 1;
116
            $revision  = 2;
117
            $keyLength = 40 / 8;
118
        }
119
120
        $permissions = -1;
121
        $ownerEntry  = EncryptionUtils::computeOwnerEntry($ownerPassword, $userPassword, $revision, $keyLength);
122
123
        if (2 === $revision) {
124
            list($userEntry, $key) = EncryptionUtils::computeUserEntryRev2(
125
                $userPassword,
126
                $ownerEntry,
127
                $revision,
128
                $this->firstId->getValue()
0 ignored issues
show
Bug introduced by
The method getValue() does not seem to exist on object<Bacon\Pdf\Object\HexadecimalStringObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
129
            );
130
        } else {
131
            list($userEntry, $key) = EncryptionUtils::computeUserEntryRev3OrGreater(
132
                $userPassword,
133
                $revision,
134
                $keyLength,
135
                $ownerEntry,
136
                $permissions,
137
                $this->firstId->getValue()
0 ignored issues
show
Bug introduced by
The method getValue() does not seem to exist on object<Bacon\Pdf\Object\HexadecimalStringObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

This check looks for function calls that miss required arguments.

Loading history...
396
    }
397
398
    /**
399
     * Writes the file footer.
400
     *
401
     * @param SplFileObject $fileObject
402
     * @param int           $xrefOffset
403
     */
404
    private function writeFooter(SplFileObject $fileObject, $xrefOffset)
405
    {
406
        $fileObject->fwrite("\n");
407
        $fileObject->fwrite("startxref\n");
408
        $fileObject->fwrite(sprintf("%d\n", $xrefOffset));
409
        $fileObject->fwrite("%%%EOF\n");
410
    }
411
412
    /**
413
     * Computes a file identifier.
414
     *
415
     * @return string
416
     */
417
    private function generateFileIdentifier()
418
    {
419
        return hex2bin(md5(microtime()));
420
    }
421
}
422