mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-08-09 10:22:51 +03:00
Drawings: Added class to extract drawio data from png files
This commit is contained in:
7
app/Exceptions/DrawioPngReaderException.php
Normal file
7
app/Exceptions/DrawioPngReaderException.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
class DrawioPngReaderException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
122
app/Uploads/DrawioPngReader.php
Normal file
122
app/Uploads/DrawioPngReader.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Uploads;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\DrawioPngReaderException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the PNG file format: https://www.w3.org/TR/2003/REC-PNG-20031110/
|
||||||
|
* So that it can extract embedded drawing data for alternative use.
|
||||||
|
*/
|
||||||
|
class DrawioPngReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param resource $fileStream
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected $fileStream
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws DrawioPngReaderException
|
||||||
|
*/
|
||||||
|
public function extractDrawing(): string
|
||||||
|
{
|
||||||
|
$signature = fread($this->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);
|
||||||
|
}
|
||||||
|
}
|
56
tests/Uploads/DrawioPngReaderTest.php
Normal file
56
tests/Uploads/DrawioPngReaderTest.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Uploads;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\DrawioPngReaderException;
|
||||||
|
use BookStack\Uploads\DrawioPngReader;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DrawioPngReaderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_exact_drawing()
|
||||||
|
{
|
||||||
|
$file = $this->files->testFilePath('test.drawio.png');
|
||||||
|
$stream = fopen($file, 'r');
|
||||||
|
|
||||||
|
$reader = new DrawioPngReader($stream);
|
||||||
|
$drawing = $reader->extractDrawing();
|
||||||
|
|
||||||
|
$this->assertStringStartsWith('<mxfile ', $drawing);
|
||||||
|
$this->assertStringEndsWith("</mxfile>\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');
|
||||||
|
}
|
||||||
|
}
|
BIN
tests/test-data/test.drawio.png
Normal file
BIN
tests/test-data/test.drawio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user