]> BookStack Code Mirror - bookstack/blob - app/Uploads/DrawioPngReader.php
Drawings: Added class to extract drawio data from png files
[bookstack] / app / Uploads / DrawioPngReader.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use BookStack\Exceptions\DrawioPngReaderException;
6
7 /**
8  * Reads the PNG file format: https://www.w3.org/TR/2003/REC-PNG-20031110/
9  * So that it can extract embedded drawing data for alternative use.
10  */
11 class DrawioPngReader
12 {
13     /**
14      * @param resource $fileStream
15      */
16     public function __construct(
17         protected $fileStream
18     ) {
19     }
20
21     /**
22      * @throws DrawioPngReaderException
23      */
24     public function extractDrawing(): string
25     {
26         $signature = fread($this->fileStream, 8);
27         $pngSignature = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
28         if ($signature !== $pngSignature) {
29             throw new DrawioPngReaderException('File does not appear to be a valid PNG file');
30         }
31
32         $offset = 8;
33         $searching = true;
34
35         while ($searching) {
36             fseek($this->fileStream, $offset);
37
38             $lengthBytes = $this->readData(4);
39             $chunkTypeBytes = $this->readData(4);
40             $length = unpack('Nvalue', $lengthBytes)['value'];
41
42             if ($chunkTypeBytes === 'tEXt') {
43                 fseek($this->fileStream, $offset + 8);
44                 $data = $this->readData($length);
45                 $crc = $this->readData(4);
46                 $drawingData = $this->readTextForDrawing($data);
47                 if ($drawingData !== null) {
48                     $crcResult = $this->calculateCrc($chunkTypeBytes . $data);
49                     if ($crc !== $crcResult) {
50                         throw new DrawioPngReaderException('Drawing data withing PNG file appears to be corrupted');
51                     }
52                     return $drawingData;
53                 }
54             } else if ($chunkTypeBytes === 'IEND') {
55                 $searching = false;
56             }
57
58             $offset += 12 + $length; // 12 = length + type + crc bytes
59         }
60
61         throw new DrawioPngReaderException('Unable to find drawing data within PNG file');
62     }
63
64     protected function readTextForDrawing(string $data): ?string
65     {
66         // Check the keyword is mxfile to ensure we're getting the right data
67         if (!str_starts_with($data, "mxfile\u{0}")) {
68             return null;
69         }
70
71         // Extract & cleanup the drawing text
72         $drawingText = substr($data, 7);
73         return urldecode($drawingText);
74     }
75
76     protected function readData(int $length): string
77     {
78         $bytes = fread($this->fileStream, $length);
79         if ($bytes === false || strlen($bytes) < $length) {
80             throw new DrawioPngReaderException('Unable to find drawing data within PNG file');
81         }
82         return $bytes;
83     }
84
85     protected function getCrcTable(): array
86     {
87         $table = [];
88
89         for ($n = 0; $n < 256; $n++) {
90             $c = $n;
91             for ($k = 0; $k < 8; $k++) {
92                 if ($c & 1) {
93                     $c = 0xedb88320 ^ ($c >> 1);
94                 } else {
95                     $c = $c >> 1;
96                 }
97             }
98             $table[$n] = $c;
99         }
100
101         return $table;
102     }
103
104     /**
105      * Calculate a CRC for the given bytes following:
106      * https://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix
107      */
108     protected function calculateCrc(string $bytes): string
109     {
110         $table = $this->getCrcTable();
111
112         $length = strlen($bytes);
113         $c = 0xffffffff;
114
115         for ($n = 0; $n < $length; $n++) {
116             $tableIndex = ($c ^ ord($bytes[$n])) & 0xff;
117             $c = $table[$tableIndex] ^ ($c >> 8);
118         }
119
120         return pack('N', $c ^ 0xffffffff);
121     }
122 }