From b34104a1f838a5d810d0e19d6a286f5f5cc0c89f Mon Sep 17 00:00:00 2001 From: Haryto <37344295+Haryto@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:01:46 +0300 Subject: [PATCH] Bake diagram XML in the PNG image meta data (#83) * Bake diagram XML in the PNG image meta data * Change drawio default template content There was a problem with usage of "". In that case "bs:drawio" tag was parsed before "{{{diagramName}}}" parameter was passed. So it looked like diagram is named "{{{diagramName}}}". Now when using "{{#tag:drawio ... }}} diagram name is passed before parsing tag, so now it works correctly. --------- Co-authored-by: akulbii --- src/Composer/ConfluenceComposer.php | 27 ++++++ src/Composer/_defaultpages/Template/Drawio | 5 +- src/Utility/DrawIOFileHandler.php | 61 ++++++++++++++ .../DrawIOFileHandlerTest.php | 73 +++++++++++++++++ .../DrawIOFileHandler/data/diagram.drawio | 77 ++++++++++++++++++ .../DrawIOFileHandler/data/diagram.drawio.png | Bin 0 -> 12087 bytes 6 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/Utility/DrawIOFileHandler.php create mode 100644 tests/phpunit/Utility/DrawIOFileHandler/DrawIOFileHandlerTest.php create mode 100644 tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio create mode 100644 tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio.png diff --git a/src/Composer/ConfluenceComposer.php b/src/Composer/ConfluenceComposer.php index 40b5bdb..542ebf5 100644 --- a/src/Composer/ConfluenceComposer.php +++ b/src/Composer/ConfluenceComposer.php @@ -7,6 +7,7 @@ use HalloWelt\MediaWiki\Lib\Migration\DataBuckets; use HalloWelt\MediaWiki\Lib\Migration\IOutputAwareInterface; use HalloWelt\MediaWiki\Lib\Migration\Workspace; +use HalloWelt\MigrateConfluence\Utility\DrawIOFileHandler; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Symfony\Component\Console\Output\Output; @@ -95,10 +96,36 @@ public function buildXML( Builder $builder ) { $attachments = $pageAttachmentsMap[$pageTitle]; foreach ( $attachments as $attachment ) { $this->output->writeln( "Attachment: $attachment" ); + + $drawIoFileHandler = new DrawIOFileHandler(); + + // We do not need DrawIO data files in our wiki, just PNG image + if ( $drawIoFileHandler->isDrawIODataFile( $attachment ) ) { + continue; + } + if ( isset( $filesMap[$attachment] ) ) { $filePath = $filesMap[$attachment][0]; $attachmentContent = file_get_contents( $filePath ); + if ( $drawIoFileHandler->isDrawIOImage( $attachment ) ) { + // Find associated with DrawIO PNG image diagram XML + // If image has "image1.drawio.png" name, + // Then diagram XML will be stored in the "image1.drawio" file + $diagramFileName = substr( $attachment, 0, -4 ); + + if ( isset( $filesMap[$diagramFileName] ) ) { + $diagramContent = file_get_contents( $filesMap[$diagramFileName][0] ); + + // Need to bake DrawIO diagram XML into the PNG image + $attachmentContent = $drawIoFileHandler->bakeDiagramDataIntoImage( + $attachmentContent, $diagramContent + ); + } else { + $this->output->writeln( "No DrawIO diagram XML was found for image '$attachment'" ); + } + } + $this->workspace->saveUploadFile( $attachment, $attachmentContent ); $this->customBuckets->addData( 'title-uploads', $pageTitle, $attachment ); } else { diff --git a/src/Composer/_defaultpages/Template/Drawio b/src/Composer/_defaultpages/Template/Drawio index f9b5e2c..7578845 100644 --- a/src/Composer/_defaultpages/Template/Drawio +++ b/src/Composer/_defaultpages/Template/Drawio @@ -1 +1,4 @@ - \ No newline at end of file +{{#tag:drawio +| +|filename={{{diagramName}}} +}} \ No newline at end of file diff --git a/src/Utility/DrawIOFileHandler.php b/src/Utility/DrawIOFileHandler.php new file mode 100644 index 0000000..df53661 --- /dev/null +++ b/src/Utility/DrawIOFileHandler.php @@ -0,0 +1,61 @@ +assertTrue( $drawIoFileHandler->isDrawIODataFile( 'diagram.drawio' ) ); + $this->assertTrue( $drawIoFileHandler->isDrawIODataFile( 'diagram.drawio.tmp' ) ); + + $this->assertFalse( $drawIoFileHandler->isDrawIODataFile( 'diagram.drawio.png' ) ); + } + + /** + * @covers \HalloWelt\MigrateConfluence\Utility\DrawIOFileHandler::isDrawIOImage + */ + public function testIsDrawIOImage() { + $drawIoFileHandler = new DrawIOFileHandler(); + + $this->assertFalse( $drawIoFileHandler->isDrawIOImage( 'diagram.drawio' ) ); + $this->assertFalse( $drawIoFileHandler->isDrawIOImage( 'diagram.drawio.tmp' ) ); + + $this->assertTrue( $drawIoFileHandler->isDrawIOImage( 'diagram.drawio.png' ) ); + } + + /** + * @covers \HalloWelt\MigrateConfluence\Utility\DrawIOFileHandler::bakeDiagramDataIntoImage + */ + public function testBakeDiagramDataIntoImage() { + $drawIoFileHandler = new DrawIOFileHandler(); + + $diagramXml = file_get_contents( __DIR__ . '/data/diagram.drawio' ); + $imageContent = file_get_contents( __DIR__ . '/data/diagram.drawio.png' ); + + // Get expected diagram XML + $matches = []; + preg_match( '#(.*?)#s', $diagramXml, $matches ); + + $expectedDiagramXML = trim( $matches[0] ); + + // Bake diagram XML into PNG image meta data + $imageContent = $drawIoFileHandler->bakeDiagramDataIntoImage( $imageContent, $diagramXml ); + + // Extract and check diagram XML from the PNG + // Extraction is done with the same algorithm how it is done in the wiki + $encodedXML = preg_replace( + '#^.*?tEXt(.*?)IDAT.*?$#s', + '$1', + $imageContent + ); + $encodedXML = preg_replace( '/[[:^print:]]/', '', $encodedXML ); + $partiallyDecodedXML = urldecode( $encodedXML ); + + // Get actual diagram XML after extraction from PNG + $matches = []; + preg_match( '#(.*?)#s', $partiallyDecodedXML, $matches ); + + $actualDiagramXML = trim( $matches[0] ); + + $this->assertEquals( $expectedDiagramXML, $actualDiagramXML ); + } +} diff --git a/tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio b/tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio new file mode 100644 index 0000000..15f2a67 --- /dev/null +++ b/tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio.png b/tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..0568db31a9930390f670c788bf604d99c250fe28 GIT binary patch literal 12087 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