From a9f5e98ba9308c134eb5c5a72745086009bbfe88 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 19 Jun 2025 17:23:56 +0100 Subject: [PATCH] Drawings: Added class to extract drawio data from png files --- app/Exceptions/DrawioPngReaderException.php | 7 ++ app/Uploads/DrawioPngReader.php | 122 ++++++++++++++++++++ tests/Uploads/DrawioPngReaderTest.php | 56 +++++++++ tests/test-data/test.drawio.png | Bin 0 -> 1583 bytes 4 files changed, 185 insertions(+) create mode 100644 app/Exceptions/DrawioPngReaderException.php create mode 100644 app/Uploads/DrawioPngReader.php create mode 100644 tests/Uploads/DrawioPngReaderTest.php create mode 100644 tests/test-data/test.drawio.png 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 0000000000000000000000000000000000000000..5af067468f2dc64abfca7049bca518b27fdfb70c GIT binary patch literal 1583 zcmbtUTSydP6kejlEEU4aBBUM$ENq#bSvLi#_TsK}GZV4QKXBFJlr6utDea}qF;aE{_G(;jp0}cCk=)V zFdQc&5>8PT;<}(Fof% zCXRWvcJzBRJEocm=|Vz^U=b-0Sj>{ERN(yyzY}}MZ zlq6j0Xke@ua$+GE!^7SOS%qZZmJi-tsHaa5Lt zw;(w#*_mCQMj}`hY-g$@DG4G@6aJ#1kT%-Q)G>y5MKdcl+fHi6j})udb{}aT*qVRm z&UHhd+v)!a7dG~LxlBvj5f96C9m!_np?06WT`55^Y0KfjSnHZJvi1TSCc!`nmgnD$2Yl#sm#U8UgU=FcQm`#J zkHt|6R{IM;43uDf0LkWj${)_d_i-j1@E&Z~_wmx5vS3fuVE6sVo07oIC)ZyML=K;K z?%o}n&Lc0O@OUXBmdo*Vh_ed>Aq^OuU(12679$W^X1Z{N#}3`aqzBiy_< JaMRaw`WJ}Q#nAu& literal 0 HcmV?d00001