Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Brown
a9f5e98ba9 Drawings: Added class to extract drawio data from png files 2025-06-19 17:23:56 +01:00
14 changed files with 190 additions and 129 deletions

6
.gitignore vendored
View File

@@ -8,10 +8,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
/public/css
/public/js
/public/bower
/public/build/
/public/favicon.ico

View File

@@ -0,0 +1,7 @@
<?php
namespace BookStack\Exceptions;
class DrawioPngReaderException extends \Exception
{
}

View 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);
}
}

View File

@@ -1 +1 @@
ee5c1c242e5dedc76472e61f135a98ef6499c0fb886f0daad525498a847e4d67
22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1

33
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

32
public/dist/code.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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');
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +1 @@
v25.05.1
v25.02-dev