Skip to content

Commit

Permalink
Bake diagram XML in the PNG image meta data (#83)
Browse files Browse the repository at this point in the history
* Bake diagram XML in the PNG image meta data

* Change drawio default template content

There was a problem with usage of "<bs:drawio {{{diagramName}}} />".
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 <[email protected]>
  • Loading branch information
Haryto and akulbii authored Aug 30, 2023
1 parent 93acee0 commit b34104a
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 1 deletion.
27 changes: 27 additions & 0 deletions src/Composer/ConfluenceComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion src/Composer/_defaultpages/Template/Drawio
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
<bs:drawio filename="{{{diagramName}}}" />
{{#tag:drawio
|
|filename={{{diagramName}}}
}}
61 changes: 61 additions & 0 deletions src/Utility/DrawIOFileHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace HalloWelt\MigrateConfluence\Utility;

class DrawIOFileHandler {

/**
* Checks by file extension if that's associated with DrawIO temporary file.
* That could be either ".drawio" file or ".drawio.tmp" file
*
* @param string $fileName
* @return bool
*/
public function isDrawIODataFile( string $fileName ): bool {
return (
preg_match( '#\.drawio.tmp$#', $fileName ) ||
preg_match( '#\.drawio$#', $fileName )
);
}

/**
* Checks by file extension if that's associated with DrawIO PNG image
*
* @param string $fileName
* @return bool
*/
public function isDrawIOImage( string $fileName ): bool {
return preg_match( '#\.drawio.png$#', $fileName );
}

/**
* Encodes and bakes DrawIO diagram XML into PNG image "tEXt" data chunk
*
* @param string $imageContent
* @param string $diagramXml
* @return string
*/
public function bakeDiagramDataIntoImage( string $imageContent, string $diagramXml ): string {
// "urlencode" does not suit us here because it encodes spaces with "+" symbol
// That breaks diagram data processing by DrawIO editor
// We need to encode spaces as "%20", so "rawurlencode" is used
// See https://www.php.net/manual/en/function.urlencode.php
$diagramXmlEncoded = rawurlencode( $diagramXml );

$keyword = 'mxfile';

$chunkData = $keyword . "\0" . $diagramXmlEncoded;

$crc = pack( 'N', crc32( 'tEXt' . $chunkData ) );

// Create the tEXt chunk
$tEXtChunk = pack( 'N', strlen( $chunkData ) ) . 'tEXt' . $chunkData . $crc;

$IDATChunkPos = strpos( $imageContent, 'IDAT', 8 );

// Add the tEXt chunk to the image content
$imageContent = substr_replace( $imageContent, $tEXtChunk, $IDATChunkPos - 4, 0 );

return $imageContent;
}
}
73 changes: 73 additions & 0 deletions tests/phpunit/Utility/DrawIOFileHandler/DrawIOFileHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace HalloWelt\MigrateConfluence\Tests\Utility\DrawIOFileHandler;

use HalloWelt\MigrateConfluence\Utility\DrawIOFileHandler;
use PHPUnit\Framework\TestCase;

/**
* @covers \HalloWelt\MigrateConfluence\Utility\DrawIOFileHandler
*/
class DrawIOFileHandlerTest extends TestCase {

/**
* @covers \HalloWelt\MigrateConfluence\Utility\DrawIOFileHandler::isDrawIODataFile
*/
public function testIsDrawIODataFile() {
$drawIoFileHandler = new DrawIOFileHandler();

$this->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( '#<mxfile.*?>(.*?)</mxfile>#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( '#<mxfile.*?>(.*?)</mxfile>#s', $partiallyDecodedXML, $matches );

$actualDiagramXML = trim( $matches[0] );

$this->assertEquals( $expectedDiagramXML, $actualDiagramXML );
}
}
77 changes: 77 additions & 0 deletions tests/phpunit/Utility/DrawIOFileHandler/data/diagram.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<mxfile host="ac.draw.io" modified="2023-08-09T08:35:34.679Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" etag="JMqJX_jkzzPyvO56GqTc" version="21.6.9" type="embed">
<diagram id="prtHgNgQTEPvFCAcTncT" name="Page-1">
<mxGraphModel dx="1434" dy="784" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="dNxyNK7c78bLwvsdeMH5-19" value="test" style="swimlane;html=1;childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=0;startSize=20;horizontalStack=0;" parent="1" vertex="1">
<mxGeometry x="120" y="120" width="450" height="360" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-27" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=none;endFill=0;" parent="dNxyNK7c78bLwvsdeMH5-19" source="dNxyNK7c78bLwvsdeMH5-24" target="dNxyNK7c78bLwvsdeMH5-26" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" parent="dNxyNK7c78bLwvsdeMH5-19" source="dNxyNK7c78bLwvsdeMH5-28" target="dNxyNK7c78bLwvsdeMH5-30" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" parent="dNxyNK7c78bLwvsdeMH5-19" source="dNxyNK7c78bLwvsdeMH5-28" target="dNxyNK7c78bLwvsdeMH5-34" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-38" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" parent="dNxyNK7c78bLwvsdeMH5-19" source="dNxyNK7c78bLwvsdeMH5-26" target="dNxyNK7c78bLwvsdeMH5-36" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="340" />
<mxPoint x="400" y="340" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-20" value="Lane 1" style="swimlane;html=1;startSize=20;horizontal=0;" parent="dNxyNK7c78bLwvsdeMH5-19" vertex="1">
<mxGeometry x="20" width="430" height="120" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-25" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="dNxyNK7c78bLwvsdeMH5-20" source="dNxyNK7c78bLwvsdeMH5-23" target="dNxyNK7c78bLwvsdeMH5-24" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-23" value="" style="ellipse;whiteSpace=wrap;html=1;" parent="dNxyNK7c78bLwvsdeMH5-20" vertex="1">
<mxGeometry x="40" y="40" width="40" height="40" as="geometry" />
</mxCell>
<UserObject label="test" link="https://geht.atlassian.net/wiki/spaces/GENERAL/pages/256147457/Pallet+truck+2" id="dNxyNK7c78bLwvsdeMH5-24">
<mxCell style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Helvetica;fontSize=12;fontColor=#000000;align=center;" parent="dNxyNK7c78bLwvsdeMH5-20" vertex="1">
<mxGeometry x="120" y="30" width="80" height="60" as="geometry" />
</mxCell>
</UserObject>
<mxCell id="dNxyNK7c78bLwvsdeMH5-33" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" parent="dNxyNK7c78bLwvsdeMH5-20" source="dNxyNK7c78bLwvsdeMH5-30" target="dNxyNK7c78bLwvsdeMH5-32" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-30" value="test" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Helvetica;fontSize=12;fontColor=#000000;align=center;" parent="dNxyNK7c78bLwvsdeMH5-20" vertex="1">
<mxGeometry x="240" y="30" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-32" value="" style="ellipse;whiteSpace=wrap;html=1;" parent="dNxyNK7c78bLwvsdeMH5-20" vertex="1">
<mxGeometry x="360" y="40" width="40" height="40" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-21" value="Lane 2" style="swimlane;html=1;startSize=20;horizontal=0;" parent="dNxyNK7c78bLwvsdeMH5-19" vertex="1">
<mxGeometry x="20" y="120" width="430" height="120" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-29" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" parent="dNxyNK7c78bLwvsdeMH5-21" source="dNxyNK7c78bLwvsdeMH5-26" target="dNxyNK7c78bLwvsdeMH5-28" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-26" value="test" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Helvetica;fontSize=12;fontColor=#000000;align=center;" parent="dNxyNK7c78bLwvsdeMH5-21" vertex="1">
<mxGeometry x="120" y="30" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-28" value="" style="rhombus;whiteSpace=wrap;html=1;fontFamily=Helvetica;fontSize=12;fontColor=#000000;align=center;" parent="dNxyNK7c78bLwvsdeMH5-21" vertex="1">
<mxGeometry x="260" y="40" width="40" height="40" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-22" value="Lane 3" style="swimlane;html=1;startSize=20;horizontal=0;" parent="dNxyNK7c78bLwvsdeMH5-19" vertex="1">
<mxGeometry x="20" y="240" width="430" height="120" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-37" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" parent="dNxyNK7c78bLwvsdeMH5-22" source="dNxyNK7c78bLwvsdeMH5-34" target="dNxyNK7c78bLwvsdeMH5-36" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-34" value="test" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Helvetica;fontSize=12;fontColor=#000000;align=center;" parent="dNxyNK7c78bLwvsdeMH5-22" vertex="1">
<mxGeometry x="240" y="20" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="dNxyNK7c78bLwvsdeMH5-36" value="" style="rhombus;whiteSpace=wrap;html=1;fontFamily=Helvetica;fontSize=12;fontColor=#000000;align=center;" parent="dNxyNK7c78bLwvsdeMH5-22" vertex="1">
<mxGeometry x="360" y="30" width="40" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b34104a

Please sign in to comment.