Skip to content

SVG Image and Drawing Support #3452

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions app/Entities/Tools/ExportFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,16 @@ protected function replaceIframesWithLinks(string $html): string
*/
protected function containHtml(string $htmlContent): string
{
$imageTagsOutput = [];
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace embed tags with images
$htmlContent = preg_replace("/<embed (.*?)>/i", '<img $1>', $htmlContent);

// Replace image src with base64 encoded image strings
// Replace image & embed src attributes with base64 encoded data strings
$imageTagsOutput = [];
preg_match_all("/<img .*?src=['\"](.*?)['\"].*?>/i", $htmlContent, $imageTagsOutput);
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$srcString = $imageTagsOutput[1][$index];
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
Expand All @@ -232,14 +234,13 @@ protected function containHtml(string $htmlContent): string
}
}

// Replace any relative links with full system URL
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);

// Replace image src with base64 encoded image strings
preg_match_all("/<a .*href=['\"](.*?)['\"].*?>/i", $htmlContent, $linksOutput);
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
$srcString = $linksOutput[1][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
Expand All @@ -248,7 +249,6 @@ protected function containHtml(string $htmlContent): string
}
}

// Replace any relative links with system domain
return $htmlContent;
}

Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,6 @@ protected function logActivity(string $type, $detail = ''): void
*/
protected function getImageValidationRules(): array
{
return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)];
return ['image_extension', 'mimes:jpeg,png,gif,webp,svg', 'max:' . (config('app.upload_limit') * 1000)];
}
}
5 changes: 4 additions & 1 deletion app/Http/Controllers/Images/DrawioImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ public function getAsBase64($id)
return $this->jsonError('Image data could not be found');
}

$isSvg = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
$uriPrefix = $isSvg ? 'data:image/svg+xml;base64,' : 'data:image/png;base64,';

return response()->json([
'content' => base64_encode($imageData),
'content' => $uriPrefix . base64_encode($imageData),
]);
}
}
3 changes: 2 additions & 1 deletion app/Uploads/ImageRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ public function saveNewFromData(string $imageName, string $imageData, string $ty
*/
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
{
$name = 'Drawing-' . user()->id . '-' . time() . '.png';
$isSvg = strpos($base64Uri, 'data:image/svg+xml;') === 0;
$name = 'Drawing-' . user()->id . '-' . time() . ($isSvg ? '.svg' : '.png');

return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
}
Expand Down
14 changes: 11 additions & 3 deletions app/Uploads/ImageService.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ImageService
protected $image;
protected $fileSystem;

protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];

/**
* ImageService constructor.
Expand Down Expand Up @@ -230,6 +230,14 @@ protected function isGif(Image $image): bool
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
}

/**
* Check if the given image is an SVG image file.
*/
protected function isSvg(Image $image): bool
{
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
}

/**
* Check if the given image and image data is apng.
*/
Expand All @@ -255,8 +263,8 @@ protected function isApngData(Image $image, string &$imageData): bool
*/
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
{
// Do not resize GIF images where we're not cropping
if ($keepRatio && $this->isGif($image)) {
// Do not resize GIF images where we're not cropping or SVG images.
if (($keepRatio && $this->isGif($image)) || $this->isSvg($image)) {
return $this->getPublicUrl($image->path);
}

Expand Down
89 changes: 27 additions & 62 deletions resources/js/components/markdown-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ class MarkdownEditor {
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});

this.display = this.elem.querySelector('.markdown-display');

this.displayStylesLoaded = false;
this.input = this.elem.querySelector('textarea');
this.display = this.$refs.display;
this.input = this.$refs.input;

this.cm = null;
this.Code = null;
Expand All @@ -32,41 +30,31 @@ class MarkdownEditor {
});

this.onMarkdownScroll = this.onMarkdownScroll.bind(this);

const displayLoad = () => {
this.displayDoc = this.display.contentDocument;
this.init(cmLoadPromise);
};

if (this.display.contentDocument.readyState === 'complete') {
displayLoad();
} else {
this.display.addEventListener('load', displayLoad.bind(this));
}

window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
markdownIt: this.markdown,
displayEl: this.display,
codeMirrorInstance: this.cm,
});

this.init(cmLoadPromise);
}

init(cmLoadPromise) {

let lastClick = 0;

// Prevent markdown display link click redirect
this.displayDoc.addEventListener('click', event => {
let isDblClick = Date.now() - lastClick < 300;
this.display.addEventListener('click', event => {
const isDblClick = Date.now() - lastClick < 300;

let link = event.target.closest('a');
const link = event.target.closest('a');
if (link !== null) {
event.preventDefault();
window.open(link.getAttribute('href'));
return;
}

let drawing = event.target.closest('[drawio-diagram]');
const drawing = event.target.closest('[drawio-diagram]');
if (drawing !== null && isDblClick) {
this.actionEditDrawing(drawing);
return;
Expand All @@ -77,10 +65,10 @@ class MarkdownEditor {

// Button actions
this.elem.addEventListener('click', event => {
let button = event.target.closest('button[data-action]');
const button = event.target.closest('button[data-action]');
if (button === null) return;

let action = button.getAttribute('data-action');
const action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
Expand Down Expand Up @@ -132,35 +120,11 @@ class MarkdownEditor {
window.$events.emit('editor-markdown-change', content);

// Set body content
this.displayDoc.body.className = 'page-content';
this.displayDoc.body.innerHTML = html;

// Copy styles from page head and set custom styles for editor
this.loadStylesIntoDisplay();
}

loadStylesIntoDisplay() {
if (this.displayStylesLoaded) return;
this.displayDoc.documentElement.classList.add('markdown-editor-display');
// Set display to be dark mode if parent is

if (document.documentElement.classList.contains('dark-mode')) {
this.displayDoc.documentElement.style.backgroundColor = '#222';
this.displayDoc.documentElement.classList.add('dark-mode');
}

this.displayDoc.head.innerHTML = '';
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
for (let style of styles) {
const copy = style.cloneNode(true);
this.displayDoc.head.appendChild(copy);
}

this.displayStylesLoaded = true;
this.display.innerHTML = html;
}

onMarkdownScroll(lineCount) {
const elems = this.displayDoc.body.children;
const elems = this.display.children;
if (elems.length <= lineCount) return;

const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
Expand Down Expand Up @@ -317,7 +281,7 @@ class MarkdownEditor {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let newLineContent = lineContent;
let newLineContent;

if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
Expand All @@ -333,9 +297,9 @@ class MarkdownEditor {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);

let newSelection = selection;
let newSelection;
let frontDiff = 0;
let endDiff = 0;
let endDiff;

if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
newSelection = selection.slice(start.length, selection.length - end.length);
Expand Down Expand Up @@ -445,10 +409,10 @@ class MarkdownEditor {

DrawIO.show(url,() => {
return Promise.resolve('');
}, (pngData) => {
}, (drawingData) => {

const data = {
image: pngData,
image: drawingData,
uploaded_to: Number(this.pageId),
};

Expand All @@ -462,7 +426,7 @@ class MarkdownEditor {
}

insertDrawing(image, originalCursor) {
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
const newText = DrawIO.buildDrawingContentHtml(image);
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
Expand All @@ -480,21 +444,22 @@ class MarkdownEditor {

DrawIO.show(drawioUrl, () => {
return DrawIO.load(drawingId);
}, (pngData) => {
}, (drawingData) => {

let data = {
image: pngData,
image: drawingData,
uploaded_to: Number(this.pageId),
};

window.$http.post("/images/drawio", data).then(resp => {
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
let newContent = this.cm.getValue().split('\n').map(line => {
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
return newText;
}
return line;
const image = resp.data;
const newText = DrawIO.buildDrawingContentHtml(image);

const newContent = this.cm.getValue().split('\n').map(line => {
const isDrawing = line.includes(`drawio-diagram="${drawingId}"`);
return isDrawing ? newText : line;
}).join('\n');

this.cm.setValue(newContent);
this.cm.setCursor(cursorPos);
this.cm.focus();
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/page-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class PageEditor {
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
this.changelogInput = this.$refs.changelogInput;
this.changelogDisplay = this.$refs.changelogDisplay;
this.changeEditorButtons = this.$manyRefs.changeEditor;
this.changeEditorButtons = this.$manyRefs.changeEditor || [];
this.switchDialogContainer = this.$refs.switchDialog;

// Translations
Expand Down
20 changes: 17 additions & 3 deletions resources/js/services/drawio.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function drawEventExport(message) {
}

function drawEventSave(message) {
drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
drawPostMessage({action: 'export', format: 'xmlsvg', xml: message.xml, spin: 'Updating drawing'});
}

function drawEventInit() {
Expand Down Expand Up @@ -96,7 +96,21 @@ async function upload(imageData, pageUploadedToId) {
*/
async function load(drawingId) {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
return resp.data.content;
}

export default {show, close, upload, load};

function buildDrawingContentHtml(drawing) {
const isSvg = drawing.url.split('.').pop().toLowerCase() === 'svg';
const image = `<img src="${drawing.url}">`;
const embed = `<embed src="${drawing.url}" type="image/svg+xml">`;
return `<div drawio-diagram="${drawing.id}">${isSvg ? embed : image}</div>`
}

function buildDrawingContentNode(drawing) {
const div = document.createElement('div');
div.innerHTML = buildDrawingContentHtml(drawing);
return div.children[0];
}

export default {show, close, upload, load, buildDrawingContentHtml, buildDrawingContentNode};
Loading