Enabled system-storage of drawings made via draw.io

This commit is contained in:
Dan Brown
2017-12-30 15:24:03 +00:00
parent 0dc1f0b07f
commit 920964a561
5 changed files with 159 additions and 34 deletions

View File

@ -96,6 +96,7 @@ class ImageController extends Controller
* @param string $type * @param string $type
* @param Request $request * @param Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/ */
public function uploadByType($type, Request $request) public function uploadByType($type, Request $request)
{ {
@ -103,11 +104,12 @@ class ImageController extends Controller
$this->validate($request, [ $this->validate($request, [
'file' => 'is_image' 'file' => 'is_image'
]); ]);
// TODO - Restrict & validate types
$imageUpload = $request->file('file'); $imageUpload = $request->file('file');
try { try {
$uploadedTo = $request->filled('uploaded_to') ? $request->get('uploaded_to') : 0; $uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) { } catch (ImageUploadException $e) {
return response($e->getMessage(), 500); return response($e->getMessage(), 500);
@ -116,6 +118,47 @@ class ImageController extends Controller
return response()->json($image); return response()->json($image);
} }
/**
* Upload a drawing to the system.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function uploadDrawing(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getBase64Image($id)
{
$image = $this->imageRepo->getById($id);
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
/** /**
* Generate a sized thumbnail for an image. * Generate a sized thumbnail for an image.
* @param $id * @param $id
@ -123,6 +166,8 @@ class ImageController extends Controller
* @param $height * @param $height
* @param $crop * @param $crop
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/ */
public function getThumbnail($id, $width, $height, $crop) public function getThumbnail($id, $width, $height, $crop)
{ {
@ -137,6 +182,8 @@ class ImageController extends Controller
* @param integer $imageId * @param integer $imageId
* @param Request $request * @param Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/ */
public function update($imageId, Request $request) public function update($imageId, Request $request)
{ {

View File

@ -132,6 +132,8 @@ class ImageRepo
* @param string $type * @param string $type
* @param int $uploadedTo * @param int $uploadedTo
* @return Image * @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/ */
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0) public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{ {
@ -140,11 +142,27 @@ class ImageRepo
return $image; return $image;
} }
/**
* Save a drawing the the database;
* @param string $base64Uri
* @param int $uploadedTo
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function saveDrawing(string $base64Uri, int $uploadedTo)
{
$name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png';
$image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawing', $uploadedTo);
return $image;
}
/** /**
* Update the details of an image via an array of properties. * Update the details of an image via an array of properties.
* @param Image $image * @param Image $image
* @param array $updateDetails * @param array $updateDetails
* @return Image * @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/ */
public function updateImageDetails(Image $image, $updateDetails) public function updateImageDetails(Image $image, $updateDetails)
{ {
@ -170,6 +188,8 @@ class ImageRepo
/** /**
* Load thumbnails onto an image object. * Load thumbnails onto an image object.
* @param Image $image * @param Image $image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/ */
private function loadThumbs(Image $image) private function loadThumbs(Image $image)
{ {
@ -189,6 +209,8 @@ class ImageRepo
* @param int $height * @param int $height
* @param bool $keepRatio * @param bool $keepRatio
* @return string * @return string
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/ */
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{ {
@ -200,5 +222,19 @@ class ImageRepo
} }
} }
/**
* Get the raw image data from an Image.
* @param Image $image
* @return null|string
*/
public function getImageData(Image $image)
{
try {
return $this->imageService->getImageData($image);
} catch (\Exception $exception) {
return null;
}
}
} }

View File

@ -46,6 +46,24 @@ class ImageService extends UploadService
return $this->saveNew($imageName, $imageData, $type, $uploadedTo); return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
} }
/**
* Save a new image from a uri-encoded base64 string of data.
* @param string $base64Uri
* @param string $name
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
throw new ImageUploadException("Invalid base64 image data provided");
}
$data = base64_decode($splitData[1]);
return $this->saveNew($name, $data, $type, $uploadedTo);
}
/** /**
* Gets an image from url and saves it to the database. * Gets an image from url and saves it to the database.
@ -183,6 +201,19 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath); return $this->getPublicUrl($thumbFilePath);
} }
/**
* Get the raw data content from an image.
* @param Image $image
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getImageData(Image $image)
{
$imagePath = $this->getPath($image);
$storage = $this->getStorage();
return $storage->get($imagePath);
}
/** /**
* Destroys an Image object along with its files and thumbnails. * Destroys an Image object along with its files and thumbnails.
* @param Image $image * @param Image $image

View File

@ -47,7 +47,7 @@ function uploadImageFile(file) {
let formData = new FormData(); let formData = new FormData();
formData.append('file', file, remoteFilename); formData.append('file', file, remoteFilename);
return window.$http.post('/images/gallery/upload', formData).then(resp => (resp.data)); return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
} }
function registerEditorShortcuts(editor) { function registerEditorShortcuts(editor) {
@ -225,25 +225,27 @@ function drawIoPlugin() {
const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json'; const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json';
let iframe = null; let iframe = null;
let pageEditor = null; let pageEditor = null;
let currentNode = null;
function isDrawing(node) { function isDrawing(node) {
return node.hasAttribute('drawio-diagram'); return node.hasAttribute('drawio-diagram');
} }
function showDrawingEditor(mceEditor) { function showDrawingEditor(mceEditor, selectedNode = null) {
pageEditor = mceEditor; pageEditor = mceEditor;
currentNode = selectedNode;
iframe = document.createElement('iframe'); iframe = document.createElement('iframe');
iframe.setAttribute('frameborder', '0'); iframe.setAttribute('frameborder', '0');
window.addEventListener('message', drawReceive); window.addEventListener('message', drawReceive);
iframe.setAttribute('src', drawIoUrl); iframe.setAttribute('src', drawIoUrl);
iframe.setAttribute('class', 'fullscreen'); iframe.setAttribute('class', 'fullscreen');
iframe.style.backgroundColor = '#FFFFFF';
document.body.appendChild(iframe); document.body.appendChild(iframe);
} }
function drawReceive(event) { function drawReceive(event) {
if (!event.data || event.data.length < 1) return; if (!event.data || event.data.length < 1) return;
let message = JSON.parse(event.data); let message = JSON.parse(event.data);
console.log(message);
if (message.event === 'init') { if (message.event === 'init') {
drawEventInit(); drawEventInit();
} else if (message.event === 'exit') { } else if (message.event === 'exit') {
@ -255,19 +257,28 @@ function drawIoPlugin() {
} }
} }
function updateContent(svg) { function updateContent(pngData) {
let svgWrap = document.createElement('div'); let id = "image-" + Math.random().toString(16).slice(2);
svgWrap.setAttribute('drawio-diagram', svg); let loadingImage = window.baseUrl('/loading.gif');
svgWrap.setAttribute('contenteditable', 'false'); let data = {
pageEditor.insertContent(svgWrap.outerHTML); image: pngData,
} uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
function b64DecodeUnicode(str) { // TODO - Handle updating an existing image
str = str.split(';base64,')[1];
// Going backwards: from bytestream, to percent-encoding, to original string. setTimeout(() => {
return decodeURIComponent(atob(str).split('').map(function(c) { pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); drawEventClose();
}).join('')); window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => {
pageEditor.dom.setAttrib(id, 'src', resp.data.url);
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id);
}).catch(err => {
pageEditor.dom.remove(id);
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
});
}, 5);
} }
function drawEventExport(message) { function drawEventExport(message) {
@ -275,11 +286,21 @@ function drawIoPlugin() {
} }
function drawEventSave(message) { function drawEventSave(message) {
drawPostMessage({action: 'export', format: 'svg', xml: message.xml, spin: 'Updating drawing'}); drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
} }
function drawEventInit() { function drawEventInit() {
if (!currentNode) {
drawPostMessage({action: 'load', autosave: 1, xml: ''}); drawPostMessage({action: 'load', autosave: 1, xml: ''});
return;
}
let imgElem = currentNode.querySelector('img');
let drawingId = currentNode.getAttribute('drawio-diagram');
$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
let xml = `data:image/png;base64,${resp.data.content}`;
drawPostMessage({action: 'load', autosave: 1, xml});
});
} }
function drawEventClose() { function drawEventClose() {
@ -308,28 +329,16 @@ function drawIoPlugin() {
editor.on('dblclick', event => { editor.on('dblclick', event => {
let selectedNode = editor.selection.getNode(); let selectedNode = editor.selection.getNode();
if (!isDrawing(selectedNode)) return; if (!isDrawing(selectedNode)) return;
showDrawingEditor(editor); showDrawingEditor(editor, selectedNode);
});
editor.on('PreProcess', function (e) {
$('div[drawio-diagram]', e.node).
each((index, elem) => {
let $elem = $(elem);
let svgData = b64DecodeUnicode($elem.attr('drawio-diagram'));
$elem.html(svgData);
});
}); });
editor.on('SetContent', function () { editor.on('SetContent', function () {
let drawings = $('body > div[drawio-diagram]'); let drawings = $('body > div[drawio-diagram]');
if (!drawings.length) return; if (!drawings.length) return;
editor.undoManager.transact(function () { editor.undoManager.transact(function () {
drawings.each((index, elem) => { drawings.each((index, elem) => {
let svgContent = b64DecodeUnicode(elem.getAttribute('drawio-diagram'));
elem.setAttribute('contenteditable', 'false'); elem.setAttribute('contenteditable', 'false');
elem.innerHTML = svgContent;
}); });
}); });
}); });
@ -379,7 +388,7 @@ module.exports = {
paste_data_images: false, paste_data_images: false,
extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]', extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
automatic_uploads: false, automatic_uploads: false,
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[svg],+svg", valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor drawio", plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor drawio",
imagetools_toolbar: 'imageoptions', imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen drawio", toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen drawio",

View File

@ -86,13 +86,15 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); Route::get('/user/all/{page}', 'ImageController@getAllForUserType');
// Standard get, update and deletion for all types // Standard get, update and deletion for all types
Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
Route::get('/base64/{id}', 'ImageController@getBase64Image');
Route::put('/update/{imageId}', 'ImageController@update'); Route::put('/update/{imageId}', 'ImageController@update');
Route::post('/drawing/upload', 'ImageController@uploadDrawing');
Route::post('/{type}/upload', 'ImageController@uploadByType'); Route::post('/{type}/upload', 'ImageController@uploadByType');
Route::get('/{type}/all', 'ImageController@getAllByType'); Route::get('/{type}/all', 'ImageController@getAllByType');
Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
Route::get('/{type}/search/{page}', 'ImageController@searchByType'); Route::get('/{type}/search/{page}', 'ImageController@searchByType');
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
Route::delete('/{imageId}', 'ImageController@destroy'); Route::delete('/{id}', 'ImageController@destroy');
}); });
// Attachments routes // Attachments routes