Extract image export functionality, add convenience page for generating README images

This commit is contained in:
David Evans 2017-11-05 00:30:48 +00:00
parent 71437d2576
commit f783750c0d
7 changed files with 324 additions and 67 deletions

37
readme_images.htm Normal file
View File

@ -0,0 +1,37 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="content-security-policy" content="
base-uri 'self';
default-src 'none';
script-src
'self'
'sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk='
;
style-src 'self';
connect-src 'self';
img-src 'self' blob:;
form-action 'none';
">
<title>Sequence Diagram</title>
<link rel="icon" href="favicon.png">
<link rel="stylesheet" href="styles/readme_images.css">
<script src="scripts/requireConfig.js"></script>
<script
data-main="scripts/readme_images"
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"
integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk="
crossorigin="anonymous"
></script>
</head>
<body>
<noscript>This tool requires Javascript!</noscript>
</body>
</html>

View File

@ -0,0 +1,76 @@
define(() => {
'use strict';
return class Exporter {
constructor() {
this.latestSVG = null;
this.canvas = null;
this.context = null;
this.indexPNG = 0;
this.latestPNGIndex = 0;
this.latestPNG = null;
}
getSVGContent(renderer) {
return renderer.svg().outerHTML;
}
getSVGBlob(renderer) {
return new Blob(
[this.getSVGContent(renderer)],
{type: 'image/svg+xml'}
);
}
getSVGURL(renderer) {
const blob = this.getSVGBlob(renderer);
if(this.latestSVG) {
URL.revokeObjectURL(this.latestSVG);
}
this.latestSVG = URL.createObjectURL(blob);
return this.latestSVG;
}
getPNGBlob(renderer, resolution, callback) {
if(!this.canvas) {
window.devicePixelRatio = 1;
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
}
const width = renderer.width * resolution;
const height = renderer.height * resolution;
const img = new Image(width, height);
img.addEventListener('load', () => {
this.canvas.width = width;
this.canvas.height = height;
this.context.drawImage(img, 0, 0);
this.canvas.toBlob(callback, 'image/png');
}, {once: true});
img.src = this.getSVGURL(renderer);
}
getPNGURL(renderer, resolution, callback) {
++ this.indexPNG;
const index = this.indexPNG;
this.getPNGBlob(renderer, resolution, (blob) => {
const url = URL.createObjectURL(blob);
const isLatest = index >= this.latestPNGIndex;
if(isLatest) {
if(this.latestPNG) {
URL.revokeObjectURL(this.latestPNG);
}
this.latestPNG = url;
this.latestPNGIndex = index;
callback(url, true);
} else {
callback(url, false);
URL.revokeObjectURL(url);
}
});
}
};
});

View File

@ -33,16 +33,14 @@ define([
parser,
generator,
renderer,
exporter,
defaultCode = '',
localStorage = '',
}) {
window.devicePixelRatio = 1;
this.canvas = makeNode('canvas');
this.context = this.canvas.getContext('2d');
this.parser = parser;
this.generator = generator;
this.renderer = renderer;
this.exporter = exporter;
this.defaultCode = defaultCode;
this.localStorage = localStorage;
this.minScale = 1.5;
@ -50,11 +48,8 @@ define([
this.debounced = null;
this.latestSeq = null;
this.renderedSeq = null;
this.canvasDirty = true;
this.svgDirty = true;
this.latestPNG = null;
this.latestSVG = null;
this.updatingPNG = null;
this.pngDirty = true;
this.updatingPNG = false;
this._downloadSVGClick = this._downloadSVGClick.bind(this);
this._downloadPNGClick = this._downloadPNGClick.bind(this);
@ -168,8 +163,7 @@ define([
redraw(sequence) {
clearTimeout(this.debounced);
this.debounced = null;
this.canvasDirty = true;
this.svgDirty = true;
this.pngDirty = true;
this.renderedSeq = sequence;
this.renderer.render(sequence);
this.updateMinSize(this.renderer.width, this.renderer.height);
@ -250,76 +244,42 @@ define([
}
}
getSVGData() {
this.forceRender();
if(!this.svgDirty) {
return this.latestSVG;
}
this.svgDirty = false;
const src = this.renderer.svg().outerHTML;
const blob = new Blob([src], {type: 'image/svg+xml'});
if(this.latestSVG) {
URL.revokeObjectURL(this.latestSVG);
}
this.latestSVG = URL.createObjectURL(blob);
return this.latestSVG;
}
getPNGData(callback) {
this.forceRender();
if(!this.canvasDirty) {
// TODO: this could cause issues if getPNGData is called
// while another update is ongoing. Needs a more robust fix
callback(this.latestPNG);
return;
}
this.canvasDirty = false;
const width = this.renderer.width * PNG_RESOLUTION;
const height = this.renderer.height * PNG_RESOLUTION;
this.canvas.width = width;
this.canvas.height = height;
const img = new Image(width, height);
img.addEventListener('load', () => {
this.context.drawImage(img, 0, 0);
this.canvas.toBlob((blob) => {
if(this.latestPNG) {
URL.revokeObjectURL(this.latestPNG);
}
this.latestPNG = URL.createObjectURL(blob);
callback(this.latestPNG);
}, 'image/png');
}, {once: true});
img.src = this.getSVGData();
}
updatePNGLink() {
const nonce = this.updatingPNG = {};
this.getPNGData((data) => {
if(this.updatingPNG === nonce) {
this.downloadPNG.setAttribute('href', data);
this.updatingPNG = null;
this.forceRender();
if(this.updatingPNG || !this.pngDirty) {
return false;
}
});
this.pngDirty = false;
this.updatingPNG = true;
this.exporter.getPNGURL(
this.renderer,
PNG_RESOLUTION,
(url, latest) => {
if(latest) {
this.downloadPNG.setAttribute('href', url);
this.updatingPNG = false;
}
}
);
return true;
}
_downloadPNGFocus() {
this.forceRender();
if(this.canvasDirty) {
this.updatePNGLink();
}
}
_downloadPNGClick(e) {
if(this.updatingPNG) {
e.preventDefault();
} else if(this.canvasDirty) {
} else if(this.updatePNGLink()) {
e.preventDefault();
this.updatePNGLink();
}
}
_downloadSVGClick() {
this.downloadSVG.setAttribute('href', this.getSVGData());
this.forceRender();
const url = this.exporter.getSVGURL(this.renderer);
this.downloadSVG.setAttribute('href', url);
}
};
});

View File

@ -4,6 +4,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
let parser = null;
let generator = null;
let renderer = null;
let exporter = null;
let container = null;
let ui = null;
@ -26,10 +27,13 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
renderer = jasmine.createSpyObj('renderer', ['render', 'svg']);
renderer.svg.and.returnValue(document.createElement('svg'));
container = jasmine.createSpyObj('container', ['appendChild']);
exporter = jasmine.createSpyObj('exporter', ['getSVGURL']);
ui = new Interface({
parser,
generator,
renderer,
exporter,
defaultCode: 'my default code',
});
});
@ -49,10 +53,12 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
describe('download SVG', () => {
it('triggers a download of the current image in SVG format', () => {
exporter.getSVGURL.and.returnValue('mySVGURL');
ui.build(container);
expect(ui.downloadSVG.getAttribute('href')).toEqual('#');
ui.downloadSVG.dispatchEvent(new Event('click'));
expect(ui.downloadSVG.getAttribute('href')).not.toEqual('#');
expect(ui.downloadSVG.getAttribute('href')).toEqual('mySVGURL');
});
});
});

View File

@ -6,12 +6,14 @@
/* jshint -W072 */ // Allow several required modules
requirejs([
'interface/Interface',
'interface/Exporter',
'sequence/Parser',
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
], (
Interface,
Exporter,
Parser,
Generator,
Renderer,
@ -40,6 +42,7 @@
parser: new Parser(),
generator: new Generator(),
renderer: new Renderer(new Theme()),
exporter: new Exporter(),
localStorage: 'src',
});
ui.build(document.body);

115
scripts/readme_images.js Normal file
View File

@ -0,0 +1,115 @@
((() => {
'use strict';
requirejs.config(window.getRequirejsCDN());
function makeText(text = '') {
return document.createTextNode(text);
}
function makeNode(type, attrs = {}) {
const o = document.createElement(type);
for(let k in attrs) {
if(attrs.hasOwnProperty(k)) {
o.setAttribute(k, attrs[k]);
}
}
return o;
}
const SAMPLE_REGEX = new RegExp(
/<img src="screenshots\/([^"]*)"[^>]*>[\s]*```([^]+?)```/g
);
function findSamples(content) {
SAMPLE_REGEX.lastIndex = 0;
const results = [];
while(true) {
const match = SAMPLE_REGEX.exec(content);
if(!match) {
break;
}
results.push({
file: match[1],
code: match[2],
});
}
return results;
}
const PNG_RESOLUTION = 4;
/* jshint -W072 */ // Allow several required modules
requirejs([
'sequence/Parser',
'sequence/Generator',
'sequence/Renderer',
'sequence/themes/Basic',
'interface/Exporter',
], (
Parser,
Generator,
Renderer,
Theme,
Exporter
) => {
const parser = new Parser();
const generator = new Generator();
const theme = new Theme();
const status = makeNode('div', {'class': 'status'});
const statusText = makeText('Loading\u2026');
status.appendChild(statusText);
document.body.appendChild(status);
function renderSample({file, code}) {
const renderer = new Renderer(theme);
const exporter = new Exporter();
const hold = makeNode('div', {'class': 'hold'});
hold.appendChild(renderer.svg());
const raster = makeNode('img', {
'src': '',
'class': 'raster',
'title': 'new',
});
hold.appendChild(raster);
hold.appendChild(makeNode('img', {
'src': 'screenshots/' + file,
'class': 'original',
'title': 'original',
}));
const downloadPNG = makeNode('a', {'href': '#', 'download': file});
downloadPNG.appendChild(makeText('Download PNG'));
hold.appendChild(downloadPNG);
document.body.appendChild(hold);
const parsed = parser.parse(code);
const sequence = generator.generate(parsed);
renderer.render(sequence);
exporter.getPNGURL(renderer, PNG_RESOLUTION, (url) => {
raster.setAttribute('src', url);
downloadPNG.setAttribute('href', url);
});
}
(fetch('README.md')
.then((response) => response.text())
.then(findSamples)
.then((samples) => {
samples.forEach(renderSample);
})
.then(() => {
document.body.removeChild(status);
})
.catch((e) => {
statusText.nodeValue = 'Error: ' + e;
})
);
});
})());

60
styles/readme_images.css Normal file
View File

@ -0,0 +1,60 @@
html, body {
margin: 0;
padding: 0;
}
body {
margin-bottom: 50px;
background: #EEEEEE;
}
.status {
text-align: center;
font-size: 2em;
margin: 10px;
}
.hold {
display: block;
width: 920px;
background: #FFFFFF;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
padding: 5px;
margin: 10px auto;
border-radius: 3px;
overflow: hidden;
}
.hold > svg {
width: 300px;
height: auto;
margin-right: 10px;
}
.hold > .raster {
width: 300px;
margin-right: 10px;
}
.hold > .original {
width: 300px;
background: #F8F8F8;
}
.hold a {
display: block;
text-align: center;
font-family: sans-serif;
padding: 15px;
margin: 5px -5px -5px;
}
.hold a:link, .hold a:visited {
color: #666699;
text-decoration: none;
cursor: pointer;
}
.hold a:active, .hold a:hover {
background: #EEEEEE;
color: #6666CC;
}