From: Dan Brown Date: Thu, 19 Jun 2025 16:23:56 +0000 (+0100) Subject: Drawings: Added class to extract drawio data from png files X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/refs/pull/5666/head?ds=inline Drawings: Added class to extract drawio data from png files --- diff --git a/app/Exceptions/DrawioPngReaderException.php b/app/Exceptions/DrawioPngReaderException.php new file mode 100644 index 000000000..15d1da75f --- /dev/null +++ b/app/Exceptions/DrawioPngReaderException.php @@ -0,0 +1,7 @@ +fileStream, 8); + $pngSignature = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"; + if ($signature !== $pngSignature) { + throw new DrawioPngReaderException('File does not appear to be a valid PNG file'); + } + + $offset = 8; + $searching = true; + + while ($searching) { + fseek($this->fileStream, $offset); + + $lengthBytes = $this->readData(4); + $chunkTypeBytes = $this->readData(4); + $length = unpack('Nvalue', $lengthBytes)['value']; + + if ($chunkTypeBytes === 'tEXt') { + fseek($this->fileStream, $offset + 8); + $data = $this->readData($length); + $crc = $this->readData(4); + $drawingData = $this->readTextForDrawing($data); + if ($drawingData !== null) { + $crcResult = $this->calculateCrc($chunkTypeBytes . $data); + if ($crc !== $crcResult) { + throw new DrawioPngReaderException('Drawing data withing PNG file appears to be corrupted'); + } + return $drawingData; + } + } else if ($chunkTypeBytes === 'IEND') { + $searching = false; + } + + $offset += 12 + $length; // 12 = length + type + crc bytes + } + + throw new DrawioPngReaderException('Unable to find drawing data within PNG file'); + } + + protected function readTextForDrawing(string $data): ?string + { + // Check the keyword is mxfile to ensure we're getting the right data + if (!str_starts_with($data, "mxfile\u{0}")) { + return null; + } + + // Extract & cleanup the drawing text + $drawingText = substr($data, 7); + return urldecode($drawingText); + } + + protected function readData(int $length): string + { + $bytes = fread($this->fileStream, $length); + if ($bytes === false || strlen($bytes) < $length) { + throw new DrawioPngReaderException('Unable to find drawing data within PNG file'); + } + return $bytes; + } + + protected function getCrcTable(): array + { + $table = []; + + for ($n = 0; $n < 256; $n++) { + $c = $n; + for ($k = 0; $k < 8; $k++) { + if ($c & 1) { + $c = 0xedb88320 ^ ($c >> 1); + } else { + $c = $c >> 1; + } + } + $table[$n] = $c; + } + + return $table; + } + + /** + * Calculate a CRC for the given bytes following: + * https://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix + */ + protected function calculateCrc(string $bytes): string + { + $table = $this->getCrcTable(); + + $length = strlen($bytes); + $c = 0xffffffff; + + for ($n = 0; $n < $length; $n++) { + $tableIndex = ($c ^ ord($bytes[$n])) & 0xff; + $c = $table[$tableIndex] ^ ($c >> 8); + } + + return pack('N', $c ^ 0xffffffff); + } +} diff --git a/tests/Uploads/DrawioPngReaderTest.php b/tests/Uploads/DrawioPngReaderTest.php new file mode 100644 index 000000000..49e7531c4 --- /dev/null +++ b/tests/Uploads/DrawioPngReaderTest.php @@ -0,0 +1,56 @@ +files->testFilePath('test.drawio.png'); + $stream = fopen($file, 'r'); + + $reader = new DrawioPngReader($stream); + $drawing = $reader->extractDrawing(); + + $this->assertStringStartsWith('assertStringEndsWith("\n", $drawing); + } + + public function test_extract_drawing_with_non_drawing_image_throws_exception() + { + $file = $this->files->testFilePath('test-image.png'); + $stream = fopen($file, 'r'); + $reader = new DrawioPngReader($stream); + + $exception = null; + try { + $drawing = $reader->extractDrawing(); + } catch (\Exception $e) { + $exception = $e; + } + + $this->assertInstanceOf(DrawioPngReaderException::class, $exception); + $this->assertEquals($exception->getMessage(), 'Unable to find drawing data within PNG file'); + } + + public function test_extract_drawing_with_non_png_image_throws_exception() + { + $file = $this->files->testFilePath('test-image.jpg'); + $stream = fopen($file, 'r'); + $reader = new DrawioPngReader($stream); + + $exception = null; + try { + $drawing = $reader->extractDrawing(); + } catch (\Exception $e) { + $exception = $e; + } + + $this->assertInstanceOf(DrawioPngReaderException::class, $exception); + $this->assertEquals($exception->getMessage(), 'File does not appear to be a valid PNG file'); + } +} diff --git a/tests/test-data/test.drawio.png b/tests/test-data/test.drawio.png new file mode 100644 index 000000000..5af067468 Binary files /dev/null and b/tests/test-data/test.drawio.png differ