From fa89713f194e820e5cb4d4996f2a00cc80208853 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 30 May 2017 14:50:56 -0400 Subject: [PATCH 001/801] Add d3 as a dependency --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 4ad37565..09a08036 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,8 @@ "bootstrap-switch": "^3.3.4", "crypto-api": "^0.6.2", "crypto-js": "^3.1.9-1", + "d3": "^4.9.1", + "d3-hexbin": "^0.2.2", "diff": "^3.2.0", "escodegen": "^1.8.1", "esmangle": "^1.0.1", From 281d558111c5094b828583109fe8417f94abfb1c Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 30 May 2017 14:53:32 -0400 Subject: [PATCH 002/801] Add hex density chart --- src/core/Utils.js | 1 + src/core/config/OperationConfig.js | 39 ++++++ src/core/operations/Charts.js | 205 +++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100755 src/core/operations/Charts.js diff --git a/src/core/Utils.js b/src/core/Utils.js index 9b0d2a30..bb05ec3d 100755 --- a/src/core/Utils.js +++ b/src/core/Utils.js @@ -1021,6 +1021,7 @@ const Utils = { "Comma": ",", "Semi-colon": ";", "Colon": ":", + "Tab": "\t", "Line feed": "\n", "CRLF": "\r\n", "Forward slash": "/", diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 5fd5a9ee..f11809ad 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -5,6 +5,7 @@ import Base64 from "../operations/Base64.js"; import BitwiseOp from "../operations/BitwiseOp.js"; import ByteRepr from "../operations/ByteRepr.js"; import CharEnc from "../operations/CharEnc.js"; +import Charts from "../operations/Charts.js"; import Checksum from "../operations/Checksum.js"; import Cipher from "../operations/Cipher.js"; import Code from "../operations/Code.js"; @@ -3388,6 +3389,44 @@ const OperationConfig = { } ] }, + "Hex Density chart": { + description: [].join("\n"), + run: Charts.runHexDensityChart, + inputType: "string", + outputType: "html", + args: [ + { + name: "Record delimiter", + type: "option", + value: Charts.RECORD_DELIMITER_OPTIONS, + }, + { + name: "Field delimiter", + type: "option", + value: Charts.FIELD_DELIMITER_OPTIONS, + }, + { + name: "Radius", + type: "number", + value: 25, + }, + { + name: "Use column headers as labels", + type: "boolean", + value: true, + }, + { + name: "X label", + type: "string", + value: "", + }, + { + name: "Y label", + type: "string", + value: "", + }, + ] + } }; export default OperationConfig; diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js new file mode 100755 index 00000000..a1ab9725 --- /dev/null +++ b/src/core/operations/Charts.js @@ -0,0 +1,205 @@ +import * as d3 from "d3"; +import {hexbin as d3hexbin} from "d3-hexbin"; +import Utils from "../Utils.js"; + +/** + * Charting operations. + * + * @author tlwr [toby@toby.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + * + * @namespace + */ +const Charts = { + /** + * @constant + * @default + */ + RECORD_DELIMITER_OPTIONS: ["Line feed", "CRLF"], + + + /** + * @constant + * @default + */ + FIELD_DELIMITER_OPTIONS: ["Space", "Comma", "Semi-colon", "Colon", "Tab"], + + + /** + * Gets values from input for a scatter plot. + * + * @param {string} input + * @param {string} recordDelimiter + * @param {string} fieldDelimiter + * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record + * @returns {Object[]} + */ + _getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { + let headings; + const values = []; + + input + .split(recordDelimiter) + .forEach((row, rowIndex) => { + let split = row.split(fieldDelimiter); + + if (split.length !== 2) throw "Each row must have length 2."; + + if (columnHeadingsAreIncluded && rowIndex === 0) { + headings = {}; + headings.x = split[0]; + headings.y = split[1]; + } else { + let x = split[0], + y = split[1]; + + x = parseFloat(x, 10); + if (Number.isNaN(x)) throw "Values must be numbers in base 10."; + + y = parseFloat(y, 10); + if (Number.isNaN(y)) throw "Values must be numbers in base 10."; + + values.push([x, y]); + } + }); + + return { headings, values}; + }, + + + /** + * Hex Bin chart operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + runHexDensityChart: function (input, args) { + const recordDelimiter = Utils.charRep[args[0]], + fieldDelimiter = Utils.charRep[args[1]], + radius = args[2], + columnHeadingsAreIncluded = args[3], + dimension = 500; + + let xLabel = args[4], + yLabel = args[5], + { headings, values } = Charts._getScatterValues( + input, + recordDelimiter, + fieldDelimiter, + columnHeadingsAreIncluded + ); + + if (headings) { + xLabel = headings.x; + yLabel = headings.y; + } + + let svg = document.createElement("svg"); + svg = d3.select(svg) + .attr("width", "100%") + .attr("height", "100%") + .attr("viewBox", `0 0 ${dimension} ${dimension}`); + + let margin = { + top: 0, + right: 0, + bottom: 30, + left: 30, + }, + width = dimension - margin.left - margin.right, + height = dimension - margin.top - margin.bottom, + marginedSpace = svg.append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + let hexbin = d3hexbin() + .radius(radius) + .extent([0, 0], [width, height]); + + let hexPoints = hexbin(values), + maxCount = Math.max(...hexPoints.map(b => b.length)); + + let xExtent = d3.extent(hexPoints, d => d.x), + yExtent = d3.extent(hexPoints, d => d.y); + xExtent[0] -= 2 * radius; + xExtent[1] += 2 * radius; + yExtent[0] -= 2 * radius; + yExtent[1] += 2 * radius; + + let xAxis = d3.scaleLinear() + .domain(xExtent) + .range([0, width]); + let yAxis = d3.scaleLinear() + .domain(yExtent) + .range([height, 0]); + + let color = d3.scaleSequential(d3.interpolateLab("white", "steelblue")) + .domain([0, maxCount]); + + marginedSpace.append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + + marginedSpace.append("g") + .attr("class", "hexagon") + .attr("clip-path", "url(#clip)") + .selectAll("path") + .data(hexPoints) + .enter() + .append("path") + .attr("d", d => { + return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(radius * 0.75)}`; + }) + .attr("fill", (d) => color(d.length)) + .append("title") + .text(d => { + let count = d.length, + perc = 100.0 * d.length / values.length, + CX = d.x, + CY = d.y, + xMin = Math.min(...d.map(d => d[0])), + xMax = Math.max(...d.map(d => d[0])), + yMin = Math.min(...d.map(d => d[1])), + yMax = Math.max(...d.map(d => d[1])), + tooltip = `Count: ${count}\n + Percentage: ${perc.toFixed(2)}%\n + Center: ${CX.toFixed(2)}, ${CY.toFixed(2)}\n + Min X: ${xMin.toFixed(2)}\n + Max X: ${xMax.toFixed(2)}\n + Min Y: ${yMin.toFixed(2)}\n + Max Y: ${yMax.toFixed(2)} + `.replace(/\s{2,}/g, "\n"); + return tooltip; + }); + + marginedSpace.append("g") + .attr("class", "axis axis--y") + .call(d3.axisLeft(yAxis).tickSizeOuter(-width)); + + svg.append("text") + .attr("transform", "rotate(-90)") + .attr("y", -margin.left) + .attr("x", -(height / 2)) + .attr("dy", "1em") + .style("text-anchor", "middle") + .text(yLabel); + + marginedSpace.append("g") + .attr("class", "axis axis--x") + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(xAxis).tickSizeOuter(-height)); + + svg.append("text") + .attr("x", width / 2) + .attr("y", dimension) + .style("text-anchor", "middle") + .text(xLabel); + + return svg._groups[0][0].outerHTML; + }, +}; + +export default Charts; From 6cdc7d3966e19443c5d4d595ff1218eb04e6fc79 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 30 May 2017 15:24:23 -0400 Subject: [PATCH 003/801] Hex density: split radius into draw & pack radii --- src/core/config/OperationConfig.js | 7 ++++++- src/core/operations/Charts.js | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index f11809ad..db7f5837 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3406,10 +3406,15 @@ const OperationConfig = { value: Charts.FIELD_DELIMITER_OPTIONS, }, { - name: "Radius", + name: "Pack radius", type: "number", value: 25, }, + { + name: "Draw radius", + type: "number", + value: 15, + }, { name: "Use column headers as labels", type: "boolean", diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index a1ab9725..1c026fb7 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -78,12 +78,13 @@ const Charts = { runHexDensityChart: function (input, args) { const recordDelimiter = Utils.charRep[args[0]], fieldDelimiter = Utils.charRep[args[1]], - radius = args[2], - columnHeadingsAreIncluded = args[3], + packRadius = args[2], + drawRadius = args[3], + columnHeadingsAreIncluded = args[4], dimension = 500; - let xLabel = args[4], - yLabel = args[5], + let xLabel = args[5], + yLabel = args[6], { headings, values } = Charts._getScatterValues( input, recordDelimiter, @@ -114,7 +115,7 @@ const Charts = { .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); let hexbin = d3hexbin() - .radius(radius) + .radius(packRadius) .extent([0, 0], [width, height]); let hexPoints = hexbin(values), @@ -122,10 +123,10 @@ const Charts = { let xExtent = d3.extent(hexPoints, d => d.x), yExtent = d3.extent(hexPoints, d => d.y); - xExtent[0] -= 2 * radius; - xExtent[1] += 2 * radius; - yExtent[0] -= 2 * radius; - yExtent[1] += 2 * radius; + xExtent[0] -= 2 * packRadius; + xExtent[1] += 2 * packRadius; + yExtent[0] -= 2 * packRadius; + yExtent[1] += 2 * packRadius; let xAxis = d3.scaleLinear() .domain(xExtent) @@ -151,7 +152,7 @@ const Charts = { .enter() .append("path") .attr("d", d => { - return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(radius * 0.75)}`; + return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`; }) .attr("fill", (d) => color(d.length)) .append("title") From dc642be1f53b270f8107b09405f79e5ecd012ef2 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 30 May 2017 15:49:22 -0400 Subject: [PATCH 004/801] Hex plot: add edge drawing & changing colour opts --- src/core/config/OperationConfig.js | 15 +++++++++++++++ src/core/operations/Charts.js | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index db7f5837..ffb75a07 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3430,6 +3430,21 @@ const OperationConfig = { type: "string", value: "", }, + { + name: "Draw hexagon edges", + type: "boolean", + value: false, + }, + { + name: "Min colour value", + type: "string", + value: Charts.COLOURS.min, + }, + { + name: "Max colour value", + type: "string", + value: Charts.COLOURS.max, + }, ] } }; diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index 1c026fb7..eb8c7efe 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -68,6 +68,19 @@ const Charts = { }, + /** + * Default from colour + * + * @constant + * @default + */ + COLOURS: { + min: "white", + max: "black", + }, + + + /** * Hex Bin chart operation. * @@ -81,6 +94,9 @@ const Charts = { packRadius = args[2], drawRadius = args[3], columnHeadingsAreIncluded = args[4], + drawEdges = args[7], + minColour = args[8], + maxColour = args[9], dimension = 500; let xLabel = args[5], @@ -135,7 +151,7 @@ const Charts = { .domain(yExtent) .range([height, 0]); - let color = d3.scaleSequential(d3.interpolateLab("white", "steelblue")) + let colour = d3.scaleSequential(d3.interpolateLab(minColour, maxColour)) .domain([0, maxCount]); marginedSpace.append("clipPath") @@ -154,7 +170,9 @@ const Charts = { .attr("d", d => { return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`; }) - .attr("fill", (d) => color(d.length)) + .attr("fill", (d) => colour(d.length)) + .attr("stroke", drawEdges ? "black" : "none") + .attr("stroke-width", drawEdges ? "0.5" : "none") .append("title") .text(d => { let count = d.length, From b4188db671ec1451c089b0f5416a9aeaf13805ec Mon Sep 17 00:00:00 2001 From: toby Date: Wed, 31 May 2017 14:56:03 -0400 Subject: [PATCH 005/801] Hexagon density: allow dense plotting of hexagons --- src/core/config/OperationConfig.js | 5 +++ src/core/operations/Charts.js | 56 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index ffb75a07..ab38b7cf 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3445,6 +3445,11 @@ const OperationConfig = { type: "string", value: Charts.COLOURS.max, }, + { + name: "Draw empty hexagons within data boundaries", + type: "boolean", + value: false, + }, ] } }; diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index eb8c7efe..447d47b2 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -80,6 +80,36 @@ const Charts = { }, + /** + * Hex Bin chart operation. + * + * @param {Object[]} - centres + * @param {number} - radius + * @returns {Object[]} + */ + _getEmptyHexagons(centres, radius) { + const emptyCentres = []; + let boundingRect = [d3.extent(centres, d => d.x), d3.extent(centres, d => d.y)], + indent = false, + hexagonCenterToEdge = Math.cos(2 * Math.PI / 12) * radius, + hexagonEdgeLength = Math.sin(2 * Math.PI / 12) * radius; + + for (let y = boundingRect[1][0]; y <= boundingRect[1][1] + radius; y += hexagonEdgeLength + radius) { + for (let x = boundingRect[0][0]; x <= boundingRect[0][1] + radius; x += 2 * hexagonCenterToEdge) { + let cx = x, + cy = y; + + if (indent && x >= boundingRect[0][1]) break; + if (indent) cx += hexagonCenterToEdge; + + emptyCentres.push({x: cx, y: cy}); + } + indent = !indent; + } + + return emptyCentres; + }, + /** * Hex Bin chart operation. @@ -97,6 +127,7 @@ const Charts = { drawEdges = args[7], minColour = args[8], maxColour = args[9], + drawEmptyHexagons = args[10], dimension = 500; let xLabel = args[5], @@ -160,6 +191,31 @@ const Charts = { .attr("width", width) .attr("height", height); + if (drawEmptyHexagons) { + marginedSpace.append("g") + .attr("class", "empty-hexagon") + .selectAll("path") + .data(Charts._getEmptyHexagons(hexPoints, packRadius)) + .enter() + .append("path") + .attr("d", d => { + return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`; + }) + .attr("fill", (d) => colour(0)) + .attr("stroke", drawEdges ? "black" : "none") + .attr("stroke-width", drawEdges ? "0.5" : "none") + .append("title") + .text(d => { + let count = 0, + perc = 0, + tooltip = `Count: ${count}\n + Percentage: ${perc.toFixed(2)}%\n + Center: ${d.x.toFixed(2)}, ${d.y.toFixed(2)}\n + `.replace(/\s{2,}/g, "\n"); + return tooltip; + }); + } + marginedSpace.append("g") .attr("class", "hexagon") .attr("clip-path", "url(#clip)") From 1c87707a76652642b544ed993a6289b2fc9a4053 Mon Sep 17 00:00:00 2001 From: toby Date: Mon, 5 Jun 2017 10:24:06 -0400 Subject: [PATCH 006/801] Add heatmap chart operation --- src/core/config/OperationConfig.js | 58 ++++++++++ src/core/operations/Charts.js | 176 +++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index ab38b7cf..62ba46e5 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3451,6 +3451,64 @@ const OperationConfig = { value: false, }, ] + }, + "Heatmap chart": { + description: [].join("\n"), + run: Charts.runHeatmapChart, + inputType: "string", + outputType: "html", + args: [ + { + name: "Record delimiter", + type: "option", + value: Charts.RECORD_DELIMITER_OPTIONS, + }, + { + name: "Field delimiter", + type: "option", + value: Charts.FIELD_DELIMITER_OPTIONS, + }, + { + name: "Number of vertical bins", + type: "number", + value: 25, + }, + { + name: "Number of horizontal bins", + type: "number", + value: 25, + }, + { + name: "Use column headers as labels", + type: "boolean", + value: true, + }, + { + name: "X label", + type: "string", + value: "", + }, + { + name: "Y label", + type: "string", + value: "", + }, + { + name: "Draw bin edges", + type: "boolean", + value: false, + }, + { + name: "Min colour value", + type: "string", + value: Charts.COLOURS.min, + }, + { + name: "Max colour value", + type: "string", + value: Charts.COLOURS.max, + }, + ] } }; diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index 447d47b2..5a927ce3 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -275,6 +275,182 @@ const Charts = { return svg._groups[0][0].outerHTML; }, + + + /** + * Packs a list of x, y coordinates into a number of bins for use in a heatmap. + * + * @param {Object[]} points + * @param {number} number of vertical bins + * @param {number} number of horizontal bins + * @returns {Object[]} a list of bins (each bin is an Array) with x y coordinates, filled with the points + */ + _getHeatmapPacking(values, vBins, hBins) { + const xBounds = d3.extent(values, d => d[0]), + yBounds = d3.extent(values, d => d[1]), + bins = []; + + if (xBounds[0] === xBounds[1]) throw "Cannot pack points. There is no difference between the minimum and maximum X coordinate."; + if (yBounds[0] === yBounds[1]) throw "Cannot pack points. There is no difference between the minimum and maximum Y coordinate."; + + for (let y = 0; y < vBins; y++) { + bins.push([]); + for (let x = 0; x < hBins; x++) { + let item = []; + item.y = y; + item.x = x; + + bins[y].push(item); + } // x + } // y + + let epsilon = 0.000000001; // This is to clamp values that are exactly the maximum; + + values.forEach(v => { + let fractionOfY = (v[1] - yBounds[0]) / ((yBounds[1] + epsilon) - yBounds[0]), + fractionOfX = (v[0] - xBounds[0]) / ((xBounds[1] + epsilon) - xBounds[0]); + let y = Math.floor(vBins * fractionOfY), + x = Math.floor(hBins * fractionOfX); + + bins[y][x].push({x: v[0], y: v[1]}); + }); + + return bins; + }, + + + /** + * Heatmap chart operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + runHeatmapChart: function (input, args) { + const recordDelimiter = Utils.charRep[args[0]], + fieldDelimiter = Utils.charRep[args[1]], + vBins = args[2], + hBins = args[3], + columnHeadingsAreIncluded = args[4], + drawEdges = args[7], + minColour = args[8], + maxColour = args[9], + dimension = 500; + + if (vBins <= 0) throw "Number of vertical bins must be greater than 0"; + if (hBins <= 0) throw "Number of horizontal bins must be greater than 0"; + + let xLabel = args[5], + yLabel = args[6], + { headings, values } = Charts._getScatterValues( + input, + recordDelimiter, + fieldDelimiter, + columnHeadingsAreIncluded + ); + + if (headings) { + xLabel = headings.x; + yLabel = headings.y; + } + + let svg = document.createElement("svg"); + svg = d3.select(svg) + .attr("width", "100%") + .attr("height", "100%") + .attr("viewBox", `0 0 ${dimension} ${dimension}`); + + let margin = { + top: 10, + right: 0, + bottom: 40, + left: 30, + }, + width = dimension - margin.left - margin.right, + height = dimension - margin.top - margin.bottom, + binWidth = width / hBins, + binHeight = height/ vBins, + marginedSpace = svg.append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + let bins = Charts._getHeatmapPacking(values, vBins, hBins), + maxCount = Math.max(...bins.map(row => { + let lengths = row.map(cell => cell.length); + return Math.max(...lengths); + })); + + let xExtent = d3.extent(values, d => d[0]), + yExtent = d3.extent(values, d => d[1]); + + let xAxis = d3.scaleLinear() + .domain(xExtent) + .range([0, width]); + let yAxis = d3.scaleLinear() + .domain(yExtent) + .range([height, 0]); + + let colour = d3.scaleSequential(d3.interpolateLab(minColour, maxColour)) + .domain([0, maxCount]); + + marginedSpace.append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + + marginedSpace.append("g") + .attr("class", "bins") + .attr("clip-path", "url(#clip)") + .selectAll("g") + .data(bins) + .enter() + .append("g") + .selectAll("rect") + .data(d => d) + .enter() + .append("rect") + .attr("x", (d) => binWidth * d.x) + .attr("y", (d) => (height - binHeight * (d.y + 1))) + .attr("width", binWidth) + .attr("height", binHeight) + .attr("fill", (d) => colour(d.length)) + .attr("stroke", drawEdges ? "rgba(0, 0, 0, 0.5)" : "none") + .attr("stroke-width", drawEdges ? "0.5" : "none") + .append("title") + .text(d => { + let count = d.length, + perc = 100.0 * d.length / values.length, + tooltip = `Count: ${count}\n + Percentage: ${perc.toFixed(2)}%\n + `.replace(/\s{2,}/g, "\n"); + return tooltip; + }); + + marginedSpace.append("g") + .attr("class", "axis axis--y") + .call(d3.axisLeft(yAxis).tickSizeOuter(-width)); + + svg.append("text") + .attr("transform", "rotate(-90)") + .attr("y", -margin.left) + .attr("x", -(height / 2)) + .attr("dy", "1em") + .style("text-anchor", "middle") + .text(yLabel); + + marginedSpace.append("g") + .attr("class", "axis axis--x") + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(xAxis).tickSizeOuter(-height)); + + svg.append("text") + .attr("x", width / 2) + .attr("y", dimension) + .style("text-anchor", "middle") + .text(xLabel); + + return svg._groups[0][0].outerHTML; + }, }; export default Charts; From 594456856592d936711f52a5a6cde5cd937694d5 Mon Sep 17 00:00:00 2001 From: toby Date: Mon, 5 Jun 2017 10:24:15 -0400 Subject: [PATCH 007/801] Change margins in hex density chart --- src/core/operations/Charts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index 5a927ce3..2202e0f1 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -151,9 +151,9 @@ const Charts = { .attr("viewBox", `0 0 ${dimension} ${dimension}`); let margin = { - top: 0, + top: 10, right: 0, - bottom: 30, + bottom: 40, left: 30, }, width = dimension - margin.left - margin.right, From 247e9bfbdeaa113b37ff1bea35c1db624a71a720 Mon Sep 17 00:00:00 2001 From: toby Date: Mon, 5 Jun 2017 21:47:32 -0400 Subject: [PATCH 008/801] Add "HTML to Text" operation --- src/core/config/OperationConfig.js | 8 ++++++++ src/core/operations/HTML.js | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 62ba46e5..cf8363f8 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3509,6 +3509,14 @@ const OperationConfig = { value: Charts.COLOURS.max, }, ] + }, + "HTML to Text": { + description: [].join("\n"), + run: HTML.runHTMLToText, + inputType: "html", + outputType: "string", + args: [ + ] } }; diff --git a/src/core/operations/HTML.js b/src/core/operations/HTML.js index 601d6102..457124be 100755 --- a/src/core/operations/HTML.js +++ b/src/core/operations/HTML.js @@ -851,6 +851,16 @@ const HTML = { "diams" : 9830, }, + /** + * HTML to text operation + * + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + runHTMLToText(input, args) { + return input; + }, }; export default HTML; From 49ea532cdc36cb6a7a52ede3cc04b40e771a3d24 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 6 Jun 2017 09:46:46 -0400 Subject: [PATCH 009/801] Tweak extent of hex density charts --- src/core/operations/Charts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index 2202e0f1..e47d26e2 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -171,7 +171,7 @@ const Charts = { let xExtent = d3.extent(hexPoints, d => d.x), yExtent = d3.extent(hexPoints, d => d.y); xExtent[0] -= 2 * packRadius; - xExtent[1] += 2 * packRadius; + xExtent[1] += 3 * packRadius; yExtent[0] -= 2 * packRadius; yExtent[1] += 2 * packRadius; From 39ab60088774f5375206209d59cadfbf2e2a84e8 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 6 Jun 2017 14:01:23 -0400 Subject: [PATCH 010/801] Add scatter plot operation --- src/core/config/OperationConfig.js | 48 ++++++ src/core/operations/Charts.js | 241 +++++++++++++++++++++++++---- 2 files changed, 258 insertions(+), 31 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index cf8363f8..d0565e7c 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3510,6 +3510,54 @@ const OperationConfig = { }, ] }, + "Scatter chart": { + description: [].join("\n"), + run: Charts.runScatterChart, + inputType: "string", + outputType: "html", + args: [ + { + name: "Record delimiter", + type: "option", + value: Charts.RECORD_DELIMITER_OPTIONS, + }, + { + name: "Field delimiter", + type: "option", + value: Charts.FIELD_DELIMITER_OPTIONS, + }, + { + name: "Use column headers as labels", + type: "boolean", + value: true, + }, + { + name: "X label", + type: "string", + value: "", + }, + { + name: "Y label", + type: "string", + value: "", + }, + { + name: "Colour", + type: "string", + value: Charts.COLOURS.max, + }, + { + name: "Point radius", + type: "number", + value: 10, + }, + { + name: "Use colour from third column", + type: "boolean", + value: false, + }, + ] + }, "HTML to Text": { description: [].join("\n"), run: HTML.runHTMLToText, diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index e47d26e2..06a3cb62 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -26,6 +26,49 @@ const Charts = { FIELD_DELIMITER_OPTIONS: ["Space", "Comma", "Semi-colon", "Colon", "Tab"], + /** + * Default from colour + * + * @constant + * @default + */ + COLOURS: { + min: "white", + max: "black", + }, + + + /** + * Gets values from input for a plot. + * + * @param {string} input + * @param {string} recordDelimiter + * @param {string} fieldDelimiter + * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record + * @returns {Object[]} + */ + _getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) { + let headings; + const values = []; + + input + .split(recordDelimiter) + .forEach((row, rowIndex) => { + let split = row.split(fieldDelimiter); + + if (split.length !== length) throw `Each row must have length ${length}.`; + + if (columnHeadingsAreIncluded && rowIndex === 0) { + headings = split; + } else { + values.push(split); + } + }); + + return { headings, values}; + }, + + /** * Gets values from input for a scatter plot. * @@ -36,47 +79,64 @@ const Charts = { * @returns {Object[]} */ _getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { - let headings; - const values = []; + let { headings, values } = Charts._getValues( + input, + recordDelimiter, fieldDelimiter, + columnHeadingsAreIncluded, + 2 + ); - input - .split(recordDelimiter) - .forEach((row, rowIndex) => { - let split = row.split(fieldDelimiter); + if (headings) { + headings = {x: headings[0], y: headings[1]}; + } - if (split.length !== 2) throw "Each row must have length 2."; + values = values.map(row => { + let x = parseFloat(row[0], 10), + y = parseFloat(row[1], 10); - if (columnHeadingsAreIncluded && rowIndex === 0) { - headings = {}; - headings.x = split[0]; - headings.y = split[1]; - } else { - let x = split[0], - y = split[1]; + if (Number.isNaN(x)) throw "Values must be numbers in base 10."; + if (Number.isNaN(y)) throw "Values must be numbers in base 10."; - x = parseFloat(x, 10); - if (Number.isNaN(x)) throw "Values must be numbers in base 10."; + return [x, y]; + }); - y = parseFloat(y, 10); - if (Number.isNaN(y)) throw "Values must be numbers in base 10."; - - values.push([x, y]); - } - }); - - return { headings, values}; + return { headings, values }; }, - + /** - * Default from colour + * Gets values from input for a scatter plot with colour from the third column. * - * @constant - * @default + * @param {string} input + * @param {string} recordDelimiter + * @param {string} fieldDelimiter + * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record + * @returns {Object[]} */ - COLOURS: { - min: "white", - max: "black", + _getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { + let { headings, values } = Charts._getValues( + input, + recordDelimiter, fieldDelimiter, + columnHeadingsAreIncluded, + 3 + ); + + if (headings) { + headings = {x: headings[0], y: headings[1]}; + } + + values = values.map(row => { + let x = parseFloat(row[0], 10), + y = parseFloat(row[1], 10), + colour = row[2]; + + if (Number.isNaN(x)) throw "Values must be numbers in base 10."; + if (Number.isNaN(y)) throw "Values must be numbers in base 10."; + + return [x, y, colour]; + }); + + return { headings, values }; }, @@ -451,6 +511,125 @@ const Charts = { return svg._groups[0][0].outerHTML; }, + + + /** + * Scatter chart operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + runScatterChart: function (input, args) { + const recordDelimiter = Utils.charRep[args[0]], + fieldDelimiter = Utils.charRep[args[1]], + columnHeadingsAreIncluded = args[2], + fillColour = args[5], + radius = args[6], + colourInInput = args[7], + dimension = 500; + + let xLabel = args[3], + yLabel = args[4]; + + let dataFunction = colourInInput ? Charts._getScatterValuesWithColour : Charts._getScatterValues; + + let { headings, values } = dataFunction( + input, + recordDelimiter, + fieldDelimiter, + columnHeadingsAreIncluded + ); + + if (headings) { + xLabel = headings.x; + yLabel = headings.y; + } + + let svg = document.createElement("svg"); + svg = d3.select(svg) + .attr("width", "100%") + .attr("height", "100%") + .attr("viewBox", `0 0 ${dimension} ${dimension}`); + + let margin = { + top: 10, + right: 0, + bottom: 40, + left: 30, + }, + width = dimension - margin.left - margin.right, + height = dimension - margin.top - margin.bottom, + marginedSpace = svg.append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + let xExtent = d3.extent(values, d => d[0]), + xDelta = xExtent[1] - xExtent[0], + yExtent = d3.extent(values, d => d[1]), + yDelta = yExtent[1] - yExtent[0], + xAxis = d3.scaleLinear() + .domain([xExtent[0] - (0.1 * xDelta), xExtent[1] + (0.1 * xDelta)]) + .range([0, width]), + yAxis = d3.scaleLinear() + .domain([yExtent[0] - (0.1 * yDelta), yExtent[1] + (0.1 * yDelta)]) + .range([height, 0]); + + marginedSpace.append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + + marginedSpace.append("g") + .attr("class", "points") + .attr("clip-path", "url(#clip)") + .selectAll("circle") + .data(values) + .enter() + .append("circle") + .attr("cx", (d) => xAxis(d[0])) + .attr("cy", (d) => yAxis(d[1])) + .attr("r", d => radius) + .attr("fill", d => { + return colourInInput ? d[2] : fillColour; + }) + .attr("stroke", "rgba(0, 0, 0, 0.5)") + .attr("stroke-width", "0.5") + .append("title") + .text(d => { + let x = d[0], + y = d[1], + tooltip = `X: ${x}\n + Y: ${y}\n + `.replace(/\s{2,}/g, "\n"); + return tooltip; + }); + + marginedSpace.append("g") + .attr("class", "axis axis--y") + .call(d3.axisLeft(yAxis).tickSizeOuter(-width)); + + svg.append("text") + .attr("transform", "rotate(-90)") + .attr("y", -margin.left) + .attr("x", -(height / 2)) + .attr("dy", "1em") + .style("text-anchor", "middle") + .text(yLabel); + + marginedSpace.append("g") + .attr("class", "axis axis--x") + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(xAxis).tickSizeOuter(-height)); + + svg.append("text") + .attr("x", width / 2) + .attr("y", dimension) + .style("text-anchor", "middle") + .text(xLabel); + + return svg._groups[0][0].outerHTML; + }, }; export default Charts; From 6784a1c0276c83e7e35020f3953a6b839c67239d Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 20 Jun 2017 15:25:16 -0400 Subject: [PATCH 011/801] Add Series chart operation --- src/core/config/OperationConfig.js | 33 +++++ src/core/operations/Charts.js | 208 ++++++++++++++++++++++++++++- 2 files changed, 240 insertions(+), 1 deletion(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index d0565e7c..f9b5937d 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3558,6 +3558,39 @@ const OperationConfig = { }, ] }, + "Series chart": { + description: [].join("\n"), + run: Charts.runSeriesChart, + inputType: "string", + outputType: "html", + args: [ + { + name: "Record delimiter", + type: "option", + value: Charts.RECORD_DELIMITER_OPTIONS, + }, + { + name: "Field delimiter", + type: "option", + value: Charts.FIELD_DELIMITER_OPTIONS, + }, + { + name: "X label", + type: "string", + value: "", + }, + { + name: "Point radius", + type: "number", + value: 1, + }, + { + name: "Series colours", + type: "string", + value: "mediumseagreen, dodgerblue, tomato", + }, + ] + }, "HTML to Text": { description: [].join("\n"), run: HTML.runHTMLToText, diff --git a/src/core/operations/Charts.js b/src/core/operations/Charts.js index 06a3cb62..2ce084d0 100755 --- a/src/core/operations/Charts.js +++ b/src/core/operations/Charts.js @@ -103,7 +103,7 @@ const Charts = { return { headings, values }; }, - + /** * Gets values from input for a scatter plot with colour from the third column. * @@ -140,6 +140,50 @@ const Charts = { }, + /** + * Gets values from input for a time series plot. + * + * @param {string} input + * @param {string} recordDelimiter + * @param {string} fieldDelimiter + * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record + * @returns {Object[]} + */ + _getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { + let { headings, values } = Charts._getValues( + input, + recordDelimiter, fieldDelimiter, + false, + 3 + ); + + let xValues = new Set(), + series = {}; + + values = values.forEach(row => { + let serie = row[0], + xVal = row[1], + val = parseFloat(row[2], 10); + + if (Number.isNaN(val)) throw "Values must be numbers in base 10."; + + xValues.add(xVal); + if (typeof series[serie] === "undefined") series[serie] = {}; + series[serie][xVal] = val; + }); + + xValues = new Array(...xValues); + + const seriesList = []; + for (let seriesName in series) { + let serie = series[seriesName]; + seriesList.push({name: seriesName, data: serie}); + } + + return { xValues, series: seriesList }; + }, + + /** * Hex Bin chart operation. * @@ -630,6 +674,168 @@ const Charts = { return svg._groups[0][0].outerHTML; }, + + + /** + * Series chart operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + runSeriesChart(input, args) { + const recordDelimiter = Utils.charRep[args[0]], + fieldDelimiter = Utils.charRep[args[1]], + xLabel = args[2], + pipRadius = args[3], + seriesColours = args[4].split(","), + svgWidth = 500, + interSeriesPadding = 20, + xAxisHeight = 50, + seriesLabelWidth = 50, + seriesHeight = 100, + seriesWidth = svgWidth - seriesLabelWidth - interSeriesPadding; + + let { xValues, series } = Charts._getSeriesValues(input, recordDelimiter, fieldDelimiter), + allSeriesHeight = Object.keys(series).length * (interSeriesPadding + seriesHeight), + svgHeight = allSeriesHeight + xAxisHeight + interSeriesPadding; + + let svg = document.createElement("svg"); + svg = d3.select(svg) + .attr("width", "100%") + .attr("height", "100%") + .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`); + + let xAxis = d3.scalePoint() + .domain(xValues) + .range([0, seriesWidth]); + + svg.append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`) + .call( + d3.axisTop(xAxis).tickValues(xValues.filter((x, i) => { + return [0, Math.round(xValues.length / 2), xValues.length -1].indexOf(i) >= 0; + })) + ); + + svg.append("text") + .attr("x", svgWidth / 2) + .attr("y", xAxisHeight / 2) + .style("text-anchor", "middle") + .text(xLabel); + + let tooltipText = {}, + tooltipAreaWidth = seriesWidth / xValues.length; + + xValues.forEach(x => { + let tooltip = []; + + series.forEach(serie => { + let y = serie.data[x]; + if (typeof y === "undefined") return; + + tooltip.push(`${serie.name}: ${y}`); + }); + + tooltipText[x] = tooltip.join("\n"); + }); + + let chartArea = svg.append("g") + .attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`); + + chartArea + .append("g") + .selectAll("rect") + .data(xValues) + .enter() + .append("rect") + .attr("x", x => { + return xAxis(x) - (tooltipAreaWidth / 2); + }) + .attr("y", 0) + .attr("width", tooltipAreaWidth) + .attr("height", allSeriesHeight) + .attr("stroke", "none") + .attr("fill", "transparent") + .append("title") + .text(x => { + return `${x}\n + --\n + ${tooltipText[x]}\n + `.replace(/\s{2,}/g, "\n"); + }); + + let yAxesArea = svg.append("g") + .attr("transform", `translate(0, ${xAxisHeight})`); + + series.forEach((serie, seriesIndex) => { + let yExtent = d3.extent(Object.values(serie.data)), + yAxis = d3.scaleLinear() + .domain(yExtent) + .range([seriesHeight, 0]); + + let seriesGroup = chartArea + .append("g") + .attr("transform", `translate(0, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`); + + let path = ""; + xValues.forEach((x, xIndex) => { + let nextX = xValues[xIndex + 1], + y = serie.data[x], + nextY= serie.data[nextX]; + + if (typeof y === "undefined" || typeof nextY === "undefined") return; + + x = xAxis(x); nextX = xAxis(nextX); + y = yAxis(y); nextY = yAxis(nextY); + + path += `M ${x} ${y} L ${nextX} ${nextY} z `; + }); + + seriesGroup + .append("path") + .attr("d", path) + .attr("fill", "none") + .attr("stroke", seriesColours[seriesIndex % seriesColours.length]) + .attr("stroke-width", "1"); + + xValues.forEach(x => { + let y = serie.data[x]; + if (typeof y === "undefined") return; + + seriesGroup + .append("circle") + .attr("cx", xAxis(x)) + .attr("cy", yAxis(y)) + .attr("r", pipRadius) + .attr("fill", seriesColours[seriesIndex % seriesColours.length]) + .append("title") + .text(d => { + return `${x}\n + --\n + ${tooltipText[x]}\n + `.replace(/\s{2,}/g, "\n"); + }); + }); + + yAxesArea + .append("g") + .attr("transform", `translate(${seriesLabelWidth - interSeriesPadding}, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`) + .attr("class", "axis axis--y") + .call(d3.axisLeft(yAxis).ticks(5)); + + yAxesArea + .append("g") + .attr("transform", `translate(0, ${seriesHeight / 2 + seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`) + .append("text") + .style("text-anchor", "middle") + .attr("transform", "rotate(-90)") + .text(serie.name); + }); + + return svg._groups[0][0].outerHTML; + }, }; export default Charts; From 59877b51389da0471c126dce242cd01c19688c31 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 13 Apr 2018 12:14:40 +0100 Subject: [PATCH 012/801] Exporing options with API. --- package.json | 3 +- src/core/Dish.mjs | 20 +++++++ src/core/operations/ToBase32.mjs | 2 - src/node/Wrapper.mjs | 99 ++++++++++++++++++++++++++++++++ src/node/index.mjs | 42 +++++++++----- 5 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 src/node/Wrapper.mjs diff --git a/package.json b/package.json index c44a3a1f..ff567519 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "build": "grunt prod", "test": "grunt test", "docs": "grunt docs", - "lint": "grunt lint" + "lint": "grunt lint", + "build-node": "grunt node" } } diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 6aeaf3e9..08c7206a 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -240,6 +240,26 @@ class Dish { } } + /** + * + */ + findType() { + if (!this.value) { + throw "Dish has no value"; + } + + const types = [Dish.BYTE_ARRAY, Dish.STRING, Dish.HTML, Dish.NUMBER, Dish.ARRAY_BUFFER, Dish.BIG_NUMBER, Dish.LIST_FILE]; + + types.find((type) => { + this.type = type; + if (this.valid()) { + return true; + } + }); + + return this.type; + } + /** * Determines how much space the Dish takes up. diff --git a/src/core/operations/ToBase32.mjs b/src/core/operations/ToBase32.mjs index 1b217a34..632d93e4 100644 --- a/src/core/operations/ToBase32.mjs +++ b/src/core/operations/ToBase32.mjs @@ -45,7 +45,6 @@ class ToBase32 extends Operation { chr1, chr2, chr3, chr4, chr5, enc1, enc2, enc3, enc4, enc5, enc6, enc7, enc8, i = 0; - while (i < input.length) { chr1 = input[i++]; chr2 = input[i++]; @@ -76,7 +75,6 @@ class ToBase32 extends Operation { alphabet.charAt(enc4) + alphabet.charAt(enc5) + alphabet.charAt(enc6) + alphabet.charAt(enc7) + alphabet.charAt(enc8); } - return output; } diff --git a/src/node/Wrapper.mjs b/src/node/Wrapper.mjs new file mode 100644 index 00000000..33f34b7b --- /dev/null +++ b/src/node/Wrapper.mjs @@ -0,0 +1,99 @@ +/** + * Wrap operations in a + * + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Dish from "../core/Dish"; +import log from "loglevel"; + +/** + * + */ +export default class Wrapper { + + /** + * + * @param arg + */ + extractArg(arg) { + if (arg.type === "option" || arg.type === "editableOption") { + return arg.value[0]; + } + + return arg.value; + } + + /** + * + */ + wrap(operation) { + this.operation = new operation(); + // This for just exposing run function: + // return this.run.bind(this); + + /** + * + * @param input + * @param args + */ + const _run = async(input, args=null) => { + const dish = new Dish(input); + + try { + dish.findType(); + } catch (e) { + log.debug(e); + } + + if (!args) { + args = this.operation.args.map(this.extractArg); + } else { + // Allows single arg ops to have arg defined not in array + if (!(args instanceof Array)) { + args = [args]; + } + } + const transformedInput = await dish.get(Dish.typeEnum(this.operation.inputType)); + return this.operation.innerRun(transformedInput, args); + }; + + // There's got to be a nicer way to do this! + this.operation.innerRun = this.operation.run; + this.operation.run = _run; + + return this.operation; + } + + /** + * + * @param input + */ + async run(input, args = null) { + const dish = new Dish(input); + + try { + dish.findType(); + } catch (e) { + log.debug(e); + } + + if (!args) { + args = this.operation.args.map(this.extractArg); + } else { + // Allows single arg ops to have arg defined not in array + if (!(args instanceof Array)) { + args = [args]; + } + } + + const transformedInput = await dish.get(Dish.typeEnum(this.operation.inputType)); + return this.operation.run(transformedInput, args); + + + + + } +} diff --git a/src/node/index.mjs b/src/node/index.mjs index c6e86c68..3dfda7d7 100644 --- a/src/node/index.mjs +++ b/src/node/index.mjs @@ -18,22 +18,36 @@ global.ENVIRONMENT_IS_WEB = function() { return typeof window === "object"; }; -import Chef from "../core/Chef"; +// import Chef from "../core/Chef"; -const CyberChef = { +// const CyberChef = { - bake: function(input, recipeConfig) { - this.chef = new Chef(); - return this.chef.bake( - input, - recipeConfig, - {}, - 0, - false - ); +// bake: function(input, recipeConfig) { +// this.chef = new Chef(); +// return this.chef.bake( +// input, +// recipeConfig, +// {}, +// 0, +// false +// ); +// } + +// }; + +// export default CyberChef; +// export {CyberChef}; + +import Wrapper from "./Wrapper"; + +import * as operations from "../core/operations/index"; + +const cyberChef = { + base32: { + from: new Wrapper().wrap(operations.FromBase32), + to: new Wrapper().wrap(operations.ToBase32), } - }; -export default CyberChef; -export {CyberChef}; +export default cyberChef; +export {cyberChef}; From fca4ed70131c57915e0119539c7adcd86986dbf7 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 20 Apr 2018 10:55:17 +0100 Subject: [PATCH 013/801] simplified API --- src/node/Wrapper.mjs | 85 +++++++++++--------------------------------- src/node/index.mjs | 45 +++++++++-------------- 2 files changed, 37 insertions(+), 93 deletions(-) diff --git a/src/node/Wrapper.mjs b/src/node/Wrapper.mjs index 33f34b7b..66876295 100644 --- a/src/node/Wrapper.mjs +++ b/src/node/Wrapper.mjs @@ -10,68 +10,28 @@ import Dish from "../core/Dish"; import log from "loglevel"; /** - * + * Extract default arg value from operation argument + * @param {Object} arg - an arg from an operation */ -export default class Wrapper { - - /** - * - * @param arg - */ - extractArg(arg) { - if (arg.type === "option" || arg.type === "editableOption") { - return arg.value[0]; - } - - return arg.value; +function extractArg(arg) { + if (arg.type === "option" || arg.type === "editableOption") { + return arg.value[0]; } + return arg.value; +} + +/** + * Wrap an operation to be consumed by node API. + * new Operation().run() becomes operation() + * @param Operation + */ +export default function wrap(Operation) { /** * */ - wrap(operation) { - this.operation = new operation(); - // This for just exposing run function: - // return this.run.bind(this); - - /** - * - * @param input - * @param args - */ - const _run = async(input, args=null) => { - const dish = new Dish(input); - - try { - dish.findType(); - } catch (e) { - log.debug(e); - } - - if (!args) { - args = this.operation.args.map(this.extractArg); - } else { - // Allows single arg ops to have arg defined not in array - if (!(args instanceof Array)) { - args = [args]; - } - } - const transformedInput = await dish.get(Dish.typeEnum(this.operation.inputType)); - return this.operation.innerRun(transformedInput, args); - }; - - // There's got to be a nicer way to do this! - this.operation.innerRun = this.operation.run; - this.operation.run = _run; - - return this.operation; - } - - /** - * - * @param input - */ - async run(input, args = null) { + return async (input, args=null) => { + const operation = new Operation(); const dish = new Dish(input); try { @@ -81,19 +41,14 @@ export default class Wrapper { } if (!args) { - args = this.operation.args.map(this.extractArg); + args = operation.args.map(extractArg); } else { // Allows single arg ops to have arg defined not in array if (!(args instanceof Array)) { args = [args]; } } - - const transformedInput = await dish.get(Dish.typeEnum(this.operation.inputType)); - return this.operation.run(transformedInput, args); - - - - - } + const transformedInput = await dish.get(Dish.typeEnum(operation.inputType)); + return operation.run(transformedInput, args); + }; } diff --git a/src/node/index.mjs b/src/node/index.mjs index 3dfda7d7..8900fbd4 100644 --- a/src/node/index.mjs +++ b/src/node/index.mjs @@ -18,36 +18,25 @@ global.ENVIRONMENT_IS_WEB = function() { return typeof window === "object"; }; -// import Chef from "../core/Chef"; -// const CyberChef = { - -// bake: function(input, recipeConfig) { -// this.chef = new Chef(); -// return this.chef.bake( -// input, -// recipeConfig, -// {}, -// 0, -// false -// ); -// } - -// }; - -// export default CyberChef; -// export {CyberChef}; - -import Wrapper from "./Wrapper"; +import wrap from "./Wrapper"; import * as operations from "../core/operations/index"; -const cyberChef = { - base32: { - from: new Wrapper().wrap(operations.FromBase32), - to: new Wrapper().wrap(operations.ToBase32), - } -}; +/** + * + * @param name + */ +function decapitalise(name) { + return `${name.charAt(0).toLowerCase()}${name.substr(1)}`; +} -export default cyberChef; -export {cyberChef}; + +// console.log(operations); +const chef = {}; +Object.keys(operations).forEach(op => + chef[decapitalise(op)] = wrap(operations[op])); + + +export default chef; +export {chef}; From b8b98358d0643abe42dc7b5c0b4aafd3120c18e4 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 20 Apr 2018 12:23:20 +0100 Subject: [PATCH 014/801] function tidy, add comments --- src/node/Wrapper.mjs | 54 -------------------- src/node/apiUtils.mjs | 112 ++++++++++++++++++++++++++++++++++++++++++ src/node/index.mjs | 22 +++------ 3 files changed, 119 insertions(+), 69 deletions(-) delete mode 100644 src/node/Wrapper.mjs create mode 100644 src/node/apiUtils.mjs diff --git a/src/node/Wrapper.mjs b/src/node/Wrapper.mjs deleted file mode 100644 index 66876295..00000000 --- a/src/node/Wrapper.mjs +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Wrap operations in a - * - * @author d98762625 [d98762625@gmail.com] - * @copyright Crown Copyright 2018 - * @license Apache-2.0 - */ - -import Dish from "../core/Dish"; -import log from "loglevel"; - -/** - * Extract default arg value from operation argument - * @param {Object} arg - an arg from an operation - */ -function extractArg(arg) { - if (arg.type === "option" || arg.type === "editableOption") { - return arg.value[0]; - } - - return arg.value; -} - -/** - * Wrap an operation to be consumed by node API. - * new Operation().run() becomes operation() - * @param Operation - */ -export default function wrap(Operation) { - /** - * - */ - return async (input, args=null) => { - const operation = new Operation(); - const dish = new Dish(input); - - try { - dish.findType(); - } catch (e) { - log.debug(e); - } - - if (!args) { - args = operation.args.map(extractArg); - } else { - // Allows single arg ops to have arg defined not in array - if (!(args instanceof Array)) { - args = [args]; - } - } - const transformedInput = await dish.get(Dish.typeEnum(operation.inputType)); - return operation.run(transformedInput, args); - }; -} diff --git a/src/node/apiUtils.mjs b/src/node/apiUtils.mjs new file mode 100644 index 00000000..f8457247 --- /dev/null +++ b/src/node/apiUtils.mjs @@ -0,0 +1,112 @@ +/** + * Wrap operations for consumption in Node + * + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Dish from "../core/Dish"; +import log from "loglevel"; + +/** + * Extract default arg value from operation argument + * @param {Object} arg - an arg from an operation + */ +function extractArg(arg) { + if (arg.type === "option" || arg.type === "editableOption") { + return arg.value[0]; + } + + return arg.value; +} + +/** + * Wrap an operation to be consumed by node API. + * new Operation().run() becomes operation() + * Perform type conversion on input + * @param {Operation} Operation + * @returns {Function} The operation's run function, wrapped in + * some type conversion logic + */ +export function wrap(Operation) { + /** + * Wrapped operation run function + */ + return async (input, args=null) => { + const operation = new Operation(); + const dish = new Dish(input); + + try { + dish.findType(); + } catch (e) { + log.debug(e); + } + + if (!args) { + args = operation.args.map(extractArg); + } else { + // Allows single arg ops to have arg defined not in array + if (!(args instanceof Array)) { + args = [args]; + } + } + const transformedInput = await dish.get(Dish.typeEnum(operation.inputType)); + return operation.run(transformedInput, args); + }; +} + +/** + * + * @param searchTerm + */ +export function search(searchTerm) { + +} + + +/** + * Extract properties from an operation by instantiating it and + * returning some of its properties for reference. + * @param {Operation} Operation - the operation to extract info from + * @returns {Object} operation properties + */ +function extractOperationInfo(Operation) { + const operation = new Operation(); + return { + name: operation.name, + module: operation.module, + description: operation.description, + inputType: operation.inputType, + outputType: operation.outputType, + args: Object.assign([], operation.args), + }; +} + + +/** + * @param {Object} operations - an object filled with operations. + * @param {String} searchTerm - the name of the operation to get help for. + * Case and whitespace are ignored in search. + * @returns {Object} listing properties of function + */ +export function help(operations, searchTerm) { + if (typeof searchTerm === "string") { + const operation = operations[Object.keys(operations).find(o => + o.toLowerCase() === searchTerm.replace(/ /g, "").toLowerCase())]; + if (operation) { + return extractOperationInfo(operation); + } + } + return null; +} + + +/** + * SomeName => someName + * @param {String} name - string to be altered + * @returns {String} decapitalised + */ +export function decapitalise(name) { + return `${name.charAt(0).toLowerCase()}${name.substr(1)}`; +} diff --git a/src/node/index.mjs b/src/node/index.mjs index 8900fbd4..1125685a 100644 --- a/src/node/index.mjs +++ b/src/node/index.mjs @@ -2,11 +2,14 @@ * Node view for CyberChef. * * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2017 + * @copyright Crown Copyright 2018 * @license Apache-2.0 */ import "babel-polyfill"; +import {wrap, help, decapitalise} from "./apiUtils"; +import * as operations from "../core/operations/index"; + // Define global environment functions global.ENVIRONMENT_IS_WORKER = function() { return typeof importScripts === "function"; @@ -19,24 +22,13 @@ global.ENVIRONMENT_IS_WEB = function() { }; -import wrap from "./Wrapper"; - -import * as operations from "../core/operations/index"; - -/** - * - * @param name - */ -function decapitalise(name) { - return `${name.charAt(0).toLowerCase()}${name.substr(1)}`; -} - - -// console.log(operations); const chef = {}; + +// Add in wrapped operations with camelCase names Object.keys(operations).forEach(op => chef[decapitalise(op)] = wrap(operations[op])); +chef.help = help.bind(null, operations); export default chef; export {chef}; From d5b5443a840bad6d5dcfb290eb6a00e123baea71 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 27 Apr 2018 09:01:45 +0100 Subject: [PATCH 015/801] update readme --- .github/CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6064121c..e90196e5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -22,7 +22,7 @@ Before your contributions can be accepted, you must: * Line endings: UNIX style (\n) -## Design Principals +## Design Principles 1. If at all possible, all operations and features should be client-side and not rely on connections to an external server. This increases the utility of CyberChef on closed networks and in virtual machines that are not connected to the Internet. Calls to external APIs may be accepted if there is no other option, but not for critical components. 2. Latency should be kept to a minimum to enhance the user experience. This means that all operation code should sit on the client, rather than being loaded dynamically from a server. @@ -30,7 +30,7 @@ Before your contributions can be accepted, you must: 4. Minimise the use of large libraries, especially for niche operations that won't be used very often - these will be downloaded by everyone using the app, whether they use that operation or not (due to principal 2). -With these principals in mind, any changes or additions to CyberChef should keep it: +With these principles in mind, any changes or additions to CyberChef should keep it: - Standalone - Efficient From fbec0a1c7d5da7db3257dc8de6507284663c332d Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sat, 21 Apr 2018 12:25:48 +0100 Subject: [PATCH 016/801] The raw, unpresented dish is now returned to the app after baking, where it can be retrieved as various different data types. --- src/core/Chef.mjs | 22 ++++++- src/core/ChefWorker.js | 19 ++++++ src/core/Dish.mjs | 17 +++-- src/core/FlowControl.js | 4 +- src/core/Recipe.mjs | 26 +++++--- src/core/Utils.mjs | 37 ++++------- src/core/config/scripts/generateConfig.mjs | 2 +- src/web/HighlighterWaiter.js | 4 +- src/web/Manager.js | 1 - src/web/OutputWaiter.js | 75 ++++++++++++++-------- src/web/WorkerWaiter.js | 28 ++++++++ src/web/stylesheets/utils/_overrides.css | 1 + 12 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs index a935d75c..79172479 100755 --- a/src/core/Chef.mjs +++ b/src/core/Chef.mjs @@ -89,7 +89,14 @@ class Chef { const threshold = (options.ioDisplayThreshold || 1024) * 1024; const returnType = this.dish.size > threshold ? Dish.ARRAY_BUFFER : Dish.STRING; + // Create a raw version of the dish, unpresented + const rawDish = new Dish(this.dish); + + // Present the raw result + await recipe.present(this.dish); + return { + dish: rawDish, result: this.dish.type === Dish.HTML ? await this.dish.get(Dish.HTML, notUTF8) : await this.dish.get(returnType, notUTF8), @@ -123,7 +130,7 @@ class Chef { const startTime = new Date().getTime(), recipe = new Recipe(recipeConfig), - dish = new Dish("", Dish.STRING); + dish = new Dish(); try { recipe.execute(dish); @@ -167,6 +174,19 @@ class Chef { }; } + + /** + * Translates the dish to a specified type and returns it. + * + * @param {Dish} dish + * @param {string} type + * @returns {Dish} + */ + async getDishAs(dish, type) { + const newDish = new Dish(dish); + return await newDish.get(type); + } + } export default Chef; diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index 604189e7..dbbda126 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -60,6 +60,9 @@ self.addEventListener("message", function(e) { case "silentBake": silentBake(r.data); break; + case "getDishAs": + getDishAs(r.data); + break; case "docURL": // Used to set the URL of the current document so that scripts can be // imported into an inline worker. @@ -125,6 +128,22 @@ function silentBake(data) { } +/** + * Translates the dish to a given type. + */ +async function getDishAs(data) { + const value = await self.chef.getDishAs(data.dish, data.type); + + self.postMessage({ + action: "dishReturned", + data: { + value: value, + id: data.id + } + }); +} + + /** * Checks that all required modules are loaded and loads them if not. * diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 08c7206a..888432da 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -17,14 +17,17 @@ class Dish { /** * Dish constructor * - * @param {byteArray|string|number|ArrayBuffer|BigNumber} [value=null] - * - The value of the input data. - * @param {number} [type=Dish.BYTE_ARRAY] - * - The data type of value, see Dish enums. + * @param {Dish} [dish=null] - A dish to clone */ - constructor(value=null, type=Dish.BYTE_ARRAY) { - this.value = value; - this.type = type; + constructor(dish=null) { + this.value = []; + this.type = Dish.BYTE_ARRAY; + + if (dish && + dish.hasOwnProperty("value") && + dish.hasOwnProperty("type")) { + this.set(dish.value, dish.type); + } } diff --git a/src/core/FlowControl.js b/src/core/FlowControl.js index 92440c49..f1bb8dab 100755 --- a/src/core/FlowControl.js +++ b/src/core/FlowControl.js @@ -68,7 +68,9 @@ const FlowControl = { op.ingValues = JSON.parse(JSON.stringify(ingValues[i])); }); - const dish = new Dish(inputs[i], inputType); + const dish = new Dish(); + dish.set(inputs[i], inputType); + try { progress = await recipe.execute(dish, 0, state); } catch (err) { diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index 006e431c..95dab22b 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -130,10 +130,12 @@ class Recipe { * - The final progress through the recipe */ async execute(dish, startFrom=0, forkState={}) { - let op, input, output, lastRunOp, + let op, input, output, numJumps = 0, numRegisters = forkState.numRegisters || 0; + if (startFrom === 0) this.lastRunOp = null; + log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`); for (let i = startFrom; i < this.opList.length; i++) { @@ -169,7 +171,7 @@ class Recipe { numRegisters = state.numRegisters; } else { output = await op.run(input, op.ingValues); - lastRunOp = op; + this.lastRunOp = op; dish.set(output, op.outputType); } } catch (err) { @@ -188,18 +190,24 @@ class Recipe { } } - // Present the results of the final operation - if (lastRunOp) { - // TODO try/catch - output = await lastRunOp.present(output); - dish.set(output, lastRunOp.presentType); - } - log.debug("Recipe complete"); return this.opList.length; } + /** + * Present the results of the final operation. + * + * @param {Dish} dish + */ + async present(dish) { + if (!this.lastRunOp) return; + + const output = await this.lastRunOp.present(await dish.get(this.lastRunOp.outputType)); + dish.set(output, this.lastRunOp.presentType); + } + + /** * Returns the recipe configuration in string format. * diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index 88cfa52e..f90bc394 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -7,7 +7,7 @@ import utf8 from "utf8"; import moment from "moment-timezone"; import {fromBase64} from "./lib/Base64"; -import {toHexFast, fromHex} from "./lib/Hex"; +import {fromHex} from "./lib/Hex"; /** @@ -833,39 +833,24 @@ class Utils { const formatFile = async function(file, i) { const buff = await Utils.readFile(file); - const fileStr = Utils.arrayBufferToStr(buff.buffer); const blob = new Blob( [buff], {type: "octet/stream"} ); - const blobUrl = URL.createObjectURL(blob); - - const viewFileElem = ``; - - const downloadFileElem = `💾`; - - const hexFileData = toHexFast(buff); - - const switchToInputElem = ``; const html = `
`; break; + case "argSelector": + html += `
+ + + ${this.hint ? "" + this.hint + "" : ""} +
`; + + this.manager.addDynamicListener(".arg-selector", "change", this.argSelectorChange, this); + break; default: break; } @@ -321,6 +342,33 @@ class HTMLIngredient { this.manager.recipe.ingChange(); } + + /** + * Handler for argument selector changes. + * Shows or hides the relevant arguments for this operation. + * + * @param {event} e + */ + argSelectorChange(e) { + e.preventDefault(); + e.stopPropagation(); + + const option = e.target.options[e.target.selectedIndex]; + const op = e.target.closest(".operation"); + const args = op.querySelectorAll(".ingredients .form-group"); + const turnon = JSON.parse(option.getAttribute("turnon")); + const turnoff = JSON.parse(option.getAttribute("turnoff")); + + args.forEach((arg, i) => { + if (turnon.includes(i)) { + arg.classList.remove("d-none"); + } + if (turnoff.includes(i)) { + arg.classList.add("d-none"); + } + }); + } + } export default HTMLIngredient; diff --git a/src/web/RecipeWaiter.mjs b/src/web/RecipeWaiter.mjs index 4c568c8b..4eca4af7 100755 --- a/src/web/RecipeWaiter.mjs +++ b/src/web/RecipeWaiter.mjs @@ -393,15 +393,6 @@ class RecipeWaiter { this.buildRecipeOperation(item); document.getElementById("rec-list").appendChild(item); - // Trigger populateOption events - const populateOptions = item.querySelectorAll(".populate-option"); - const evt = new Event("change", {bubbles: true}); - if (populateOptions.length) { - for (const el of populateOptions) { - el.dispatchEvent(evt); - } - } - item.dispatchEvent(this.manager.operationadd); return item; } @@ -439,6 +430,23 @@ class RecipeWaiter { } + /** + * Triggers various change events for operation arguments that have just been initialised. + * + * @param {HTMLElement} op + */ + triggerArgEvents(op) { + // Trigger populateOption and argSelector events + const triggerableOptions = op.querySelectorAll(".populate-option, .arg-selector"); + const evt = new Event("change", {bubbles: true}); + if (triggerableOptions.length) { + for (const el of triggerableOptions) { + el.dispatchEvent(evt); + } + } + } + + /** * Handler for operationadd events. * @@ -448,6 +456,8 @@ class RecipeWaiter { */ opAdd(e) { log.debug(`'${e.target.querySelector(".op-title").textContent}' added to recipe`); + + this.triggerArgEvents(e.target); window.dispatchEvent(this.manager.statechange); } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 0f00f1be..9e5a79c6 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -16,10 +16,11 @@ TestRegister.addTests([ { "op": "Bombe", "args": [ - "BDFHJLCPRTXVZNYEIWGAKMUSQO Date: Thu, 28 Feb 2019 16:56:28 +0000 Subject: [PATCH 312/801] Tweaks for new rotor order --- src/core/lib/Enigma.mjs | 1 - src/core/operations/Bombe.mjs | 2 +- src/core/operations/Enigma.mjs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 1ed0ea2b..39193f69 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -25,7 +25,6 @@ export const ROTORS = [ ]; export const ROTORS_FOURTH = [ - {name: "None", value: ""}, {name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"}, {name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"}, ]; diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index ea3210fa..5e128498 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -44,7 +44,7 @@ class Bombe extends Operation { ] }, { - name: "Left-most rotor", + name: "Left-most (4th) rotor", type: "editableOption", value: ROTORS_FOURTH, defaultIndex: 0 diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index ace50604..77333b18 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -42,7 +42,7 @@ class Enigma extends Operation { ] }, { - name: "Left-most rotor", + name: "Left-most (4th) rotor", type: "editableOption", value: ROTORS_FOURTH, defaultIndex: 0 From 1f9fd92b01db91518855039a41aee470daf3608f Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 17:21:47 +0000 Subject: [PATCH 313/801] Typex: rotors in same order as Enigma --- src/core/operations/Typex.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 504cb891..9c963357 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -29,10 +29,10 @@ class Typex extends Operation { this.outputType = "string"; this.args = [ { - name: "1st (right-hand, static) rotor", + name: "1st (left-hand) rotor", type: "editableOption", value: ROTORS, - defaultIndex: 4 + defaultIndex: 0 }, { name: "1st rotor reversed", @@ -50,10 +50,10 @@ class Typex extends Operation { value: LETTERS }, { - name: "2nd (static) rotor", + name: "2nd rotor", type: "editableOption", value: ROTORS, - defaultIndex: 3 + defaultIndex: 1 }, { name: "2nd rotor reversed", @@ -71,7 +71,7 @@ class Typex extends Operation { value: LETTERS }, { - name: "3rd rotor", + name: "3rd (middle) rotor", type: "editableOption", value: ROTORS, defaultIndex: 2 @@ -92,10 +92,10 @@ class Typex extends Operation { value: LETTERS }, { - name: "4th rotor", + name: "4th (static) rotor", type: "editableOption", value: ROTORS, - defaultIndex: 1 + defaultIndex: 3 }, { name: "4th rotor reversed", @@ -113,10 +113,10 @@ class Typex extends Operation { value: LETTERS }, { - name: "5th rotor", + name: "5th (right-hand, static) rotor", type: "editableOption", value: ROTORS, - defaultIndex: 0 + defaultIndex: 4 }, { name: "5th rotor reversed", @@ -190,6 +190,8 @@ class Typex extends Operation { const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*4]); rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*4 + 1], args[i*4+2], args[i*4+3])); } + // Rotors are handled in reverse + rotors.reverse(); const reflector = new Reflector(reflectorstr); let plugboardstrMod = plugboardstr; if (plugboardstrMod === "") { From 765aded208b7f87ffef92d30294ac3edca763a2a Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 17:22:09 +0000 Subject: [PATCH 314/801] Typex: add simple tests --- tests/operations/index.mjs | 1 + tests/operations/tests/Typex.mjs | 105 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tests/operations/tests/Typex.mjs diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index ff967163..cff77217 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -86,6 +86,7 @@ import "./tests/ConvertCoordinateFormat"; import "./tests/Enigma"; import "./tests/Bombe"; import "./tests/MultipleBombe"; +import "./tests/Typex"; // Cannot test operations that use the File type yet //import "./tests/SplitColourChannels"; diff --git a/tests/operations/tests/Typex.mjs b/tests/operations/tests/Typex.mjs new file mode 100644 index 00000000..e3751e8a --- /dev/null +++ b/tests/operations/tests/Typex.mjs @@ -0,0 +1,105 @@ +/** + * Typex machine tests. + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + // Unlike Enigma we're not verifying against a real machine here, so this is just a test + // to catch inadvertent breakage. + name: "Typex: basic", + input: "hello world, this is a test message.", + expectedOutput: "VIXQQ VHLPN UCVLA QDZNZ EAYAT HWC", + recipeConfig: [ + { + "op": "Typex", + "args": [ + "MCYLPQUVRXGSAOWNBJEZDTFKHI Date: Thu, 28 Feb 2019 17:50:10 +0000 Subject: [PATCH 315/801] Add some files that escaped commit before --- package.json | 1 + webpack.config.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cb59db38..64ef09cc 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "sass-loader": "^7.1.0", "sitemap": "^2.1.0", "style-loader": "^0.23.1", + "svg-url-loader": "^2.3.2", "url-loader": "^1.1.2", "web-resource-inliner": "^4.2.1", "webpack": "^4.28.3", diff --git a/webpack.config.js b/webpack.config.js index 054152b2..e2a7c728 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -100,8 +100,15 @@ module.exports = { limit: 10000 } }, + { + test: /\.svg$/, + loader: "svg-url-loader", + options: { + encoding: "base64" + } + }, { // First party images are saved as files to be cached - test: /\.(png|jpg|gif|svg)$/, + test: /\.(png|jpg|gif)$/, exclude: /node_modules/, loader: "file-loader", options: { @@ -109,7 +116,7 @@ module.exports = { } }, { // Third party images are inlined - test: /\.(png|jpg|gif|svg)$/, + test: /\.(png|jpg|gif)$/, exclude: /web\/static/, loader: "url-loader", options: { From 9323737d1da3dc7b9a2d4f485e456829e7aa0e98 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:37:48 +0000 Subject: [PATCH 316/801] Bombe: fix rotor listing order for multibombe --- src/core/operations/MultipleBombe.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 7a0ae2fd..6887bc46 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -291,7 +291,7 @@ class MultipleBombe extends Operation { let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n`; for (const run of output.bombeRuns) { - html += `\nRotors: ${run.rotors.join(", ")}\nReflector: ${run.reflector}\n`; + html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`; html += ""; for (const [setting, stecker, decrypt] of run.result) { html += `\n`; From a446ec31c712d4a820e2cd484bed97b2a71c9e83 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:48:36 +0000 Subject: [PATCH 317/801] Improve Enigma/Bombe descriptions a little. --- src/core/operations/Bombe.mjs | 2 +- src/core/operations/Enigma.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 5e128498..f0d7048c 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -23,7 +23,7 @@ class Bombe extends Operation { this.name = "Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; + this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "JSON"; diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index 77333b18..71593070 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -22,7 +22,7 @@ class Enigma extends Operation { this.name = "Enigma"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Enigma machine.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses the thin reflectors and the beta or gamma rotor in the 4th slot)."; + this.description = "Encipher/decipher with the WW2 Enigma machine.

Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot)."; this.infoURL = "https://wikipedia.org/wiki/Enigma_machine"; this.inputType = "string"; this.outputType = "string"; From 9a0b78415360c5d7c6ccf9ea025bedbea74f0d41 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:56:59 +0000 Subject: [PATCH 318/801] Typex: improve operation description --- src/core/operations/Typex.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 9c963357..760914f5 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -23,7 +23,7 @@ class Typex extends Operation { this.name = "Typex"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Typex machine.

Typex rotors were changed regularly and none are public: a random example set are provided. Later Typexes had a reflector which could be configured with a plugboard: to configure this, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). These Typexes also have an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points."; + this.description = "Encipher/decipher with the WW2 Typex machine.

Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.

To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points."; this.infoURL = "https://wikipedia.org/wiki/Typex"; this.inputType = "string"; this.outputType = "string"; From 0a1ca18de53a0527175ee57e6e7f76f96e92dc3d Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 1 Mar 2019 08:59:18 +0000 Subject: [PATCH 319/801] refactor Dish get to handle sync and async --- src/core/Dish.mjs | 455 +++++++++++++++++++++++----------------------- 1 file changed, 229 insertions(+), 226 deletions(-) diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 17a6d2a0..64602181 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -11,228 +11,6 @@ import BigNumber from "bignumber.js"; import log from "loglevel"; -/** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - */ -async function _asyncTranslate(toType, notUTF8=false) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - try { - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - case Dish.JSON: - this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; - break; - case Dish.FILE: - this.value = await Utils.readFile(this.value); - this.value = Array.prototype.slice.call(this.value); - break; - case Dish.LIST_FILE: - this.value = await Promise.all(this.value.map(async f => Utils.readFile(f))); - this.value = this.value.map(b => Array.prototype.slice.call(b)); - this.value = [].concat.apply([], this.value); - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - try { - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - case Dish.JSON: - this.value = JSON.parse(byteArrayToStr(this.value)); - this.type = Dish.JSON; - break; - case Dish.FILE: - this.value = new File(this.value, "unknown"); - break; - case Dish.LIST_FILE: - this.value = [new File(this.value, "unknown")]; - this.type = Dish.LIST_FILE; - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); - } -} - - -/** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - */ -function _translate(toType, notUTF8=false) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - try { - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - case Dish.JSON: - this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; - break; - case Dish.FILE: - this.value = Utils.readFileSync(this.value); - this.value = Array.prototype.slice.call(this.value); - break; - case Dish.LIST_FILE: - this.value = this.value.map(f => Utils.readFileSync(f)); - this.value = this.value.map(b => Array.prototype.slice.call(b)); - this.value = [].concat.apply([], this.value); - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - try { - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - case Dish.JSON: - this.value = JSON.parse(byteArrayToStr(this.value)); - this.type = Dish.JSON; - break; - case Dish.FILE: - this.value = new File(this.value, "unknown"); - break; - case Dish.LIST_FILE: - this.value = [new File(this.value, "unknown")]; - this.type = Dish.LIST_FILE; - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); - } -} - -/** - * Returns the value of the data in the type format specified. - * - * @param {number} type - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - * @returns {*} - The value of the output data. - */ -async function asyncGet(type, notUTF8=false) { - if (typeof type === "string") { - type = Dish.typeEnum(type); - } - if (this.type !== type) { - await this._translate(type, notUTF8); - } - return this.value; -} - -/** - * Returns the value of the data in the type format specified. - * - * @param {number} type - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - * @returns {*} - The value of the output data. - */ -function get(type, notUTF8=false) { - if (typeof type === "string") { - type = Dish.typeEnum(type); - } - if (this.type !== type) { - this._translate(type, notUTF8); - } - return this.value; -} - - /** * The data being operated on by each operation. */ @@ -335,6 +113,41 @@ class Dish { } + /** + * Returns the value of the data in the type format specified. + * + * @param {number} type - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + * @returns {* | Promise} - (Broswer) A promise | (Node) value of dish in given type + */ + get(type, notUTF8=false) { + if (typeof type === "string") { + type = Dish.typeEnum(type); + } + + if (this.type !== type) { + + // Browser environment => _translate is async + if (Utils.isBrowser()) { + return new Promise((resolve, reject) => { + this._translate(type, notUTF8) + .then(() => { + resolve(this.value); + }) + .catch(reject); + }); + + // Node environment => _translate is sync + } else { + this._translate(type, notUTF8); + return this.value; + } + } + + return this.value; + } + + /** * Sets the data value and type and then validates them. * @@ -553,12 +366,202 @@ Dish.FILE = 7; */ Dish.LIST_FILE = 8; + + + +/** + * Translates the data to the given type format. + * + * @param {number} toType - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + */ +async function _asyncTranslate(toType, notUTF8=false) { + log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); + const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; + + // Convert data to intermediate byteArray type + try { + switch (this.type) { + case Dish.STRING: + this.value = this.value ? Utils.strToByteArray(this.value) : []; + break; + case Dish.NUMBER: + this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; + break; + case Dish.HTML: + this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; + break; + case Dish.ARRAY_BUFFER: + // Array.from() would be nicer here, but it's slightly slower + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); + break; + case Dish.BIG_NUMBER: + this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; + break; + case Dish.JSON: + this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; + break; + case Dish.FILE: + this.value = await Utils.readFile(this.value); + this.value = Array.prototype.slice.call(this.value); + break; + case Dish.LIST_FILE: + this.value = await Promise.all(this.value.map(async f => Utils.readFile(f))); + this.value = this.value.map(b => Array.prototype.slice.call(b)); + this.value = [].concat.apply([], this.value); + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); + } + + this.type = Dish.BYTE_ARRAY; + + // Convert from byteArray to toType + try { + switch (toType) { + case Dish.STRING: + case Dish.HTML: + this.value = this.value ? byteArrayToStr(this.value) : ""; + this.type = Dish.STRING; + break; + case Dish.NUMBER: + this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; + this.type = Dish.NUMBER; + break; + case Dish.ARRAY_BUFFER: + this.value = new Uint8Array(this.value).buffer; + this.type = Dish.ARRAY_BUFFER; + break; + case Dish.BIG_NUMBER: + try { + this.value = new BigNumber(byteArrayToStr(this.value)); + } catch (err) { + this.value = new BigNumber(NaN); + } + this.type = Dish.BIG_NUMBER; + break; + case Dish.JSON: + this.value = JSON.parse(byteArrayToStr(this.value)); + this.type = Dish.JSON; + break; + case Dish.FILE: + this.value = new File(this.value, "unknown"); + break; + case Dish.LIST_FILE: + this.value = [new File(this.value, "unknown")]; + this.type = Dish.LIST_FILE; + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); + } +} + + +/** + * Translates the data to the given type format. + * + * @param {number} toType - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + */ +function _translate(toType, notUTF8=false) { + log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); + const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; + + // Convert data to intermediate byteArray type + try { + switch (this.type) { + case Dish.STRING: + this.value = this.value ? Utils.strToByteArray(this.value) : []; + break; + case Dish.NUMBER: + this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; + break; + case Dish.HTML: + this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; + break; + case Dish.ARRAY_BUFFER: + // Array.from() would be nicer here, but it's slightly slower + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); + break; + case Dish.BIG_NUMBER: + this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; + break; + case Dish.JSON: + this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; + break; + case Dish.FILE: + this.value = Utils.readFileSync(this.value); + this.value = Array.prototype.slice.call(this.value); + break; + case Dish.LIST_FILE: + this.value = this.value.map(f => Utils.readFileSync(f)); + this.value = this.value.map(b => Array.prototype.slice.call(b)); + this.value = [].concat.apply([], this.value); + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); + } + + this.type = Dish.BYTE_ARRAY; + + // Convert from byteArray to toType + try { + switch (toType) { + case Dish.STRING: + case Dish.HTML: + this.value = this.value ? byteArrayToStr(this.value) : ""; + this.type = Dish.STRING; + break; + case Dish.NUMBER: + this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; + this.type = Dish.NUMBER; + break; + case Dish.ARRAY_BUFFER: + this.value = new Uint8Array(this.value).buffer; + this.type = Dish.ARRAY_BUFFER; + break; + case Dish.BIG_NUMBER: + try { + this.value = new BigNumber(byteArrayToStr(this.value)); + } catch (err) { + this.value = new BigNumber(NaN); + } + this.type = Dish.BIG_NUMBER; + break; + case Dish.JSON: + this.value = JSON.parse(byteArrayToStr(this.value)); + this.type = Dish.JSON; + break; + case Dish.FILE: + this.value = new File(this.value, "unknown"); + break; + case Dish.LIST_FILE: + this.value = [new File(this.value, "unknown")]; + this.type = Dish.LIST_FILE; + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); + } +} + + if (Utils.isBrowser()) { - Dish.prototype._translate = _asyncTranslate - Dish.prototype.get = asyncGet + Dish.prototype._translate = _asyncTranslate; + } else { - Dish.prototype._translate = _translate - Dish.prototype.get = get + Dish.prototype._translate = _translate; } export default Dish; From b48c16b4db221c3a4ea03c740b35a40b552424ee Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 1 Mar 2019 16:02:21 +0000 Subject: [PATCH 320/801] Refactor Dish _translate to handle sync and async depending on environment. --- Gruntfile.js | 3 - src/core/Dish.mjs | 326 +++++++----------- src/core/Utils.mjs | 20 +- .../dishTranslationTypes/DishArrayBuffer.mjs | 32 ++ .../dishTranslationTypes/DishBigNumber.mjs | 40 +++ src/core/dishTranslationTypes/DishFile.mjs | 44 +++ src/core/dishTranslationTypes/DishHTML.mjs | 35 ++ src/core/dishTranslationTypes/DishJSON.mjs | 34 ++ .../dishTranslationTypes/DishListFile.mjs | 42 +++ src/core/dishTranslationTypes/DishNumber.mjs | 34 ++ src/core/dishTranslationTypes/DishString.mjs | 34 ++ .../DishTranslationType.mjs | 39 +++ src/core/dishTranslationTypes/index.mjs | 26 ++ .../tests/ConvertCoordinateFormat.mjs | 2 +- .../tests/ToFromInsensitiveRegex.mjs | 2 +- tests/operations/tests/YARA.mjs | 2 +- 16 files changed, 492 insertions(+), 223 deletions(-) create mode 100644 src/core/dishTranslationTypes/DishArrayBuffer.mjs create mode 100644 src/core/dishTranslationTypes/DishBigNumber.mjs create mode 100644 src/core/dishTranslationTypes/DishFile.mjs create mode 100644 src/core/dishTranslationTypes/DishHTML.mjs create mode 100644 src/core/dishTranslationTypes/DishJSON.mjs create mode 100644 src/core/dishTranslationTypes/DishListFile.mjs create mode 100644 src/core/dishTranslationTypes/DishNumber.mjs create mode 100644 src/core/dishTranslationTypes/DishString.mjs create mode 100644 src/core/dishTranslationTypes/DishTranslationType.mjs create mode 100644 src/core/dishTranslationTypes/index.mjs diff --git a/Gruntfile.js b/Gruntfile.js index a14c3001..30b9fa8c 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -365,9 +365,6 @@ module.exports = function (grunt) { "./config/modules/OpModules": "./config/modules/Default" } }, - output: { - // globalObject: "this", - }, plugins: [ new webpack.DefinePlugin(BUILD_CONSTANTS), new HtmlWebpackPlugin({ diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 64602181..452be80d 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -10,6 +10,17 @@ import DishError from "./errors/DishError"; import BigNumber from "bignumber.js"; import log from "loglevel"; +import { + DishArrayBuffer, + DishBigNumber, + DishFile, + DishHTML, + DishJSON, + DishListFile, + DishNumber, + DishString, +} from "./dishTranslationTypes"; + /** * The data being operated on by each operation. @@ -116,6 +127,8 @@ class Dish { /** * Returns the value of the data in the type format specified. * + * If running in a browser, get is asynchronous. + * * @param {number} type - The data type of value, see Dish enums. * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. * @returns {* | Promise} - (Broswer) A promise | (Node) value of dish in given type @@ -127,8 +140,13 @@ class Dish { if (this.type !== type) { + // Node environment => _translate is sync + if (Utils.isNode()) { + this._translate(type, notUTF8); + return this.value; + // Browser environment => _translate is async - if (Utils.isBrowser()) { + } else { return new Promise((resolve, reject) => { this._translate(type, notUTF8) .then(() => { @@ -136,11 +154,6 @@ class Dish { }) .catch(reject); }); - - // Node environment => _translate is sync - } else { - this._translate(type, notUTF8); - return this.value; } } @@ -308,6 +321,110 @@ class Dish { return newDish; } + + /** + * Translates the data to the given type format. + * + * If running in the browser, _translate is asynchronous. + * + * @param {number} toType - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + * @returns {Promise || undefined} + */ + _translate(toType, notUTF8=false) { + log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); + + // Node environment => translate is sync + if (Utils.isNode()) { + this._toByteArray(); + this._fromByteArray(toType, notUTF8); + + // Browser environment => translate is async + } else { + return new Promise((resolve, reject) => { + this._toByteArray() + .then(() => this.type = Dish.BYTE_ARRAY) + .then(() => { + this._fromByteArray(toType); + resolve(); + }) + .catch(reject); + }); + } + + } + + /** + * Convert this.value to a ByteArray + * + * If running in a browser, _toByteArray is asynchronous. + * + * @returns {Promise || undefined} + */ + _toByteArray() { + // Using 'bind' here to allow this.value to be mutated within translation functions + const toByteArrayFuncs = { + browser: { + [Dish.STRING]: () => Promise.resolve(DishString.toByteArray.bind(this)()), + [Dish.NUMBER]: () => Promise.resolve(DishNumber.toByteArray.bind(this)()), + [Dish.HTML]: () => Promise.resolve(DishHTML.toByteArray.bind(this)()), + [Dish.ARRAY_BUFFER]: () => Promise.resolve(DishArrayBuffer.toByteArray.bind(this)()), + [Dish.BIG_NUMBER]: () => Promise.resolve(DishBigNumber.toByteArray.bind(this)()), + [Dish.JSON]: () => Promise.resolve(DishJSON.toByteArray.bind(this)()), + [Dish.FILE]: () => DishFile.toByteArray.bind(this)(), + [Dish.LIST_FILE]: () => DishListFile.toByteArray.bind(this)(), + [Dish.BYTE_ARRAY]: () => Promise.resolve(), + }, + node: { + [Dish.STRING]: () => DishString.toByteArray.bind(this)(), + [Dish.NUMBER]: () => DishNumber.toByteArray.bind(this)(), + [Dish.HTML]: () => DishHTML.toByteArray.bind(this)(), + [Dish.ARRAY_BUFFER]: () => DishArrayBuffer.toByteArray.bind(this)(), + [Dish.BIG_NUMBER]: () => DishBigNumber.toByteArray.bind(this)(), + [Dish.JSON]: () => DishJSON.toByteArray.bind(this)(), + [Dish.FILE]: () => DishFile.toByteArray.bind(this)(), + [Dish.LIST_FILE]: () => DishListFile.toByteArray.bind(this)(), + [Dish.BYTE_ARRAY]: () => {}, + } + }; + + try { + return toByteArrayFuncs[Utils.isNode() && "node" || "browser"][this.type](); + } catch (err) { + throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); + } + } + + /** + * Convert this.value to the given type. + * + * @param {number} toType - the Dish enum to convert to + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + */ + _fromByteArray(toType, notUTF8) { + const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; + + // Using 'bind' here to allow this.value to be mutated within translation functions + const toTypeFunctions = { + [Dish.STRING]: () => DishString.fromByteArray.bind(this)(byteArrayToStr), + [Dish.NUMBER]: () => DishNumber.fromByteArray.bind(this)(byteArrayToStr), + [Dish.HTML]: () => DishHTML.fromByteArray.bind(this)(byteArrayToStr), + [Dish.ARRAY_BUFFER]: () => DishArrayBuffer.fromByteArray.bind(this)(), + [Dish.BIG_NUMBER]: () => DishBigNumber.fromByteArray.bind(this)(byteArrayToStr), + [Dish.JSON]: () => DishJSON.fromByteArray.bind(this)(byteArrayToStr), + [Dish.FILE]: () => DishFile.fromByteArray.bind(this)(), + [Dish.LIST_FILE]: () => DishListFile.fromByteArray.bind(this)(), + [Dish.BYTE_ARRAY]: () => {}, + }; + + try { + toTypeFunctions[toType](); + this.type = toType; + } catch (err) { + throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); + } + } + } @@ -367,201 +484,4 @@ Dish.FILE = 7; Dish.LIST_FILE = 8; - - -/** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - */ -async function _asyncTranslate(toType, notUTF8=false) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - try { - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - case Dish.JSON: - this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; - break; - case Dish.FILE: - this.value = await Utils.readFile(this.value); - this.value = Array.prototype.slice.call(this.value); - break; - case Dish.LIST_FILE: - this.value = await Promise.all(this.value.map(async f => Utils.readFile(f))); - this.value = this.value.map(b => Array.prototype.slice.call(b)); - this.value = [].concat.apply([], this.value); - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - try { - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - case Dish.JSON: - this.value = JSON.parse(byteArrayToStr(this.value)); - this.type = Dish.JSON; - break; - case Dish.FILE: - this.value = new File(this.value, "unknown"); - break; - case Dish.LIST_FILE: - this.value = [new File(this.value, "unknown")]; - this.type = Dish.LIST_FILE; - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); - } -} - - -/** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - */ -function _translate(toType, notUTF8=false) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - try { - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - case Dish.JSON: - this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; - break; - case Dish.FILE: - this.value = Utils.readFileSync(this.value); - this.value = Array.prototype.slice.call(this.value); - break; - case Dish.LIST_FILE: - this.value = this.value.map(f => Utils.readFileSync(f)); - this.value = this.value.map(b => Array.prototype.slice.call(b)); - this.value = [].concat.apply([], this.value); - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - try { - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - case Dish.JSON: - this.value = JSON.parse(byteArrayToStr(this.value)); - this.type = Dish.JSON; - break; - case Dish.FILE: - this.value = new File(this.value, "unknown"); - break; - case Dish.LIST_FILE: - this.value = [new File(this.value, "unknown")]; - this.type = Dish.LIST_FILE; - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); - } -} - - -if (Utils.isBrowser()) { - Dish.prototype._translate = _asyncTranslate; - -} else { - Dish.prototype._translate = _translate; -} - export default Dish; diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index c4cd14ab..934186ab 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -926,7 +926,11 @@ class Utils { * await Utils.readFile(new File(["hello"], "test")) */ static readFile(file) { - if (Utils.isBrowser()) { + + if (Utils.isNode()) { + return Buffer.from(file).buffer; + + } else { return new Promise((resolve, reject) => { const reader = new FileReader(); const data = new Uint8Array(file.size); @@ -954,17 +958,12 @@ class Utils { seek(); }); - - } else if (Utils.isNode()) { - return Buffer.from(file).buffer; } - - throw new Error("Unkown environment!"); } /** */ static readFileSync(file) { - if (Utils.isBrowser()) { + if (!Utils.isNode()) { throw new TypeError("Browser environment cannot support readFileSync"); } @@ -1065,13 +1064,6 @@ class Utils { }[token]; } - /** - * Check if code is running in a browser environment - */ - static isBrowser() { - return typeof window !== "undefined" && typeof window.document !== "undefined"; - } - /** * Check if code is running in a Node environment */ diff --git a/src/core/dishTranslationTypes/DishArrayBuffer.mjs b/src/core/dishTranslationTypes/DishArrayBuffer.mjs new file mode 100644 index 00000000..96a8b8e3 --- /dev/null +++ b/src/core/dishTranslationTypes/DishArrayBuffer.mjs @@ -0,0 +1,32 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import DishTranslationType from "./DishTranslationType"; + +/** + * Translation methods for ArrayBuffer Dishes + */ +class DishArrayBuffer extends DishTranslationType { + + /** + * convert the given value to a ByteArray + */ + static toByteArray() { + DishArrayBuffer.checkForValue(this.value); + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); + } + + /** + * convert the given value from a ByteArray + * @param {function} byteArrayToStr + */ + static fromByteArray() { + DishArrayBuffer.checkForValue(this.value); + this.value = new Uint8Array(this.value).buffer; + } +} + +export default DishArrayBuffer; diff --git a/src/core/dishTranslationTypes/DishBigNumber.mjs b/src/core/dishTranslationTypes/DishBigNumber.mjs new file mode 100644 index 00000000..e2aae7b9 --- /dev/null +++ b/src/core/dishTranslationTypes/DishBigNumber.mjs @@ -0,0 +1,40 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; +import BigNumber from "bignumber.js"; + +/** + * translation methods for BigNumber Dishes + */ +class DishBigNumber extends DishTranslationType { + + /** + * convert the given value to a ByteArray + * @param {BigNumber} value + */ + static toByteArray() { + DishBigNumber.checkForValue(this.value); + this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; + } + + /** + * convert the given value from a ByteArray + * @param {ByteArray} value + * @param {function} byteArrayToStr + */ + static fromByteArray(byteArrayToStr) { + DishBigNumber.checkForValue(this.value); + try { + this.value = new BigNumber(byteArrayToStr(this.value)); + } catch (err) { + this.value = new BigNumber(NaN); + } + } +} + +export default DishBigNumber; diff --git a/src/core/dishTranslationTypes/DishFile.mjs b/src/core/dishTranslationTypes/DishFile.mjs new file mode 100644 index 00000000..11a9c871 --- /dev/null +++ b/src/core/dishTranslationTypes/DishFile.mjs @@ -0,0 +1,44 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; + +/** + * Translation methods for file Dishes + */ +class DishFile extends DishTranslationType { + + /** + * convert the given value to a ByteArray + * @param {File} value + */ + static toByteArray() { + DishFile.checkForValue(this.value); + if (Utils.isNode()) { + this.value = Array.prototype.slice.call(Utils.readFileSync(this.value)); + } else { + return new Promise((resolve, reject) => { + Utils.readFile(this.value) + .then(v => this.value = Array.prototype.slice.call(v)) + .then(resolve) + .catch(reject); + }); + } + } + + /** + * convert the given value from a ByteArray + * @param {ByteArray} value + * @param {function} byteArrayToStr + */ + static fromByteArray() { + DishFile.checkForValue(this.value); + this.value = new File(this.value, "unknown"); + } +} + +export default DishFile; diff --git a/src/core/dishTranslationTypes/DishHTML.mjs b/src/core/dishTranslationTypes/DishHTML.mjs new file mode 100644 index 00000000..f7a74d9e --- /dev/null +++ b/src/core/dishTranslationTypes/DishHTML.mjs @@ -0,0 +1,35 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; +import DishString from "./DishString"; + +/** + * Translation methods for HTML Dishes + */ +class DishHTML extends DishTranslationType { + + /** + * convert the given value to a ByteArray + * @param {String} value + */ + static toByteArray() { + DishHTML.checkForValue(this.value); + this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; + } + + /** + * convert the given value from a ByteArray + * @param {function} byteArrayToStr + */ + static fromByteArray(byteArrayToStr) { + DishHTML.checkForValue(this.value); + DishString.fromByteArray(this.value, byteArrayToStr); + } +} + +export default DishHTML; diff --git a/src/core/dishTranslationTypes/DishJSON.mjs b/src/core/dishTranslationTypes/DishJSON.mjs new file mode 100644 index 00000000..a2c20390 --- /dev/null +++ b/src/core/dishTranslationTypes/DishJSON.mjs @@ -0,0 +1,34 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; + +/** + * Translation methods for JSON dishes + */ +class DishJSON extends DishTranslationType { + + /** + * convert the given value to a ByteArray + */ + static toByteArray() { + DishJSON.checkForValue(this.value); + this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; + } + + /** + * convert the given value from a ByteArray + * @param {ByteArray} value + * @param {function} byteArrayToStr + */ + static fromByteArray(byteArrayToStr) { + DishJSON.checkForValue(this.value); + this.value = JSON.parse(byteArrayToStr(this.value)); + } +} + +export default DishJSON; diff --git a/src/core/dishTranslationTypes/DishListFile.mjs b/src/core/dishTranslationTypes/DishListFile.mjs new file mode 100644 index 00000000..678e2d59 --- /dev/null +++ b/src/core/dishTranslationTypes/DishListFile.mjs @@ -0,0 +1,42 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; + +/** + * Translation methods for ListFile Dishes + */ +class DishListFile extends DishTranslationType { + + /** + * convert the given value to a ByteArray + */ + static toByteArray() { + DishListFile.checkForValue(this.value); + if (Utils.isNode()) { + this.value = [].concat.apply([], this.value.map(f => Utils.readFileSync(f)).map(b => Array.prototype.slice.call(b))); + } else { + return new Promise((resolve, reject) => { + Promise.all(this.value.map(async f => Utils.readFile(f))) + .then(values => this.value = values.map(b => [].concat.apply([], Array.prototype.slice.call(b)))) + .then(resolve) + .catch(reject); + }); + } + } + + /** + * convert the given value from a ByteArray + * @param {function} byteArrayToStr + */ + static fromByteArray() { + DishListFile.checkForValue(this.value); + this.value = [new File(this.value, "unknown")]; + } +} + +export default DishListFile; diff --git a/src/core/dishTranslationTypes/DishNumber.mjs b/src/core/dishTranslationTypes/DishNumber.mjs new file mode 100644 index 00000000..0cc97af0 --- /dev/null +++ b/src/core/dishTranslationTypes/DishNumber.mjs @@ -0,0 +1,34 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; + +/** + * Translation methods for number dishes + */ +class DishNumber extends DishTranslationType { + + /** + * convert the given value to a ByteArray + */ + static toByteArray() { + DishNumber.checkForValue(this.value); + this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; + } + + /** + * convert the given value from a ByteArray + * @param {function} byteArrayToStr + */ + static fromByteArray(byteArrayToStr) { + DishNumber.checkForValue(this.value); + this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; + } +} + +export default DishNumber; diff --git a/src/core/dishTranslationTypes/DishString.mjs b/src/core/dishTranslationTypes/DishString.mjs new file mode 100644 index 00000000..78c273c6 --- /dev/null +++ b/src/core/dishTranslationTypes/DishString.mjs @@ -0,0 +1,34 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + + +import DishTranslationType from "./DishTranslationType"; +import Utils from "../Utils"; + +/** + * Translation methods for string dishes + */ +class DishString extends DishTranslationType { + + /** + * convert the given value to a ByteArray + */ + static toByteArray() { + DishString.checkForValue(this.value); + this.value = this.value ? Utils.strToByteArray(this.value) : []; + } + + /** + * convert the given value from a ByteArray + * @param {function} byteArrayToStr + */ + static fromByteArray(byteArrayToStr) { + DishString.checkForValue(this.value); + this.value = this.value ? byteArrayToStr(this.value) : ""; + } +} + +export default DishString; diff --git a/src/core/dishTranslationTypes/DishTranslationType.mjs b/src/core/dishTranslationTypes/DishTranslationType.mjs new file mode 100644 index 00000000..261f9bcd --- /dev/null +++ b/src/core/dishTranslationTypes/DishTranslationType.mjs @@ -0,0 +1,39 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + + +/** + * Abstract class for dish translation methods + */ +class DishTranslationType { + + /** + * Warn translations dont work without value from bind + */ + static checkForValue(value) { + if (value === undefined) { + throw new Error("only use translation methods with .bind"); + } + } + + /** + * convert the given value to a ByteArray + * @param {*} value + */ + static toByteArray() { + throw new Error("toByteArray has not been implemented"); + } + + /** + * convert the given value from a ByteArray + * @param {function} byteArrayToStr + */ + static fromByteArray(byteArrayToStr=undefined) { + throw new Error("toType has not been implemented"); + } +} + +export default DishTranslationType; diff --git a/src/core/dishTranslationTypes/index.mjs b/src/core/dishTranslationTypes/index.mjs new file mode 100644 index 00000000..8f6f920c --- /dev/null +++ b/src/core/dishTranslationTypes/index.mjs @@ -0,0 +1,26 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + + +import DishArrayBuffer from "./DishArrayBuffer"; +import DishBigNumber from "./DishBigNumber"; +import DishFile from "./DishFile"; +import DishHTML from "./DishHTML"; +import DishJSON from "./DishJSON"; +import DishListFile from "./DishListFile"; +import DishNumber from "./DishNumber"; +import DishString from "./DishString"; + +export { + DishArrayBuffer, + DishBigNumber, + DishFile, + DishHTML, + DishJSON, + DishListFile, + DishNumber, + DishString, +}; diff --git a/tests/operations/tests/ConvertCoordinateFormat.mjs b/tests/operations/tests/ConvertCoordinateFormat.mjs index 1291aa4d..80a336b7 100644 --- a/tests/operations/tests/ConvertCoordinateFormat.mjs +++ b/tests/operations/tests/ConvertCoordinateFormat.mjs @@ -18,7 +18,7 @@ * UTM: 30N 699456 5709791, */ -import TestRegister from "../TestRegister"; +import TestRegister from "../../lib/TestRegister"; TestRegister.addTests([ { diff --git a/tests/operations/tests/ToFromInsensitiveRegex.mjs b/tests/operations/tests/ToFromInsensitiveRegex.mjs index fa191951..0aaf89e2 100644 --- a/tests/operations/tests/ToFromInsensitiveRegex.mjs +++ b/tests/operations/tests/ToFromInsensitiveRegex.mjs @@ -6,7 +6,7 @@ * @copyright Crown Copyright 2018 * @license Apache-2.0 */ -import TestRegister from "../TestRegister"; +import TestRegister from "../../lib/TestRegister"; TestRegister.addTests([ { diff --git a/tests/operations/tests/YARA.mjs b/tests/operations/tests/YARA.mjs index e3c28ef1..5495ca69 100644 --- a/tests/operations/tests/YARA.mjs +++ b/tests/operations/tests/YARA.mjs @@ -6,7 +6,7 @@ * @copyright Crown Copyright 2019 * @license Apache-2.0 */ -import TestRegister from "../TestRegister"; +import TestRegister from "../../lib/TestRegister"; TestRegister.addTests([ { From 6d219ade2d47500c0176a293500e576750942fd9 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 1 Mar 2019 16:56:14 +0000 Subject: [PATCH 321/801] remove legacy async api from NodeRecipe --- src/node/NodeDish.mjs | 11 +++++++++++ src/node/NodeRecipe.mjs | 8 ++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/node/NodeDish.mjs b/src/node/NodeDish.mjs index bddc96e0..f619fdd8 100644 --- a/src/node/NodeDish.mjs +++ b/src/node/NodeDish.mjs @@ -30,6 +30,17 @@ class NodeDish extends Dish { super(inputOrDish, type); } + /** + * Apply the inputted operation to the dish. + * + * @param {WrappedOperation} operation the operation to perform + * @param {*} args - any arguments for the operation + * @returns {Dish} a new dish with the result of the operation. + */ + apply(operation, args=null) { + return operation(this.value, args); + } + /** * alias for get * @param args see get args diff --git a/src/node/NodeRecipe.mjs b/src/node/NodeRecipe.mjs index aa72fa6b..070c5433 100644 --- a/src/node/NodeRecipe.mjs +++ b/src/node/NodeRecipe.mjs @@ -76,14 +76,14 @@ class NodeRecipe { * @param {NodeDish} dish * @returns {NodeDish} */ - async execute(dish) { - return await this.opList.reduce(async (prev, curr) => { + execute(dish) { + return this.opList.reduce((prev, curr) => { // CASE where opLis item is op and args if (curr.hasOwnProperty("op") && curr.hasOwnProperty("args")) { - return await curr.op(prev, curr.args); + return curr.op(prev, curr.args); } // CASE opList item is just op. - return await curr(prev); + return curr(prev); }, dish); } } From e4b688a2c3ea192fa93a3c35ed35db32ec930eaf Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 1 Mar 2019 16:58:43 +0000 Subject: [PATCH 322/801] Make Frequency dist test more sensible --- tests/node/tests/ops.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/node/tests/ops.mjs b/tests/node/tests/ops.mjs index 864adf5a..9f621cec 100644 --- a/tests/node/tests/ops.mjs +++ b/tests/node/tests/ops.mjs @@ -510,7 +510,8 @@ Top Drawer`, { it("Frequency distribution", () => { const result = chef.frequencyDistribution("Don't Count Your Chickens Before They Hatch"); const expected = "{\"dataLength\":43,\"percentages\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13.953488372093023,0,0,0,0,0,0,2.3255813953488373,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2.3255813953488373,4.651162790697675,2.3255813953488373,0,0,0,2.3255813953488373,0,0,0,0,0,0,0,0,0,0,0,2.3255813953488373,0,0,0,0,2.3255813953488373,0,0,0,0,0,0,0,2.3255813953488373,0,4.651162790697675,0,9.30232558139535,2.3255813953488373,0,6.976744186046512,2.3255813953488373,0,2.3255813953488373,0,0,6.976744186046512,9.30232558139535,0,0,4.651162790697675,2.3255813953488373,6.976744186046512,4.651162790697675,0,0,0,2.3255813953488373,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"distribution\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,1,0,2,0,4,1,0,3,1,0,1,0,0,3,4,0,0,2,1,3,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"bytesRepresented\":22}"; - assert.strictEqual(result.toString(), expected); + // Whacky formatting, but the data is all there + assert.strictEqual(result.toString().replace(/\r?\n|\r|\s/g, ""), expected); }), it("From base", () => { From 9fa7edffbf1d559e54c31501b7f8be4e7b86448b Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sat, 2 Mar 2019 16:12:21 +0000 Subject: [PATCH 323/801] Improved file extraction error handling --- src/core/lib/FileSignatures.mjs | 6 +++--- src/core/operations/ExtractFiles.mjs | 22 +++++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/core/lib/FileSignatures.mjs b/src/core/lib/FileSignatures.mjs index 93247413..36e6818e 100644 --- a/src/core/lib/FileSignatures.mjs +++ b/src/core/lib/FileSignatures.mjs @@ -1057,7 +1057,7 @@ export function extractJPEG(bytes, offset) { while (stream.hasMore()) { const marker = stream.getBytes(2); - if (marker[0] !== 0xff) throw new Error("Invalid JPEG marker: " + marker); + if (marker[0] !== 0xff) throw new Error(`Invalid marker while parsing JPEG at pos ${stream.position}: ${marker}`); let segmentSize = 0; switch (marker[1]) { @@ -1609,7 +1609,7 @@ function parseDEFLATE(stream) { parseHuffmanBlock(stream, dynamicLiteralTable, dynamicDistanceTable); } else { - throw new Error("Invalid block type"); + throw new Error(`Invalid block type while parsing DEFLATE stream at pos ${stream.position}`); } } @@ -1712,7 +1712,7 @@ function readHuffmanCode(stream, table) { const codeLength = codeWithLength >>> 16; if (codeLength > maxCodeLength) { - throw new Error("Invalid code length: " + codeLength); + throw new Error(`Invalid Huffman Code length while parsing DEFLATE block at pos ${stream.position}: ${codeLength}`); } stream.moveBackwardsByBits(maxCodeLength - codeLength); diff --git a/src/core/operations/ExtractFiles.mjs b/src/core/operations/ExtractFiles.mjs index f172d926..d2b87990 100644 --- a/src/core/operations/ExtractFiles.mjs +++ b/src/core/operations/ExtractFiles.mjs @@ -5,7 +5,7 @@ */ import Operation from "../Operation"; -// import OperationError from "../errors/OperationError"; +import OperationError from "../errors/OperationError"; import Utils from "../Utils"; import {scanForFileTypes, extractFile} from "../lib/FileType"; import {FILE_SIGNATURES} from "../lib/FileSignatures"; @@ -34,7 +34,13 @@ class ExtractFiles extends Operation { type: "boolean", value: cat === "Miscellaneous" ? false : true }; - }); + }).concat([ + { + name: "Ignore failed extractions", + type: "boolean", + value: "true" + } + ]); } /** @@ -44,7 +50,8 @@ class ExtractFiles extends Operation { */ run(input, args) { const bytes = new Uint8Array(input), - categories = []; + categories = [], + ignoreFailedExtractions = args.pop(1); args.forEach((cat, i) => { if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]); @@ -59,8 +66,13 @@ class ExtractFiles extends Operation { try { files.push(extractFile(bytes, detectedFile.fileDetails, detectedFile.offset)); } catch (err) { - if (err.message.indexOf("No extraction algorithm available") < 0) - throw err; + if (!ignoreFailedExtractions && err.message.indexOf("No extraction algorithm available") < 0) { + throw new OperationError( + `Error while attempting to extract ${detectedFile.fileDetails.name} ` + + `at offset ${detectedFile.offset}:\n` + + `${err.message}` + ); + } } }); From 7975fadfe91ebd27b36c99d8eb54273f58efd648 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 11:46:27 +0000 Subject: [PATCH 324/801] Add options for min, max and step values for number inputs. --- src/core/Ingredient.mjs | 6 ++++++ src/core/Operation.mjs | 3 +++ src/web/HTMLIngredient.mjs | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs index 96cdd400..2c7154d9 100755 --- a/src/core/Ingredient.mjs +++ b/src/core/Ingredient.mjs @@ -27,6 +27,9 @@ class Ingredient { this.toggleValues = []; this.target = null; this.defaultIndex = 0; + this.min = null; + this.max = null; + this.step = 1; if (ingredientConfig) { this._parseConfig(ingredientConfig); @@ -50,6 +53,9 @@ class Ingredient { this.toggleValues = ingredientConfig.toggleValues; this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null; this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0; + this.min = ingredientConfig.min; + this.max = ingredientConfig.max; + this.step = ingredientConfig.step; } diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs index c0907fe8..c0656151 100755 --- a/src/core/Operation.mjs +++ b/src/core/Operation.mjs @@ -184,6 +184,9 @@ class Operation { if (ing.disabled) conf.disabled = ing.disabled; if (ing.target) conf.target = ing.target; if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex; + if (typeof ing.min === "number") conf.min = ing.min; + if (typeof ing.max === "number") conf.max = ing.max; + if (ing.step) conf.step = ing.step; return conf; }); } diff --git a/src/web/HTMLIngredient.mjs b/src/web/HTMLIngredient.mjs index ab7f682b..98d63be7 100755 --- a/src/web/HTMLIngredient.mjs +++ b/src/web/HTMLIngredient.mjs @@ -32,6 +32,9 @@ class HTMLIngredient { this.defaultIndex = config.defaultIndex || 0; this.toggleValues = config.toggleValues; this.id = "ing-" + this.app.nextIngId(); + this.min = (typeof config.min === "number") ? config.min : ""; + this.max = (typeof config.max === "number") ? config.max : ""; + this.step = config.step || 1; } @@ -103,6 +106,9 @@ class HTMLIngredient { id="${this.id}" arg-name="${this.name}" value="${this.value}" + min="${this.min}" + max="${this.max}" + step="${this.step}" ${this.disabled ? "disabled" : ""}> ${this.hint ? "" + this.hint + "" : ""} `; From 7b6062a4a287701cb33e4da7b4a70a306305fdf2 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 11:47:50 +0000 Subject: [PATCH 325/801] Set min blur amount to 1, add status message for gaussian blur. --- src/core/operations/BlurImage.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs index 68ae0b0f..562df8c7 100644 --- a/src/core/operations/BlurImage.mjs +++ b/src/core/operations/BlurImage.mjs @@ -32,7 +32,8 @@ class BlurImage extends Operation { { name: "Blur Amount", type: "number", - value: 5 + value: 5, + min: 1 }, { name: "Blur Type", @@ -59,6 +60,8 @@ class BlurImage extends Operation { image.blur(blurAmount); break; case "Gaussian": + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Gaussian blurring image. This will take a while..."); image.gaussian(blurAmount); break; } From d09e6089cac97e5e19c587d608ef3ffef4c03062 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 11:52:54 +0000 Subject: [PATCH 326/801] Add min width and height values --- src/core/operations/ResizeImage.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs index aa5cb24b..59d5b2ac 100644 --- a/src/core/operations/ResizeImage.mjs +++ b/src/core/operations/ResizeImage.mjs @@ -32,12 +32,14 @@ class ResizeImage extends Operation { { name: "Width", type: "number", - value: 100 + value: 100, + min: 1 }, { name: "Height", type: "number", - value: 100 + value: 100, + min: 1 }, { name: "Unit type", From f281a32a4e9342d944f8835ae7fcb407089e9cf4 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 13:48:13 +0000 Subject: [PATCH 327/801] Add Wikipedia URLs --- src/core/operations/BlurImage.mjs | 2 +- src/core/operations/ResizeImage.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs index 562df8c7..000f3677 100644 --- a/src/core/operations/BlurImage.mjs +++ b/src/core/operations/BlurImage.mjs @@ -24,7 +24,7 @@ class BlurImage extends Operation { this.name = "Blur Image"; this.module = "Image"; this.description = "Applies a blur effect to the image.

Gaussian blur is much slower than fast blur, but produces better results."; - this.infoURL = ""; + this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur"; this.inputType = "byteArray"; this.outputType = "byteArray"; this.presentType = "html"; diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs index 59d5b2ac..ecba7f55 100644 --- a/src/core/operations/ResizeImage.mjs +++ b/src/core/operations/ResizeImage.mjs @@ -24,7 +24,7 @@ class ResizeImage extends Operation { this.name = "Resize Image"; this.module = "Image"; this.description = "Resizes an image to the specified width and height values."; - this.infoURL = ""; + this.infoURL = "https://wikipedia.org/wiki/Image_scaling"; this.inputType = "byteArray"; this.outputType = "byteArray"; this.presentType = "html"; From 588a8b2a3a2fc6cd1feb9ad2d16207356efbf8e7 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 13:48:29 +0000 Subject: [PATCH 328/801] Fix code syntax --- src/core/operations/RotateImage.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/operations/RotateImage.mjs b/src/core/operations/RotateImage.mjs index 1bab6c98..bbeea5c5 100644 --- a/src/core/operations/RotateImage.mjs +++ b/src/core/operations/RotateImage.mjs @@ -30,9 +30,9 @@ class RotateImage extends Operation { this.presentType = "html"; this.args = [ { - "name": "Rotation amount (degrees)", - "type": "number", - "value": 90 + name: "Rotation amount (degrees)", + type: "number", + value: 90 } ]; } From 4f1a897e1876e62039396c4bbfbfe5e0fa6a53cb Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 13:48:48 +0000 Subject: [PATCH 329/801] Add Crop Image operation --- src/core/config/Categories.json | 3 +- src/core/operations/CropImage.mjs | 139 ++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/CropImage.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 9b0f8249..0ab9b1e5 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -365,7 +365,8 @@ "Blur Image", "Dither Image", "Invert Image", - "Flip Image" + "Flip Image", + "Crop Image" ] }, { diff --git a/src/core/operations/CropImage.mjs b/src/core/operations/CropImage.mjs new file mode 100644 index 00000000..9ccc5ec5 --- /dev/null +++ b/src/core/operations/CropImage.mjs @@ -0,0 +1,139 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Crop Image operation + */ +class CropImage extends Operation { + + /** + * CropImage constructor + */ + constructor() { + super(); + + this.name = "Crop Image"; + this.module = "Image"; + this.description = "Crops an image to the specified region, or automatically crop edges.

Autocrop
Automatically crops same-colour borders from the image.

Autocrop tolerance
A percentage value for the tolerance of colour difference between pixels.

Only autocrop frames
Only crop real frames (all sides must have the same border)

Symmetric autocrop
Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)

Autocrop keep border
The number of pixels of border to leave around the image."; + this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)"; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "X Position", + type: "number", + value: 0, + min: 0 + }, + { + name: "Y Position", + type: "number", + value: 0, + min: 0 + }, + { + name: "Width", + type: "number", + value: 10, + min: 1 + }, + { + name: "Height", + type: "number", + value: 10, + min: 1 + }, + { + name: "Autocrop", + type: "boolean", + value: false + }, + { + name: "Autocrop tolerance (%)", + type: "number", + value: 0.02, + min: 0, + max: 100, + step: 0.01 + }, + { + name: "Only autocrop frames", + type: "boolean", + value: true + }, + { + name: "Symmetric autocrop", + type: "boolean", + value: false + }, + { + name: "Autocrop keep border (px)", + type: "number", + value: 0, + min: 0 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + // const [firstArg, secondArg] = args; + const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args; + const type = Magic.magicFileType(input); + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + if (autocrop) { + image.autocrop({ + tolerance: (autoTolerance / 100), + cropOnlyFrames: autoFrames, + cropSymmetric: autoSymmetric, + leaveBorder: autoBorder + }); + } else { + image.crop(xPos, yPos, width, height); + } + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the cropped image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type"); + } + dataURI += "base64," + toBase64(data); + + return ""; + } + +} + +export default CropImage; From 737ce9939823528ba1c79195a78378f8b8bf7483 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 14:24:57 +0000 Subject: [PATCH 330/801] Add image brightness / contrast operation --- src/core/config/Categories.json | 3 +- .../operations/ImageBrightnessContrast.mjs | 91 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/ImageBrightnessContrast.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 0ab9b1e5..411f980f 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -366,7 +366,8 @@ "Dither Image", "Invert Image", "Flip Image", - "Crop Image" + "Crop Image", + "Image Brightness / Contrast" ] }, { diff --git a/src/core/operations/ImageBrightnessContrast.mjs b/src/core/operations/ImageBrightnessContrast.mjs new file mode 100644 index 00000000..51c61c70 --- /dev/null +++ b/src/core/operations/ImageBrightnessContrast.mjs @@ -0,0 +1,91 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Image Brightness / Contrast operation + */ +class ImageBrightnessContrast extends Operation { + + /** + * ImageBrightnessContrast constructor + */ + constructor() { + super(); + + this.name = "Image Brightness / Contrast"; + this.module = "Image"; + this.description = "Adjust the brightness and contrast of an image."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Brightness", + type: "number", + value: 0, + min: -100, + max: 100 + }, + { + name: "Contrast", + type: "number", + value: 0, + min: -100, + max: 100 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [brightness, contrast] = args; + const type = Magic.magicFileType(input); + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + image.brightness(brightness / 100); + image.contrast(contrast / 100); + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type"); + } + dataURI += "base64," + toBase64(data); + + return ""; + } + +} + +export default ImageBrightnessContrast; From ec1fd7b923cf1049be2c908ee25e2c66a2e1be1a Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 14:38:25 +0000 Subject: [PATCH 331/801] Add image opacity operation --- src/core/config/Categories.json | 3 +- src/core/operations/ImageOpacity.mjs | 83 ++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/ImageOpacity.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 411f980f..78270fb0 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -367,7 +367,8 @@ "Invert Image", "Flip Image", "Crop Image", - "Image Brightness / Contrast" + "Image Brightness / Contrast", + "Image Opacity" ] }, { diff --git a/src/core/operations/ImageOpacity.mjs b/src/core/operations/ImageOpacity.mjs new file mode 100644 index 00000000..11a364b8 --- /dev/null +++ b/src/core/operations/ImageOpacity.mjs @@ -0,0 +1,83 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Image Opacity operation + */ +class ImageOpacity extends Operation { + + /** + * ImageOpacity constructor + */ + constructor() { + super(); + + this.name = "Image Opacity"; + this.module = "Image"; + this.description = "Adjust the opacity of an image."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Opacity (%)", + type: "number", + value: 100, + min: 0, + max: 100 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [opacity] = args; + const type = Magic.magicFileType(input); + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + image.opacity(opacity / 100); + + const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG); + return [...imageBuffer]; + } + + /** + * Displays the image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type"); + } + dataURI += "base64," + toBase64(data); + + return ""; + } + +} + +export default ImageOpacity; From 514eef50debdf8a57ee46082e64ab6038f5dd046 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 4 Mar 2019 14:48:17 +0000 Subject: [PATCH 332/801] Add image filter operation --- src/core/config/Categories.json | 3 +- src/core/operations/ImageFilter.mjs | 90 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/ImageFilter.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 78270fb0..70390c8d 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -368,7 +368,8 @@ "Flip Image", "Crop Image", "Image Brightness / Contrast", - "Image Opacity" + "Image Opacity", + "Image Filter" ] }, { diff --git a/src/core/operations/ImageFilter.mjs b/src/core/operations/ImageFilter.mjs new file mode 100644 index 00000000..370f5e6f --- /dev/null +++ b/src/core/operations/ImageFilter.mjs @@ -0,0 +1,90 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Image Filter operation + */ +class ImageFilter extends Operation { + + /** + * ImageFilter constructor + */ + constructor() { + super(); + + this.name = "Image Filter"; + this.module = "Image"; + this.description = "Applies a greyscale or sepia filter to an image."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Filter type", + type: "option", + value: [ + "Greyscale", + "Sepia" + ] + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [filterType] = args; + const type = Magic.magicFileType(input); + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + + if (filterType === "Greyscale") { + image.greyscale(); + } else { + image.sepia(); + } + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the blurred image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type."); + } + dataURI += "base64," + toBase64(data); + + return ""; + + } + +} + +export default ImageFilter; From 370ae323f6f0bac878f7987b35e1b23e9dec8ba2 Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 5 Mar 2019 11:49:25 +0000 Subject: [PATCH 333/801] Fix linting --- src/core/operations/ResizeImage.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs index ecba7f55..8d46b9cf 100644 --- a/src/core/operations/ResizeImage.mjs +++ b/src/core/operations/ResizeImage.mjs @@ -86,10 +86,11 @@ class ResizeImage extends Operation { "Hermite": jimp.RESIZE_HERMITE, "Bezier": jimp.RESIZE_BEZIER }; - + if (!type || type.mime.indexOf("image") !== 0){ throw new OperationError("Invalid file type."); } + const image = await jimp.read(Buffer.from(input)); if (unit === "Percent") { From 662922be6fd6cb9cd6099444d83d065aeb77adf5 Mon Sep 17 00:00:00 2001 From: j433866 Date: Wed, 6 Mar 2019 10:32:58 +0000 Subject: [PATCH 334/801] Add resizing status message --- src/core/operations/ResizeImage.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs index 8d46b9cf..e1ce7d45 100644 --- a/src/core/operations/ResizeImage.mjs +++ b/src/core/operations/ResizeImage.mjs @@ -97,6 +97,9 @@ class ResizeImage extends Operation { width = image.getWidth() * (width / 100); height = image.getHeight() * (height / 100); } + + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Resizing image..."); if (aspect) { image.scaleToFit(width, height, resizeMap[resizeAlg]); } else { From 833c1cd98f8257e130dafd5554e5080bf62c7566 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 10:02:37 +0000 Subject: [PATCH 335/801] Add Contain Image, Cover Image and Image Hue / Saturation / Lightness ops --- src/core/config/Categories.json | 5 +- src/core/operations/ContainImage.mjs | 140 ++++++++++++++++++ src/core/operations/CoverImage.mjs | 139 +++++++++++++++++ .../ImageHueSaturationLightness.mjs | 126 ++++++++++++++++ 4 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/ContainImage.mjs create mode 100644 src/core/operations/CoverImage.mjs create mode 100644 src/core/operations/ImageHueSaturationLightness.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 70390c8d..8430e498 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -369,7 +369,10 @@ "Crop Image", "Image Brightness / Contrast", "Image Opacity", - "Image Filter" + "Image Filter", + "Contain Image", + "Cover Image", + "Image Hue/Saturation/Lightness" ] }, { diff --git a/src/core/operations/ContainImage.mjs b/src/core/operations/ContainImage.mjs new file mode 100644 index 00000000..056244df --- /dev/null +++ b/src/core/operations/ContainImage.mjs @@ -0,0 +1,140 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Contain Image operation + */ +class ContainImage extends Operation { + + /** + * ContainImage constructor + */ + constructor() { + super(); + + this.name = "Contain Image"; + this.module = "Image"; + this.description = "Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Width", + type: "number", + value: 100, + min: 1 + }, + { + name: "Height", + type: "number", + value: 100, + min: 1 + }, + { + name: "Horizontal align", + type: "option", + value: [ + "Left", + "Center", + "Right" + ], + defaultIndex: 1 + }, + { + name: "Vertical align", + type: "option", + value: [ + "Top", + "Middle", + "Bottom" + ], + defaultIndex: 1 + }, + { + name: "Resizing algorithm", + type: "option", + value: [ + "Nearest Neighbour", + "Bilinear", + "Bicubic", + "Hermite", + "Bezier" + ], + defaultIndex: 1 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [width, height, hAlign, vAlign, alg] = args; + const type = Magic.magicFileType(input); + + const resizeMap = { + "Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR, + "Bilinear": jimp.RESIZE_BILINEAR, + "Bicubic": jimp.RESIZE_BICUBIC, + "Hermite": jimp.RESIZE_HERMITE, + "Bezier": jimp.RESIZE_BEZIER + }; + + const alignMap = { + "Left": jimp.HORIZONTAL_ALIGN_LEFT, + "Center": jimp.HORIZONTAL_ALIGN_CENTER, + "Right": jimp.HORIZONTAL_ALIGN_RIGHT, + "Top": jimp.VERTICAL_ALIGN_TOP, + "Middle": jimp.VERTICAL_ALIGN_MIDDLE, + "Bottom": jimp.VERTICAL_ALIGN_BOTTOM + }; + + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Containing image..."); + image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the contained image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type"); + } + dataURI += "base64," + toBase64(data); + + return ""; + } + +} + +export default ContainImage; diff --git a/src/core/operations/CoverImage.mjs b/src/core/operations/CoverImage.mjs new file mode 100644 index 00000000..57258ec3 --- /dev/null +++ b/src/core/operations/CoverImage.mjs @@ -0,0 +1,139 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Cover Image operation + */ +class CoverImage extends Operation { + + /** + * CoverImage constructor + */ + constructor() { + super(); + + this.name = "Cover Image"; + this.module = "Image"; + this.description = "Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Width", + type: "number", + value: 100, + min: 1 + }, + { + name: "Height", + type: "number", + value: 100, + min: 1 + }, + { + name: "Horizontal align", + type: "option", + value: [ + "Left", + "Center", + "Right" + ], + defaultIndex: 1 + }, + { + name: "Vertical align", + type: "option", + value: [ + "Top", + "Middle", + "Bottom" + ], + defaultIndex: 1 + }, + { + name: "Resizing algorithm", + type: "option", + value: [ + "Nearest Neighbour", + "Bilinear", + "Bicubic", + "Hermite", + "Bezier" + ], + defaultIndex: 1 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [width, height, hAlign, vAlign, alg] = args; + const type = Magic.magicFileType(input); + + const resizeMap = { + "Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR, + "Bilinear": jimp.RESIZE_BILINEAR, + "Bicubic": jimp.RESIZE_BICUBIC, + "Hermite": jimp.RESIZE_HERMITE, + "Bezier": jimp.RESIZE_BEZIER + }; + + const alignMap = { + "Left": jimp.HORIZONTAL_ALIGN_LEFT, + "Center": jimp.HORIZONTAL_ALIGN_CENTER, + "Right": jimp.HORIZONTAL_ALIGN_RIGHT, + "Top": jimp.VERTICAL_ALIGN_TOP, + "Middle": jimp.VERTICAL_ALIGN_MIDDLE, + "Bottom": jimp.VERTICAL_ALIGN_BOTTOM + }; + + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Covering image..."); + image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the covered image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type"); + } + dataURI += "base64," + toBase64(data); + + return ""; + } + +} + +export default CoverImage; diff --git a/src/core/operations/ImageHueSaturationLightness.mjs b/src/core/operations/ImageHueSaturationLightness.mjs new file mode 100644 index 00000000..29293fdb --- /dev/null +++ b/src/core/operations/ImageHueSaturationLightness.mjs @@ -0,0 +1,126 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64.mjs"; +import jimp from "jimp"; + +/** + * Image Hue/Saturation/Lightness operation + */ +class ImageHueSaturationLightness extends Operation { + + /** + * ImageHueSaturationLightness constructor + */ + constructor() { + super(); + + this.name = "Image Hue/Saturation/Lightness"; + this.module = "Image"; + this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Hue", + type: "number", + value: 0, + min: -360, + max: 360 + }, + { + name: "Saturation", + type: "number", + value: 0, + min: -100, + max: 100 + }, + { + name: "Lightness", + type: "number", + value: 0, + min: -100, + max: 100 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [hue, saturation, lightness] = args; + const type = Magic.magicFileType(input); + + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + + if (hue !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image hue..."); + image.colour([ + { + apply: "hue", + params: [hue] + } + ]); + } + if (saturation !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image saturation..."); + image.colour([ + { + apply: "saturate", + params: [saturation] + } + ]); + } + if (lightness !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image lightness..."); + image.colour([ + { + apply: "lighten", + params: [lightness] + } + ]); + } + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type"); + } + dataURI += "base64," + toBase64(data); + + return ""; + } +} + +export default ImageHueSaturationLightness; From 4a7ea469d483e906bd032fd272e9047f52d3b207 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 10:03:09 +0000 Subject: [PATCH 336/801] Add status messages for image operations --- src/core/operations/CropImage.mjs | 2 ++ src/core/operations/DitherImage.mjs | 2 ++ src/core/operations/FlipImage.mjs | 2 ++ src/core/operations/ImageBrightnessContrast.mjs | 12 ++++++++++-- src/core/operations/ImageFilter.mjs | 3 ++- src/core/operations/ImageOpacity.mjs | 2 ++ src/core/operations/InvertImage.mjs | 2 ++ src/core/operations/RotateImage.mjs | 2 ++ 8 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/core/operations/CropImage.mjs b/src/core/operations/CropImage.mjs index 9ccc5ec5..e29db631 100644 --- a/src/core/operations/CropImage.mjs +++ b/src/core/operations/CropImage.mjs @@ -99,6 +99,8 @@ class CropImage extends Operation { } const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Cropping image..."); if (autocrop) { image.autocrop({ tolerance: (autoTolerance / 100), diff --git a/src/core/operations/DitherImage.mjs b/src/core/operations/DitherImage.mjs index 2cc9ac2d..e6856d4a 100644 --- a/src/core/operations/DitherImage.mjs +++ b/src/core/operations/DitherImage.mjs @@ -41,6 +41,8 @@ class DitherImage extends Operation { if (type && type.mime.indexOf("image") === 0){ const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Applying dither to image..."); image.dither565(); const imageBuffer = await image.getBufferAsync(jimp.AUTO); return [...imageBuffer]; diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs index fa3054e2..3185df9f 100644 --- a/src/core/operations/FlipImage.mjs +++ b/src/core/operations/FlipImage.mjs @@ -51,6 +51,8 @@ class FlipImage extends Operation { const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Flipping image..."); switch (flipAxis){ case "Horizontal": image.flip(true, false); diff --git a/src/core/operations/ImageBrightnessContrast.mjs b/src/core/operations/ImageBrightnessContrast.mjs index 51c61c70..7d8eca4f 100644 --- a/src/core/operations/ImageBrightnessContrast.mjs +++ b/src/core/operations/ImageBrightnessContrast.mjs @@ -59,8 +59,16 @@ class ImageBrightnessContrast extends Operation { } const image = await jimp.read(Buffer.from(input)); - image.brightness(brightness / 100); - image.contrast(contrast / 100); + if (brightness !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image brightness..."); + image.brightness(brightness / 100); + } + if (contrast !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image contrast..."); + image.contrast(contrast / 100); + } const imageBuffer = await image.getBufferAsync(jimp.AUTO); return [...imageBuffer]; diff --git a/src/core/operations/ImageFilter.mjs b/src/core/operations/ImageFilter.mjs index 370f5e6f..b756b9f2 100644 --- a/src/core/operations/ImageFilter.mjs +++ b/src/core/operations/ImageFilter.mjs @@ -53,7 +53,8 @@ class ImageFilter extends Operation { } const image = await jimp.read(Buffer.from(input)); - + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image..."); if (filterType === "Greyscale") { image.greyscale(); } else { diff --git a/src/core/operations/ImageOpacity.mjs b/src/core/operations/ImageOpacity.mjs index 11a364b8..090a8975 100644 --- a/src/core/operations/ImageOpacity.mjs +++ b/src/core/operations/ImageOpacity.mjs @@ -52,6 +52,8 @@ class ImageOpacity extends Operation { } const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image opacity..."); image.opacity(opacity / 100); const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG); diff --git a/src/core/operations/InvertImage.mjs b/src/core/operations/InvertImage.mjs index 87da0156..99de9f0f 100644 --- a/src/core/operations/InvertImage.mjs +++ b/src/core/operations/InvertImage.mjs @@ -42,6 +42,8 @@ class InvertImage extends Operation { throw new OperationError("Invalid input file format."); } const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Inverting image..."); image.invert(); const imageBuffer = await image.getBufferAsync(jimp.AUTO); return [...imageBuffer]; diff --git a/src/core/operations/RotateImage.mjs b/src/core/operations/RotateImage.mjs index bbeea5c5..76947037 100644 --- a/src/core/operations/RotateImage.mjs +++ b/src/core/operations/RotateImage.mjs @@ -48,6 +48,8 @@ class RotateImage extends Operation { if (type && type.mime.indexOf("image") === 0){ const image = await jimp.read(Buffer.from(input)); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Rotating image..."); image.rotate(degrees); const imageBuffer = await image.getBufferAsync(jimp.AUTO); return [...imageBuffer]; From 1031429550e69f63373e1cc2a5fde2118328c9b3 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 11:19:04 +0000 Subject: [PATCH 337/801] Add error handling --- src/core/operations/BlurImage.mjs | 34 ++++--- src/core/operations/ContainImage.mjs | 22 +++-- src/core/operations/CoverImage.mjs | 21 +++-- src/core/operations/CropImage.mjs | 37 +++++--- src/core/operations/DitherImage.mjs | 21 +++-- src/core/operations/FlipImage.mjs | 34 ++++--- .../operations/ImageBrightnessContrast.mjs | 33 ++++--- src/core/operations/ImageFilter.mjs | 27 ++++-- .../ImageHueSaturationLightness.mjs | 72 ++++++++------- src/core/operations/ImageOpacity.mjs | 21 +++-- src/core/operations/InvertImage.mjs | 22 +++-- src/core/operations/NormaliseImage.mjs | 91 +++++++++++++++++++ src/core/operations/ResizeImage.mjs | 36 +++++--- src/core/operations/RotateImage.mjs | 21 +++-- 14 files changed, 348 insertions(+), 144 deletions(-) create mode 100644 src/core/operations/NormaliseImage.mjs diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs index 000f3677..fba3c927 100644 --- a/src/core/operations/BlurImage.mjs +++ b/src/core/operations/BlurImage.mjs @@ -53,21 +53,29 @@ class BlurImage extends Operation { const type = Magic.magicFileType(input); if (type && type.mime.indexOf("image") === 0){ - const image = await jimp.read(Buffer.from(input)); - - switch (blurType){ - case "Fast": - image.blur(blurAmount); - break; - case "Gaussian": - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Gaussian blurring image. This will take a while..."); - image.gaussian(blurAmount); - break; + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } + try { + switch (blurType){ + case "Fast": + image.blur(blurAmount); + break; + case "Gaussian": + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Gaussian blurring image. This will take a while..."); + image.gaussian(blurAmount); + break; + } - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error blurring image. (${err})`); + } } else { throw new OperationError("Invalid file type."); } diff --git a/src/core/operations/ContainImage.mjs b/src/core/operations/ContainImage.mjs index 056244df..a2da5363 100644 --- a/src/core/operations/ContainImage.mjs +++ b/src/core/operations/ContainImage.mjs @@ -106,13 +106,21 @@ class ContainImage extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Containing image..."); - image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Containing image..."); + image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error containing image. (${err})`); + } } /** diff --git a/src/core/operations/CoverImage.mjs b/src/core/operations/CoverImage.mjs index 57258ec3..f49e08b7 100644 --- a/src/core/operations/CoverImage.mjs +++ b/src/core/operations/CoverImage.mjs @@ -106,12 +106,21 @@ class CoverImage extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Covering image..."); - image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Covering image..."); + image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error covering image. (${err})`); + } } /** diff --git a/src/core/operations/CropImage.mjs b/src/core/operations/CropImage.mjs index e29db631..7f1eabdf 100644 --- a/src/core/operations/CropImage.mjs +++ b/src/core/operations/CropImage.mjs @@ -98,22 +98,31 @@ class CropImage extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Cropping image..."); - if (autocrop) { - image.autocrop({ - tolerance: (autoTolerance / 100), - cropOnlyFrames: autoFrames, - cropSymmetric: autoSymmetric, - leaveBorder: autoBorder - }); - } else { - image.crop(xPos, yPos, width, height); + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Cropping image..."); + if (autocrop) { + image.autocrop({ + tolerance: (autoTolerance / 100), + cropOnlyFrames: autoFrames, + cropSymmetric: autoSymmetric, + leaveBorder: autoBorder + }); + } else { + image.crop(xPos, yPos, width, height); + } - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error cropping image. (${err})`); + } } /** diff --git a/src/core/operations/DitherImage.mjs b/src/core/operations/DitherImage.mjs index e6856d4a..f7ef4e33 100644 --- a/src/core/operations/DitherImage.mjs +++ b/src/core/operations/DitherImage.mjs @@ -40,12 +40,21 @@ class DitherImage extends Operation { const type = Magic.magicFileType(input); if (type && type.mime.indexOf("image") === 0){ - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Applying dither to image..."); - image.dither565(); - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Applying dither to image..."); + image.dither565(); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error applying dither to image. (${err})`); + } } else { throw new OperationError("Invalid file type."); } diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs index 3185df9f..09791ca6 100644 --- a/src/core/operations/FlipImage.mjs +++ b/src/core/operations/FlipImage.mjs @@ -49,21 +49,29 @@ class FlipImage extends Operation { throw new OperationError("Invalid input file type."); } - const image = await jimp.read(Buffer.from(input)); - - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Flipping image..."); - switch (flipAxis){ - case "Horizontal": - image.flip(true, false); - break; - case "Vertical": - image.flip(false, true); - break; + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Flipping image..."); + switch (flipAxis){ + case "Horizontal": + image.flip(true, false); + break; + case "Vertical": + image.flip(false, true); + break; + } - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error flipping image. (${err})`); + } } /** diff --git a/src/core/operations/ImageBrightnessContrast.mjs b/src/core/operations/ImageBrightnessContrast.mjs index 7d8eca4f..2f49bab7 100644 --- a/src/core/operations/ImageBrightnessContrast.mjs +++ b/src/core/operations/ImageBrightnessContrast.mjs @@ -58,20 +58,29 @@ class ImageBrightnessContrast extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - if (brightness !== 0) { - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Changing image brightness..."); - image.brightness(brightness / 100); - } - if (contrast !== 0) { - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Changing image contrast..."); - image.contrast(contrast / 100); + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } + try { + if (brightness !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image brightness..."); + image.brightness(brightness / 100); + } + if (contrast !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image contrast..."); + image.contrast(contrast / 100); + } - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error adjusting image brightness / contrast. (${err})`); + } } /** diff --git a/src/core/operations/ImageFilter.mjs b/src/core/operations/ImageFilter.mjs index b756b9f2..5d7f505d 100644 --- a/src/core/operations/ImageFilter.mjs +++ b/src/core/operations/ImageFilter.mjs @@ -52,17 +52,26 @@ class ImageFilter extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image..."); - if (filterType === "Greyscale") { - image.greyscale(); - } else { - image.sepia(); + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image..."); + if (filterType === "Greyscale") { + image.greyscale(); + } else { + image.sepia(); + } - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error applying filter to image. (${err})`); + } } /** diff --git a/src/core/operations/ImageHueSaturationLightness.mjs b/src/core/operations/ImageHueSaturationLightness.mjs index 29293fdb..9e63a6b3 100644 --- a/src/core/operations/ImageHueSaturationLightness.mjs +++ b/src/core/operations/ImageHueSaturationLightness.mjs @@ -66,40 +66,48 @@ class ImageHueSaturationLightness extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - - if (hue !== 0) { - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Changing image hue..."); - image.colour([ - { - apply: "hue", - params: [hue] - } - ]); + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } - if (saturation !== 0) { - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Changing image saturation..."); - image.colour([ - { - apply: "saturate", - params: [saturation] - } - ]); + try { + if (hue !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image hue..."); + image.colour([ + { + apply: "hue", + params: [hue] + } + ]); + } + if (saturation !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image saturation..."); + image.colour([ + { + apply: "saturate", + params: [saturation] + } + ]); + } + if (lightness !== 0) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image lightness..."); + image.colour([ + { + apply: "lighten", + params: [lightness] + } + ]); + } + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`); } - if (lightness !== 0) { - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Changing image lightness..."); - image.colour([ - { - apply: "lighten", - params: [lightness] - } - ]); - } - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; } /** diff --git a/src/core/operations/ImageOpacity.mjs b/src/core/operations/ImageOpacity.mjs index 090a8975..76a23f77 100644 --- a/src/core/operations/ImageOpacity.mjs +++ b/src/core/operations/ImageOpacity.mjs @@ -51,13 +51,22 @@ class ImageOpacity extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Changing image opacity..."); - image.opacity(opacity / 100); + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Changing image opacity..."); + image.opacity(opacity / 100); - const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG); - return [...imageBuffer]; + const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG); + return [...imageBuffer]; + } catch (err) { + throw new OperateionError(`Error changing image opacity. (${err})`); + } } /** diff --git a/src/core/operations/InvertImage.mjs b/src/core/operations/InvertImage.mjs index 99de9f0f..c2625d9a 100644 --- a/src/core/operations/InvertImage.mjs +++ b/src/core/operations/InvertImage.mjs @@ -41,12 +41,22 @@ class InvertImage extends Operation { if (!type || type.mime.indexOf("image") !== 0) { throw new OperationError("Invalid input file format."); } - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Inverting image..."); - image.invert(); - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Inverting image..."); + image.invert(); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error inverting image. (${err})`); + } } /** diff --git a/src/core/operations/NormaliseImage.mjs b/src/core/operations/NormaliseImage.mjs new file mode 100644 index 00000000..1815c7f1 --- /dev/null +++ b/src/core/operations/NormaliseImage.mjs @@ -0,0 +1,91 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Magic from "../lib/Magic"; +import { toBase64 } from "../lib/Base64"; +import jimp from "jimp"; + +/** + * Normalise Image operation + */ +class NormaliseImage extends Operation { + + /** + * NormaliseImage constructor + */ + constructor() { + super(); + + this.name = "Normalise Image"; + this.module = "Image"; + this.description = "Normalise the image colours."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType= "html"; + this.args = [ + /* Example arguments. See the project wiki for full details. + { + name: "First arg", + type: "string", + value: "Don't Panic" + }, + { + name: "Second arg", + type: "number", + value: 42 + } + */ + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + // const [firstArg, secondArg] = args; + const type = Magic.magicFileType(input); + + if (!type || type.mime.indexOf("image") !== 0){ + throw new OperationError("Invalid file type."); + } + + const image = await jimp.read(Buffer.from(input)); + + image.normalize(); + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } + + /** + * Displays the normalised image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + let dataURI = "data:"; + const type = Magic.magicFileType(data); + if (type && type.mime.indexOf("image") === 0){ + dataURI += type.mime + ";"; + } else { + throw new OperationError("Invalid file type."); + } + dataURI += "base64," + toBase64(data); + + return ""; + + } + +} + +export default NormaliseImage; diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs index e1ce7d45..36b0c805 100644 --- a/src/core/operations/ResizeImage.mjs +++ b/src/core/operations/ResizeImage.mjs @@ -91,23 +91,31 @@ class ResizeImage extends Operation { throw new OperationError("Invalid file type."); } - const image = await jimp.read(Buffer.from(input)); - - if (unit === "Percent") { - width = image.getWidth() * (width / 100); - height = image.getHeight() * (height / 100); + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); } + try { + if (unit === "Percent") { + width = image.getWidth() * (width / 100); + height = image.getHeight() * (height / 100); + } - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Resizing image..."); - if (aspect) { - image.scaleToFit(width, height, resizeMap[resizeAlg]); - } else { - image.resize(width, height, resizeMap[resizeAlg]); + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Resizing image..."); + if (aspect) { + image.scaleToFit(width, height, resizeMap[resizeAlg]); + } else { + image.resize(width, height, resizeMap[resizeAlg]); + } + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error resizing image. (${err})`); } - - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; } /** diff --git a/src/core/operations/RotateImage.mjs b/src/core/operations/RotateImage.mjs index 76947037..b2b1e059 100644 --- a/src/core/operations/RotateImage.mjs +++ b/src/core/operations/RotateImage.mjs @@ -47,12 +47,21 @@ class RotateImage extends Operation { const type = Magic.magicFileType(input); if (type && type.mime.indexOf("image") === 0){ - const image = await jimp.read(Buffer.from(input)); - if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Rotating image..."); - image.rotate(degrees); - const imageBuffer = await image.getBufferAsync(jimp.AUTO); - return [...imageBuffer]; + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Rotating image..."); + image.rotate(degrees); + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error rotating image. (${err})`); + } } else { throw new OperationError("Invalid file type."); } From 0c9db5afe9e3dff8f70f05a634326d036b15a397 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 11:36:29 +0000 Subject: [PATCH 338/801] Fix typo --- src/core/operations/ImageOpacity.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/ImageOpacity.mjs b/src/core/operations/ImageOpacity.mjs index 76a23f77..5a547992 100644 --- a/src/core/operations/ImageOpacity.mjs +++ b/src/core/operations/ImageOpacity.mjs @@ -65,7 +65,7 @@ class ImageOpacity extends Operation { const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG); return [...imageBuffer]; } catch (err) { - throw new OperateionError(`Error changing image opacity. (${err})`); + throw new OperationError(`Error changing image opacity. (${err})`); } } From 21a8d0320190644148072e45ed46610fbb14824e Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 13:21:26 +0000 Subject: [PATCH 339/801] Move parsing and generation of QR codes to lib folder. Also rewrote QR code parsing to be more readable and actually error out properly. --- src/core/lib/QRCode.mjs | 90 ++++++++++++++++++++++++++ src/core/operations/GenerateQRCode.mjs | 26 +------- src/core/operations/ParseQRCode.mjs | 60 ++--------------- 3 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 src/core/lib/QRCode.mjs diff --git a/src/core/lib/QRCode.mjs b/src/core/lib/QRCode.mjs new file mode 100644 index 00000000..bf134367 --- /dev/null +++ b/src/core/lib/QRCode.mjs @@ -0,0 +1,90 @@ +/** + * QR code resources + * + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError"; +import jsQR from "jsqr"; +import qr from "qr-image"; +import jimp from "jimp"; + +/** + * Parses a QR code image from an image + * + * @param {byteArray} input + * @param {boolean} normalise + * @returns {string} + */ +export async function parseQrCode(input, normalise) { + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error opening image. (${err})`); + } + + try { + if (normalise) { + image.rgba(false); + image.background(0xFFFFFFFF); + image.normalize(); + image.greyscale(); + } + } catch (err) { + throw new OperationError(`Error normalising iamge. (${err})`); + } + + const qrData = jsQR(image.bitmap.data, image.getWidth(), image.getHeight()); + if (qrData) { + return qrData.data; + } else { + throw new OperationError("Could not read a QR code from the image."); + } +} + +/** + * Generates a QR code from the input string + * + * @param {string} input + * @param {string} format + * @param {number} moduleSize + * @param {number} margin + * @param {string} errorCorrection + * @returns {byteArray} + */ +export function generateQrCode(input, format, moduleSize, margin, errorCorrection) { + const formats = ["SVG", "EPS", "PDF", "PNG"]; + if (!formats.includes(format.toUpperCase())) { + throw new OperationError("Unsupported QR code format."); + } + + let qrImage; + try { + qrImage = qr.imageSync(input, { + type: format, + size: moduleSize, + margin: margin, + "ec_level": errorCorrection.charAt(0).toUpperCase() + }); + } catch (err) { + throw new OperationError(`Error generating QR code. (${err})`); + } + + if (!qrImage) { + throw new OperationError("Error generating QR code."); + } + + switch (format) { + case "SVG": + case "EPS": + case "PDF": + return [...Buffer.from(qrImage)]; + case "PNG": + return [...qrImage]; + default: + throw new OperationError("Unsupported QR code format."); + } +} diff --git a/src/core/operations/GenerateQRCode.mjs b/src/core/operations/GenerateQRCode.mjs index edab6d40..4e1983e5 100644 --- a/src/core/operations/GenerateQRCode.mjs +++ b/src/core/operations/GenerateQRCode.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation"; import OperationError from "../errors/OperationError"; -import qr from "qr-image"; +import { generateQrCode } from "../lib/QRCode"; import { toBase64 } from "../lib/Base64"; import Magic from "../lib/Magic"; import Utils from "../Utils"; @@ -62,29 +62,7 @@ class GenerateQRCode extends Operation { run(input, args) { const [format, size, margin, errorCorrection] = args; - // Create new QR image from the input data, and convert it to a buffer - const qrImage = qr.imageSync(input, { - type: format, - size: size, - margin: margin, - "ec_level": errorCorrection.charAt(0).toUpperCase() - }); - - if (qrImage == null) { - throw new OperationError("Error generating QR code."); - } - - switch (format) { - case "SVG": - case "EPS": - case "PDF": - return [...Buffer.from(qrImage)]; - case "PNG": - // Return the QR image buffer as a byte array - return [...qrImage]; - default: - throw new OperationError("Unsupported QR code format."); - } + return generateQrCode(input, format, size, margin, errorCorrection); } /** diff --git a/src/core/operations/ParseQRCode.mjs b/src/core/operations/ParseQRCode.mjs index 75a24d55..816b6e75 100644 --- a/src/core/operations/ParseQRCode.mjs +++ b/src/core/operations/ParseQRCode.mjs @@ -7,8 +7,7 @@ import Operation from "../Operation"; import OperationError from "../errors/OperationError"; import Magic from "../lib/Magic"; -import jsqr from "jsqr"; -import jimp from "jimp"; +import { parseQrCode } from "../lib/QRCode"; /** * Parse QR Code operation @@ -42,64 +41,13 @@ class ParseQRCode extends Operation { * @returns {string} */ async run(input, args) { - const type = Magic.magicFileType(input); const [normalise] = args; + const type = Magic.magicFileType(input); - // Make sure that the input is an image - if (type && type.mime.indexOf("image") === 0) { - let image = input; - - if (normalise) { - // Process the image to be easier to read by jsqr - // Disables the alpha channel - // Sets the image default background to white - // Normalises the image colours - // Makes the image greyscale - // Converts image to a JPEG - image = await new Promise((resolve, reject) => { - jimp.read(Buffer.from(input)) - .then(image => { - image - .rgba(false) - .background(0xFFFFFFFF) - .normalize() - .greyscale() - .getBuffer(jimp.MIME_JPEG, (error, result) => { - resolve(result); - }); - }) - .catch(err => { - reject(new OperationError("Error reading the image file.")); - }); - }); - } - - if (image instanceof OperationError) { - throw image; - } - - return new Promise((resolve, reject) => { - jimp.read(Buffer.from(image)) - .then(image => { - if (image.bitmap != null) { - const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight()); - if (qrData != null) { - resolve(qrData.data); - } else { - reject(new OperationError("Couldn't read a QR code from the image.")); - } - } else { - reject(new OperationError("Error reading the image file.")); - } - }) - .catch(err => { - reject(new OperationError("Error reading the image file.")); - }); - }); - } else { + if (!type || type.mime.indexOf("image") !== 0) { throw new OperationError("Invalid file type."); } - + return await parseQrCode(input, normalise); } } From 11451ac6b9f42e22a446d6c5e5e95259895dbed3 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 13:35:37 +0000 Subject: [PATCH 340/801] Add image format pattern. ("borrowed" from RenderImage) --- src/core/operations/ParseQRCode.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/operations/ParseQRCode.mjs b/src/core/operations/ParseQRCode.mjs index 816b6e75..d929500b 100644 --- a/src/core/operations/ParseQRCode.mjs +++ b/src/core/operations/ParseQRCode.mjs @@ -33,6 +33,14 @@ class ParseQRCode extends Operation { "value": false } ]; + this.patterns = [ + { + "match": "^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)", + "flags": "", + "args": [false], + "useful": true + } + ]; } /** From 2b538061e940c70bc3446a9bc772a83e5cb81b2b Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 16:26:42 +0000 Subject: [PATCH 341/801] Fix fork operation not setting ingredient values correctly. --- src/core/operations/Fork.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Fork.mjs b/src/core/operations/Fork.mjs index 27a1af96..02aba3e8 100644 --- a/src/core/operations/Fork.mjs +++ b/src/core/operations/Fork.mjs @@ -89,7 +89,7 @@ class Fork extends Operation { // Run recipe over each tranche for (i = 0; i < inputs.length; i++) { // Baseline ing values for each tranche so that registers are reset - subOpList.forEach((op, i) => { + recipe.opList.forEach((op, i) => { op.ingValues = JSON.parse(JSON.stringify(ingValues[i])); }); From d923c99975b87fc7869ea26c6c1e4ce9981becb0 Mon Sep 17 00:00:00 2001 From: j433866 Date: Thu, 7 Mar 2019 16:33:38 +0000 Subject: [PATCH 342/801] Fix same bug in subsection --- src/core/operations/Subsection.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Subsection.mjs b/src/core/operations/Subsection.mjs index 8133d31c..548780c8 100644 --- a/src/core/operations/Subsection.mjs +++ b/src/core/operations/Subsection.mjs @@ -116,7 +116,7 @@ class Subsection extends Operation { } // Baseline ing values for each tranche so that registers are reset - subOpList.forEach((op, i) => { + recipe.opList.forEach((op, i) => { op.ingValues = JSON.parse(JSON.stringify(ingValues[i])); }); From 3e428c044ac6e6bc13b232d8fdb91f0c36875a78 Mon Sep 17 00:00:00 2001 From: j433866 Date: Fri, 8 Mar 2019 13:38:59 +0000 Subject: [PATCH 343/801] Add min values to operation args --- src/core/operations/GenerateQRCode.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/operations/GenerateQRCode.mjs b/src/core/operations/GenerateQRCode.mjs index 4e1983e5..d88eee15 100644 --- a/src/core/operations/GenerateQRCode.mjs +++ b/src/core/operations/GenerateQRCode.mjs @@ -38,12 +38,14 @@ class GenerateQRCode extends Operation { { "name": "Module size (px)", "type": "number", - "value": 5 + "value": 5, + "min": 1 }, { "name": "Margin (num modules)", "type": "number", - "value": 2 + "value": 2, + "min": 0 }, { "name": "Error correction", From 58d41f4458b4f442cf10e10b3bce9e95a7121366 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sat, 9 Mar 2019 05:38:13 +0000 Subject: [PATCH 344/801] 8.24.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d1cb4e1..8a35ebb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "8.24.2", + "version": "8.24.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a14af274..f8db4aa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "8.24.2", + "version": "8.24.3", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 84d31c1d597921ade565f30e60b887f2cc20ed4c Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sat, 9 Mar 2019 06:25:27 +0000 Subject: [PATCH 345/801] Added 'Move to input' button to output file list. Improved zlib extraction efficiency. --- .eslintrc.json | 1 + src/core/Utils.mjs | 31 +++++++++++++-- src/core/lib/FileSignatures.mjs | 50 +++++++++++++----------- src/core/lib/FileType.mjs | 7 +++- src/core/operations/ExtractFiles.mjs | 8 +++- src/web/Manager.mjs | 1 + src/web/OutputWaiter.mjs | 18 +++++++++ src/web/html/index.html | 2 +- src/web/stylesheets/components/_pane.css | 4 ++ 9 files changed, 94 insertions(+), 28 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d5e4e768..7dcb705c 100755 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -102,6 +102,7 @@ "$": false, "jQuery": false, "log": false, + "app": false, "COMPILE_TIME": false, "COMPILE_MSG": false, diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index f70e2941..8e69b020 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -832,8 +832,9 @@ class Utils { const buff = await Utils.readFile(file); const blob = new Blob( [buff], - {type: "octet/stream"} + {type: file.type || "octet/stream"} ); + const blobURL = URL.createObjectURL(blob); const html = `
@@ -1163,6 +1173,21 @@ String.prototype.count = function(chr) { }; +/** + * Wrapper for self.sendStatusMessage to handle different environments. + * + * @param {string} msg + */ +export function sendStatusMessage(msg) { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage(msg); + else if (ENVIRONMENT_IS_WEB()) + app.alert(msg, 10000); + else if (ENVIRONMENT_IS_NODE()) + log.debug(msg); +} + + /* * Polyfills */ diff --git a/src/core/lib/FileSignatures.mjs b/src/core/lib/FileSignatures.mjs index 36e6818e..61e37b88 100644 --- a/src/core/lib/FileSignatures.mjs +++ b/src/core/lib/FileSignatures.mjs @@ -1518,26 +1518,26 @@ export function extractELF(bytes, offset) { } +// Construct required Huffman Tables +const fixedLiteralTableLengths = new Array(288); +for (let i = 0; i < fixedLiteralTableLengths.length; i++) { + fixedLiteralTableLengths[i] = + (i <= 143) ? 8 : + (i <= 255) ? 9 : + (i <= 279) ? 7 : + 8; +} +const fixedLiteralTable = buildHuffmanTable(fixedLiteralTableLengths); +const fixedDistanceTableLengths = new Array(30).fill(5); +const fixedDistanceTable = buildHuffmanTable(fixedDistanceTableLengths); +const huffmanOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + /** * Steps through a DEFLATE stream * * @param {Stream} stream */ function parseDEFLATE(stream) { - // Construct required Huffman Tables - const fixedLiteralTableLengths = new Uint8Array(288); - for (let i = 0; i < fixedLiteralTableLengths.length; i++) { - fixedLiteralTableLengths[i] = - (i <= 143) ? 8 : - (i <= 255) ? 9 : - (i <= 279) ? 7 : - 8; - } - const fixedLiteralTable = buildHuffmanTable(fixedLiteralTableLengths); - const fixedDistanceTableLengths = new Uint8Array(30).fill(5); - const fixedDistanceTable = buildHuffmanTable(fixedDistanceTableLengths); - const huffmanOrder = new Uint8Array([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); - // Parse DEFLATE data let finalBlock = 0; @@ -1619,6 +1619,14 @@ function parseDEFLATE(stream) { } +// Static length tables +const lengthExtraTable = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 +]; +const distanceExtraTable = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 +]; + /** * Parses a Huffman Block given the literal and distance tables * @@ -1627,20 +1635,18 @@ function parseDEFLATE(stream) { * @param {Uint32Array} distTab */ function parseHuffmanBlock(stream, litTab, distTab) { - const lengthExtraTable = new Uint8Array([ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 - ]); - const distanceExtraTable = new Uint8Array([ - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 - ]); - let code; + let loops = 0; while ((code = readHuffmanCode(stream, litTab))) { // console.log("Code: " + code + " (" + Utils.chr(code) + ") " + Utils.bin(code)); // End of block if (code === 256) break; + // Detect probably infinite loops + if (++loops > 10000) + throw new Error("Caught in probable infinite loop while parsing Huffman Block"); + // Literal if (code < 256) continue; @@ -1657,7 +1663,7 @@ function parseHuffmanBlock(stream, litTab, distTab) { /** * Builds a Huffman table given the relevant code lengths * - * @param {Uint8Array} lengths + * @param {Array} lengths * @returns {Array} result * @returns {Uint32Array} result.table * @returns {number} result.maxCodeLength diff --git a/src/core/lib/FileType.mjs b/src/core/lib/FileType.mjs index e5d990d9..e961a76f 100644 --- a/src/core/lib/FileType.mjs +++ b/src/core/lib/FileType.mjs @@ -7,6 +7,7 @@ * */ import {FILE_SIGNATURES} from "./FileSignatures"; +import {sendStatusMessage} from "../Utils"; /** @@ -148,6 +149,7 @@ export function scanForFileTypes(buf, categories=Object.keys(FILE_SIGNATURES)) { let pos = 0; while ((pos = locatePotentialSig(buf, sig, pos)) >= 0) { if (bytesMatch(sig, buf, pos)) { + sendStatusMessage(`Found potential signature for ${filetype.name} at pos ${pos}`); foundFiles.push({ offset: pos, fileDetails: filetype @@ -249,9 +251,12 @@ export function isImage(buf) { */ export function extractFile(bytes, fileDetail, offset) { if (fileDetail.extractor) { + sendStatusMessage(`Attempting to extract ${fileDetail.name} at pos ${offset}...`); const fileData = fileDetail.extractor(bytes, offset); const ext = fileDetail.extension.split(",")[0]; - return new File([fileData], `extracted_at_0x${offset.toString(16)}.${ext}`); + return new File([fileData], `extracted_at_0x${offset.toString(16)}.${ext}`, { + type: fileDetail.mime + }); } throw new Error(`No extraction algorithm available for "${fileDetail.mime}" files`); diff --git a/src/core/operations/ExtractFiles.mjs b/src/core/operations/ExtractFiles.mjs index d2b87990..b9b260bb 100644 --- a/src/core/operations/ExtractFiles.mjs +++ b/src/core/operations/ExtractFiles.mjs @@ -62,12 +62,13 @@ class ExtractFiles extends Operation { // Extract each file that we support const files = []; + const errors = []; detectedFiles.forEach(detectedFile => { try { files.push(extractFile(bytes, detectedFile.fileDetails, detectedFile.offset)); } catch (err) { if (!ignoreFailedExtractions && err.message.indexOf("No extraction algorithm available") < 0) { - throw new OperationError( + errors.push( `Error while attempting to extract ${detectedFile.fileDetails.name} ` + `at offset ${detectedFile.offset}:\n` + `${err.message}` @@ -76,9 +77,14 @@ class ExtractFiles extends Operation { } }); + if (errors.length) { + throw new OperationError(errors.join("\n\n")); + } + return files; } + /** * Displays the files in HTML for web apps. * diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 30cb4943..5fa0e8c1 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -173,6 +173,7 @@ class Manager { this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output); this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output); document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output)); + this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output); // Options document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options)); diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs index 2d93507c..0a10b8b2 100755 --- a/src/web/OutputWaiter.mjs +++ b/src/web/OutputWaiter.mjs @@ -494,6 +494,24 @@ class OutputWaiter { magicButton.setAttribute("data-original-title", "Magic!"); } + + /** + * Handler for extract file events. + * + * @param {Event} e + */ + async extractFileClick(e) { + e.preventDefault(); + e.stopPropagation(); + + const el = e.target.nodeName === "I" ? e.target.parentNode : e.target; + const blobURL = el.getAttribute("blob-url"); + const fileName = el.getAttribute("file-name"); + + const blob = await fetch(blobURL).then(r => r.blob()); + this.manager.input.loadFile(new File([blob], fileName, {type: blob.type})); + } + } export default OutputWaiter; diff --git a/src/web/html/index.html b/src/web/html/index.html index 74eb0ed8..302355d9 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -271,7 +271,7 @@ content_copy
Rotor stopsPartial plugboardDecryption preview
${setting}${stecker}${decrypt}
"; + html += "
Rotor stopsPartial plugboardDecryption preview
\n"; for (const [setting, stecker, decrypt] of output.result) { - html += `\n`; + html += `\n`; } html += "
Rotor stops Partial plugboard Decryption preview
${setting}${stecker}${decrypt}
${setting} ${stecker} ${decrypt}
"; return html; diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 6887bc46..03364a01 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -292,9 +292,9 @@ class MultipleBombe extends Operation { for (const run of output.bombeRuns) { html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`; - html += ""; + html += "
Rotor stopsPartial plugboardDecryption preview
\n"; for (const [setting, stecker, decrypt] of run.result) { - html += `\n`; + html += `\n`; } html += "
Rotor stops Partial plugboard Decryption preview
${setting}${stecker}${decrypt}
${setting} ${stecker} ${decrypt}
\n"; } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 9e5a79c6..b44e032c 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -11,7 +11,7 @@ TestRegister.addTests([ // Plugboard for this test is BO LC KE GA name: "Bombe: 3 rotor (self-stecker)", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}SS<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -31,7 +31,7 @@ TestRegister.addTests([ // This test produces a menu that doesn't use the first letter, which is also a good test name: "Bombe: 3 rotor (other stecker)", input: "JBYALIHDYNUAAVKBYM", - expectedMatch: /LGA<\/td>AG<\/td>QFIMUMAFKMQSKMYNGW<\/td>/, + expectedMatch: /LGA<\/td> {2}AG<\/td> {2}QFIMUMAFKMQSKMYNGW<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -50,7 +50,7 @@ TestRegister.addTests([ { name: "Bombe: crib offset", input: "AAABBYFLTHHYIJQAYBBYS", // first three chars here are faked - expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}SS<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -69,7 +69,7 @@ TestRegister.addTests([ { name: "Bombe: multiple stops", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>TT<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}TT<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -88,7 +88,7 @@ TestRegister.addTests([ { name: "Bombe: checking machine", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>TT AG BO CL EK FF HH II JJ SS YY<\/td>THISISATESTMESSAGE<\/td>/, + expectedMatch: /LGA<\/td> {2}TT AG BO CL EK FF HH II JJ SS YY<\/td> {2}THISISATESTMESSAGE<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -108,7 +108,7 @@ TestRegister.addTests([ { name: "Bombe: 4 rotor", input: "LUOXGJSHGEDSRDOQQX", - expectedMatch: /LHSC<\/td>SS<\/td>HHHSSSGQUUQPKSEKWK<\/td>/, + expectedMatch: /LHSC<\/td> {2}SS<\/td> {2}HHHSSSGQUUQPKSEKWK<\/td>/, recipeConfig: [ { "op": "Bombe", diff --git a/tests/operations/tests/MultipleBombe.mjs b/tests/operations/tests/MultipleBombe.mjs index 8e2cc685..32d2db08 100644 --- a/tests/operations/tests/MultipleBombe.mjs +++ b/tests/operations/tests/MultipleBombe.mjs @@ -10,7 +10,7 @@ TestRegister.addTests([ { name: "Multi-Bombe: 3 rotor", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}SS<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Multiple Bombe", From cf32372a57e0cf4cf85c3b620979d229c1969895 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:08:35 +0000 Subject: [PATCH 372/801] Added Enigma wiki article link to Enigma, Typex, Bombe and Multi-Bombe operation descriptions. --- src/core/operations/Bombe.mjs | 2 +- src/core/operations/Enigma.mjs | 2 +- src/core/operations/MultipleBombe.mjs | 2 +- src/core/operations/Typex.mjs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 00d883ed..c2ea82bf 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -23,7 +23,7 @@ class Bombe extends Operation { this.name = "Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; + this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine.

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "JSON"; diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index 71593070..542e8281 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -22,7 +22,7 @@ class Enigma extends Operation { this.name = "Enigma"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Enigma machine.

Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot)."; + this.description = "Encipher/decipher with the WW2 Enigma machine.

Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot).

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Enigma_machine"; this.inputType = "string"; this.outputType = "string"; diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 03364a01..b6a48872 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -53,7 +53,7 @@ class MultipleBombe extends Operation { this.name = "Multiple Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.

You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib."; + this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.

You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib.

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "JSON"; diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 760914f5..70b5f6c3 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -23,7 +23,7 @@ class Typex extends Operation { this.name = "Typex"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Typex machine.

Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.

To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points."; + this.description = "Encipher/decipher with the WW2 Typex machine.

Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.

To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Typex"; this.inputType = "string"; this.outputType = "string"; From 33db0e666a5b2a0115118a0e6c6e0ebc94769c07 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:11:41 +0000 Subject: [PATCH 373/801] Final tweaks to Bombe svg and preloader css --- src/web/static/images/bombe.svg | 4 ++-- src/web/stylesheets/preloader.css | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/web/static/images/bombe.svg b/src/web/static/images/bombe.svg index 1fd40554..a970903a 100644 --- a/src/web/static/images/bombe.svg +++ b/src/web/static/images/bombe.svg @@ -1,8 +1,8 @@ diff --git a/src/web/stylesheets/preloader.css b/src/web/stylesheets/preloader.css index 690fe5c1..288ffc28 100755 --- a/src/web/stylesheets/preloader.css +++ b/src/web/stylesheets/preloader.css @@ -160,12 +160,3 @@ transform: translate3d(0, 200px, 0); } } - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} From ef38897a010208f5311850351c71218714294a26 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:20:05 +0000 Subject: [PATCH 374/801] Updated CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3eca29..0d2eca7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master). +### [8.26.0] - 2019-03-09 +- Various image manipulation operations added [@j433866] | [#506] + +### [8.25.0] - 2019-03-09 +- 'Extract Files' operation added and more file formats supported [@n1474335] | [#440] + ### [8.24.0] - 2019-02-08 - 'DNS over HTTPS' operation added [@h345983745] | [#489] @@ -106,6 +112,8 @@ All major and minor version changes will be documented in this file. Details of +[8.26.0]: https://github.com/gchq/CyberChef/releases/tag/v8.26.0 +[8.25.0]: https://github.com/gchq/CyberChef/releases/tag/v8.25.0 [8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0 [8.23.1]: https://github.com/gchq/CyberChef/releases/tag/v8.23.1 [8.23.0]: https://github.com/gchq/CyberChef/releases/tag/v8.23.0 @@ -180,6 +188,7 @@ All major and minor version changes will be documented in this file. Details of [#394]: https://github.com/gchq/CyberChef/pull/394 [#428]: https://github.com/gchq/CyberChef/pull/428 [#439]: https://github.com/gchq/CyberChef/pull/439 +[#440]: https://github.com/gchq/CyberChef/pull/440 [#441]: https://github.com/gchq/CyberChef/pull/441 [#443]: https://github.com/gchq/CyberChef/pull/443 [#446]: https://github.com/gchq/CyberChef/pull/446 @@ -192,3 +201,4 @@ All major and minor version changes will be documented in this file. Details of [#468]: https://github.com/gchq/CyberChef/pull/468 [#476]: https://github.com/gchq/CyberChef/pull/476 [#489]: https://github.com/gchq/CyberChef/pull/489 +[#506]: https://github.com/gchq/CyberChef/pull/506 From c8a2a8b003a31ddf9f0860e63db068972d3f820b Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:26:00 +0000 Subject: [PATCH 375/801] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2eca7e..b21944fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master). +### [8.27.0] - 2019-03-14 +- 'Enigma', 'Typex', 'Bombe' and 'Multiple Bombe' operations added [@s2224834] | [#516] +- See [this wiki article](https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex) for a full explanation of these operations. +- New Bombe-style loading animation added for long-running operations [@n1474335] +- New operation argument types added: `populateMultiOption` and `argSelector` [@n1474335] + ### [8.26.0] - 2019-03-09 - Various image manipulation operations added [@j433866] | [#506] From 3ff10bfeaebad28c72ceff53f8c78ebb4ebb0755 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:26:07 +0000 Subject: [PATCH 376/801] 8.27.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 425b3f76..9fd4a068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "8.26.3", + "version": "8.27.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 453c9d96..e650b272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "8.26.3", + "version": "8.27.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 3ad5f889a0621bdc523452f2d912f45418933b11 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 14 Mar 2019 13:37:11 +0000 Subject: [PATCH 377/801] Wrote some tests, fixed imports for node --- src/core/lib/Charts.mjs | 4 +- src/core/operations/HTMLToText.mjs | 41 ++++++++++++++++++ src/core/operations/HeatmapChart.mjs | 8 +++- src/core/operations/HexDensityChart.mjs | 12 ++++-- src/core/operations/ScatterChart.mjs | 9 +++- src/core/operations/SeriesChart.mjs | 8 +++- tests/operations/index.mjs | 1 + tests/operations/tests/Charts.mjs | 55 +++++++++++++++++++++++++ 8 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 src/core/operations/HTMLToText.mjs create mode 100644 tests/operations/tests/Charts.mjs diff --git a/src/core/lib/Charts.mjs b/src/core/lib/Charts.mjs index 1b9be128..fa3e5137 100644 --- a/src/core/lib/Charts.mjs +++ b/src/core/lib/Charts.mjs @@ -1,6 +1,6 @@ /** - * @author tlwr [toby@toby.codes] - Original - * @author Matt C [me@mitt.dev] - Conversion to new format + * @author tlwr [toby@toby.codes] + * @author Matt C [me@mitt.dev] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ diff --git a/src/core/operations/HTMLToText.mjs b/src/core/operations/HTMLToText.mjs new file mode 100644 index 00000000..a47ffc46 --- /dev/null +++ b/src/core/operations/HTMLToText.mjs @@ -0,0 +1,41 @@ +/** + * @author tlwr [toby@toby.codes] + * @author Matt C [me@mitt.dev] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; + +/** + * HTML To Text operation + */ +class HTMLToText extends Operation { + + /** + * HTMLToText constructor + */ + constructor() { + super(); + + this.name = "HTML To Text"; + this.module = "Default"; + this.description = "Converts a HTML ouput from an operation to a readable string instead of being rendered in the DOM."; + this.infoURL = ""; + this.inputType = "html"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {html} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + return input; + } + +} + +export default HTMLToText; diff --git a/src/core/operations/HeatmapChart.mjs b/src/core/operations/HeatmapChart.mjs index 6620e7aa..4cde1f30 100644 --- a/src/core/operations/HeatmapChart.mjs +++ b/src/core/operations/HeatmapChart.mjs @@ -1,11 +1,12 @@ /** * @author tlwr [toby@toby.codes] + * @author Matt C [me@mitt.dev] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ -import * as d3 from "d3"; -import * as nodom from "nodom"; +import * as d3temp from "d3"; +import * as nodomtemp from "nodom"; import { getScatterValues, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts"; @@ -13,6 +14,9 @@ import Operation from "../Operation"; import OperationError from "../errors/OperationError"; import Utils from "../Utils"; +const d3 = d3temp.default ? d3temp.default : d3temp; +const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp; + /** * Heatmap chart operation */ diff --git a/src/core/operations/HexDensityChart.mjs b/src/core/operations/HexDensityChart.mjs index c9912599..6414d97a 100644 --- a/src/core/operations/HexDensityChart.mjs +++ b/src/core/operations/HexDensityChart.mjs @@ -1,17 +1,23 @@ /** * @author tlwr [toby@toby.codes] + * @author Matt C [me@mitt.dev] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ -import * as d3 from "d3"; -import * as d3hexbin from "d3-hexbin"; -import * as nodom from "nodom"; +import * as d3temp from "d3"; +import * as d3hexbintemp from "d3-hexbin"; +import * as nodomtemp from "nodom"; import { getScatterValues, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts"; import Operation from "../Operation"; import Utils from "../Utils"; +const d3 = d3temp.default ? d3temp.default : d3temp; +const d3hexbin = d3hexbintemp.default ? d3hexbintemp.default : d3hexbintemp; +const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp; + + /** * Hex Density chart operation */ diff --git a/src/core/operations/ScatterChart.mjs b/src/core/operations/ScatterChart.mjs index fa642449..e6d0ec9d 100644 --- a/src/core/operations/ScatterChart.mjs +++ b/src/core/operations/ScatterChart.mjs @@ -1,16 +1,21 @@ /** * @author tlwr [toby@toby.codes] + * @author Matt C [me@mitt.dev] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ -import * as d3 from "d3"; -import * as nodom from "nodom"; +import * as d3temp from "d3"; +import * as nodomtemp from "nodom"; + import { getScatterValues, getScatterValuesWithColour, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts"; import Operation from "../Operation"; import Utils from "../Utils"; +const d3 = d3temp.default ? d3temp.default : d3temp; +const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp; + /** * Scatter chart operation */ diff --git a/src/core/operations/SeriesChart.mjs b/src/core/operations/SeriesChart.mjs index bccbc7ed..cdae32b7 100644 --- a/src/core/operations/SeriesChart.mjs +++ b/src/core/operations/SeriesChart.mjs @@ -1,16 +1,20 @@ /** * @author tlwr [toby@toby.codes] + * @author Matt C [me@mitt.dev] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ -import * as d3 from "d3"; -import * as nodom from "nodom"; +import * as d3temp from "d3"; +import * as nodomtemp from "nodom"; import { getSeriesValues, RECORD_DELIMITER_OPTIONS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts"; import Operation from "../Operation"; import Utils from "../Utils"; +const d3 = d3temp.default ? d3temp.default : d3temp; +const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp; + /** * Series chart operation */ diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index fb68ed9c..817529c8 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -33,6 +33,7 @@ import "./tests/BitwiseOp"; import "./tests/ByteRepr"; import "./tests/CartesianProduct"; import "./tests/CharEnc"; +import "./tests/Charts"; import "./tests/Checksum"; import "./tests/Ciphers"; import "./tests/Code"; diff --git a/tests/operations/tests/Charts.mjs b/tests/operations/tests/Charts.mjs new file mode 100644 index 00000000..3bd5c4fd --- /dev/null +++ b/tests/operations/tests/Charts.mjs @@ -0,0 +1,55 @@ +/** + * Chart tests. + * + * @author Matt C [me@mitt.dev] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + name: "Scatter chart", + input: "100 100\n200 200\n300 300\n400 400\n500 500", + expectedMatch: /^ Date: Thu, 14 Mar 2019 16:08:25 +0000 Subject: [PATCH 378/801] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b21944fe..11a18d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ All major and minor version changes will be documented in this file. Details of +[8.27.0]: https://github.com/gchq/CyberChef/releases/tag/v8.27.0 [8.26.0]: https://github.com/gchq/CyberChef/releases/tag/v8.26.0 [8.25.0]: https://github.com/gchq/CyberChef/releases/tag/v8.25.0 [8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0 From 2019ae43d7fe12956b5145d11a68713284039782 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Thu, 14 Mar 2019 16:33:09 +0000 Subject: [PATCH 379/801] File shim now translates correctly --- package.json | 2 +- src/core/Dish.mjs | 2 -- src/core/Utils.mjs | 11 +++++------ src/core/dishTranslationTypes/DishFile.mjs | 11 ++--------- src/core/dishTranslationTypes/DishString.mjs | 2 -- src/core/operations/Tar.mjs | 2 -- src/node/File.mjs | 12 ++++++------ 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index caf10d64..c300d179 100644 --- a/package.json +++ b/package.json @@ -144,11 +144,11 @@ "build": "grunt prod", "node": "NODE_ENV=development grunt node", "node-prod": "NODE_ENV=production grunt node", + "repl": "grunt node && node build/node/CyberChef-repl.js", "test": "grunt test", "testui": "grunt testui", "docs": "grunt docs", "lint": "grunt lint", - "repl": "node --experimental-modules --no-warnings src/node/repl-index.mjs", "newop": "node --experimental-modules src/core/config/scripts/newOperation.mjs", "postinstall": "[ -f node_modules/crypto-api/src/crypto-api.mjs ] || npx j2m node_modules/crypto-api/src/crypto-api.js" } diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 104bbc2b..452be80d 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -336,13 +336,11 @@ class Dish { // Node environment => translate is sync if (Utils.isNode()) { - console.log('Running in node'); this._toByteArray(); this._fromByteArray(toType, notUTF8); // Browser environment => translate is async } else { - console.log('Running in browser'); return new Promise((resolve, reject) => { this._toByteArray() .then(() => this.type = Dish.BYTE_ARRAY) diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index b61357de..979b6482 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -472,7 +472,6 @@ class Utils { const str = Utils.byteArrayToChars(byteArray); try { const utf8Str = utf8.decode(str); - if (str.length !== utf8Str.length) { if (ENVIRONMENT_IS_WORKER()) { self.setOption("attemptHighlight", false); @@ -966,12 +965,12 @@ class Utils { if (!Utils.isNode()) { throw new TypeError("Browser environment cannot support readFileSync"); } + let bytes = []; + for (const byte of file.data.values()) { + bytes = bytes.concat(byte); + } - console.log('readFileSync:'); - console.log(file); - console.log(Buffer.from(file.data).toString()); - - return Buffer.from(file.data).buffer; + return bytes; } diff --git a/src/core/dishTranslationTypes/DishFile.mjs b/src/core/dishTranslationTypes/DishFile.mjs index b04bd462..9e1df730 100644 --- a/src/core/dishTranslationTypes/DishFile.mjs +++ b/src/core/dishTranslationTypes/DishFile.mjs @@ -19,12 +19,7 @@ class DishFile extends DishTranslationType { static toByteArray() { DishFile.checkForValue(this.value); if (Utils.isNode()) { - console.log('toByteArray original value:'); - console.log(this.value); - // this.value = Utils.readFileSync(this.value); - this.value = Array.prototype.slice.call(Utils.readFileSync(this.value)); - console.log('toByteArray value:'); - console.log(this.value); + this.value = Utils.readFileSync(this.value); } else { return new Promise((resolve, reject) => { Utils.readFile(this.value) @@ -42,9 +37,7 @@ class DishFile extends DishTranslationType { */ static fromByteArray() { DishFile.checkForValue(this.value); - this.value = new File(this.value, "unknown"); - console.log('from Byte array'); - console.log(this.value); + this.value = new File(this.value, "file.txt"); } } diff --git a/src/core/dishTranslationTypes/DishString.mjs b/src/core/dishTranslationTypes/DishString.mjs index 40b23001..78c273c6 100644 --- a/src/core/dishTranslationTypes/DishString.mjs +++ b/src/core/dishTranslationTypes/DishString.mjs @@ -17,10 +17,8 @@ class DishString extends DishTranslationType { * convert the given value to a ByteArray */ static toByteArray() { - console.log('string to byte array'); DishString.checkForValue(this.value); this.value = this.value ? Utils.strToByteArray(this.value) : []; - console.log(this.value); } /** diff --git a/src/core/operations/Tar.mjs b/src/core/operations/Tar.mjs index 10748340..84674bff 100644 --- a/src/core/operations/Tar.mjs +++ b/src/core/operations/Tar.mjs @@ -132,8 +132,6 @@ class Tar extends Operation { tarball.writeBytes(input); tarball.writeEndBlocks(); - console.log('Tar bytes'); - console.log(tarball.bytes); return new File([new Uint8Array(tarball.bytes)], args[0]); } diff --git a/src/node/File.mjs b/src/node/File.mjs index 938c8fd4..33c0dc73 100644 --- a/src/node/File.mjs +++ b/src/node/File.mjs @@ -19,21 +19,21 @@ class File { /** * Constructor * + * https://w3c.github.io/FileAPI/#file-constructor + * * @param {String|Array|ArrayBuffer|Buffer} bits - file content * @param {String} name (optional) - file name * @param {Object} stats (optional) - file stats e.g. lastModified */ constructor(data, name="", stats={}) { - // Look at File API definition to see how to handle this. - this.data = Buffer.from(data[0]); + const buffers = data.map(d => Buffer.from(d)); + const totalLength = buffers.reduce((p, c) => p + c.length, 0); + this.data = Buffer.concat(buffers, totalLength); + this.name = name; this.lastModified = stats.lastModified || Date.now(); this.type = stats.type || mime.getType(this.name); - console.log('File constructor'); - console.log(typeof data); - console.log(data); - console.log(this.data); } /** From b8cb7e9ba828fe4e9a15c0b9d8f5ffa4d5b2d147 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Thu, 14 Mar 2019 17:54:06 +0000 Subject: [PATCH 380/801] add tests for File and test based operations. Only unzip to go --- src/node/File.mjs | 8 +++++ src/node/config/excludedOperations.mjs | 6 ---- tests/node/index.mjs | 1 + tests/node/tests/File.mjs | 20 +++++++++++ tests/node/tests/nodeApi.mjs | 2 +- tests/node/tests/ops.mjs | 46 ++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tests/node/tests/File.mjs diff --git a/src/node/File.mjs b/src/node/File.mjs index 33c0dc73..1d0234ae 100644 --- a/src/node/File.mjs +++ b/src/node/File.mjs @@ -42,6 +42,14 @@ class File { get size() { return this.data.length; } + + /** + * Return lastModified as Date + */ + get lastModifiedDate() { + return new Date(this.lastModified); + } + } export default File; diff --git a/src/node/config/excludedOperations.mjs b/src/node/config/excludedOperations.mjs index 53cdfa95..e4ec6390 100644 --- a/src/node/config/excludedOperations.mjs +++ b/src/node/config/excludedOperations.mjs @@ -13,12 +13,6 @@ export default [ "Label", "Comment", - // Exclude file ops until HTML5 File Object can be mimicked - // "Tar", - // "Untar", - "Unzip", - "Zip", - // esprima doesn't work in .mjs "JavaScriptBeautify", "JavaScriptMinify", diff --git a/tests/node/index.mjs b/tests/node/index.mjs index 112525cc..c78a2d9c 100644 --- a/tests/node/index.mjs +++ b/tests/node/index.mjs @@ -30,6 +30,7 @@ global.ENVIRONMENT_IS_WEB = function() { import TestRegister from "../lib/TestRegister"; import "./tests/nodeApi"; import "./tests/ops"; +import "./tests/File"; const testStatus = { allTestsPassing: true, diff --git a/tests/node/tests/File.mjs b/tests/node/tests/File.mjs new file mode 100644 index 00000000..dc9ddffc --- /dev/null +++ b/tests/node/tests/File.mjs @@ -0,0 +1,20 @@ +import assert from "assert"; +import it from "../assertionHandler"; +import TestRegister from "../../lib/TestRegister"; +import File from "../../../src/node/File"; + +TestRegister.addApiTests([ + it("File: should exist", () => { + assert(File); + }), + + it("File: Should have same properties as DOM File object", () => { + const uint8Array = new Uint8Array(Buffer.from("hello")); + const file = new File([uint8Array], "name.txt"); + assert.equal(file.name, "name.txt"); + assert(typeof file.lastModified, "number"); + assert(file.lastModifiedDate instanceof Date); + assert.equal(file.size, uint8Array.length); + assert.equal(file.type, "text/plain"); + }), +]); diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs index 11361893..2bd07231 100644 --- a/tests/node/tests/nodeApi.mjs +++ b/tests/node/tests/nodeApi.mjs @@ -387,7 +387,7 @@ TestRegister.addApiTests([ it("Operation arguments: should be accessible from operation object if op has array arg", () => { assert.ok(chef.toCharcode.argOptions); - assert.equal(chef.unzip.argOptions, undefined); + assert.deepEqual(chef.unzip.argOptions, {}); }), it("Operation arguments: should have key for each array-based argument in operation", () => { diff --git a/tests/node/tests/ops.mjs b/tests/node/tests/ops.mjs index 9f621cec..8952cfde 100644 --- a/tests/node/tests/ops.mjs +++ b/tests/node/tests/ops.mjs @@ -35,6 +35,9 @@ import { } from "../../../src/node/index"; import chef from "../../../src/node/index"; import TestRegister from "../../lib/TestRegister"; +import File from "../../../src/node/File"; + +global.File = File; TestRegister.addApiTests([ @@ -971,5 +974,48 @@ ExifImageWidth: 57 ExifImageHeight: 57`); }), + it("Tar", () => { + const tarred = chef.tar("some file content", { + filename: "test.txt" + }); + assert.strictEqual(tarred.type, 7); + assert.strictEqual(tarred.value.size, 2048); + assert.strictEqual(tarred.value.data.toString().substr(0, 8), "test.txt"); + }), + + it("Untar", () => { + const tarred = chef.tar("some file content", { + filename: "filename.txt", + }); + const untarred = chef.untar(tarred); + assert.strictEqual(untarred.type, 8); + assert.strictEqual(untarred.value.length, 1); + assert.strictEqual(untarred.value[0].name, "filename.txt"); + assert.strictEqual(untarred.value[0].data.toString(), "some file content"); + }), + + it("Zip", () => { + const zipped = chef.zip("some file content", { + filename: "sample.zip", + comment: "added", + operaringSystem: "Unix", + }); + + assert.strictEqual(zipped.type, 7); + assert.equal(zipped.value.data.toString().indexOf("sample.zip"), 30); + assert.equal(zipped.value.data.toString().indexOf("added"), 122); + }), + + // it("Unzip", () => { + // const zipped = chef.zip("some file content", { + // filename: "zipped.zip", + // comment: "zippy", + // }); + // const unzipped = chef.unzip(zipped); + + // assert.equal(unzipped.type, 8); + // assert.equal(unzipped.value = "zipped.zip"); + // }), + ]); From a5703cb4f151a5d75f6bee5cdc6af77463f61529 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 15 Mar 2019 15:17:15 +0000 Subject: [PATCH 381/801] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a18d86..06a5eb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ All major and minor version changes will be documented in this file. Details of [@j433866]: https://github.com/j433866 [@GCHQ77703]: https://github.com/GCHQ77703 [@h345983745]: https://github.com/h345983745 +[@s2224834]: https://github.com/s2224834 [@artemisbot]: https://github.com/artemisbot [@picapi]: https://github.com/picapi [@Dachande663]: https://github.com/Dachande663 @@ -209,3 +210,4 @@ All major and minor version changes will be documented in this file. Details of [#476]: https://github.com/gchq/CyberChef/pull/476 [#489]: https://github.com/gchq/CyberChef/pull/489 [#506]: https://github.com/gchq/CyberChef/pull/506 +[#516]: https://github.com/gchq/CyberChef/pull/516 From 8e74acbf3e56a2e00b7f9abac6559bd91352d929 Mon Sep 17 00:00:00 2001 From: j433866 Date: Mon, 18 Mar 2019 09:43:37 +0000 Subject: [PATCH 382/801] Add opaque background option --- src/core/operations/ContainImage.mjs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/core/operations/ContainImage.mjs b/src/core/operations/ContainImage.mjs index c6df81ef..4cb7cfdf 100644 --- a/src/core/operations/ContainImage.mjs +++ b/src/core/operations/ContainImage.mjs @@ -72,6 +72,11 @@ class ContainImage extends Operation { "Bezier" ], defaultIndex: 1 + }, + { + name: "Opaque background", + type: "boolean", + value: true } ]; } @@ -82,7 +87,7 @@ class ContainImage extends Operation { * @returns {byteArray} */ async run(input, args) { - const [width, height, hAlign, vAlign, alg] = args; + const [width, height, hAlign, vAlign, alg, opaqueBg] = args; const resizeMap = { "Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR, @@ -115,6 +120,13 @@ class ContainImage extends Operation { if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Containing image..."); image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]); + + if (opaqueBg) { + const newImage = await jimp.read(width, height, 0x000000FF); + newImage.blit(image, 0, 0); + image = newImage; + } + const imageBuffer = await image.getBufferAsync(jimp.AUTO); return [...imageBuffer]; } catch (err) { From b3d92b04cb4a2e2bcd5c30ad20880aa64015811c Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 19 Mar 2019 11:24:29 +0000 Subject: [PATCH 383/801] Updated nodom dependency to upstream --- package-lock.json | 27 +++++++++++++++++++-------- package.json | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5026aec..fca640e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5438,12 +5438,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5458,17 +5460,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5585,7 +5590,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5597,6 +5603,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5611,6 +5618,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5722,7 +5730,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5855,6 +5864,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9253,8 +9263,9 @@ } }, "nodom": { - "version": "github:artemisbot/nodom#0071b2fa25cbc74e14c7d911cda9b03ea26eac7b", - "from": "github:artemisbot/nodom" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nodom/-/nodom-2.2.0.tgz", + "integrity": "sha512-+W3jlsobV3NNkO15xQXkWoboeq1RPa/SKi8NMHmWF33SCMX4ALcM5dpPLEnUs69Gu+uZoCX9wcWXy866LXvd8w==" }, "nomnom": { "version": "1.5.2", diff --git a/package.json b/package.json index 61e9d4ed..dda5a279 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "ngeohash": "^0.6.3", "node-forge": "^0.7.6", "node-md6": "^0.1.0", - "nodom": "github:artemisbot/nodom", + "nodom": "^2.2.0", "notepack.io": "^2.2.0", "nwmatcher": "^1.4.4", "otp": "^0.1.3", From ce72acdd613cd281da622e0c3bfe71338ee06f51 Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 19 Mar 2019 13:53:09 +0000 Subject: [PATCH 384/801] Add 'add text to image' operation. Included variants of the Roboto fonts as bitmap fonts for jimp. Changed webpack config to import the font files. --- src/core/config/Categories.json | 3 +- src/core/operations/AddTextToImage.mjs | 257 +++++++++ .../static/fonts/bmfonts/Roboto72White.fnt | 485 +++++++++++++++++ .../static/fonts/bmfonts/Roboto72White.png | Bin 0 -> 52730 bytes .../fonts/bmfonts/RobotoBlack72White.fnt | 488 +++++++++++++++++ .../fonts/bmfonts/RobotoBlack72White.png | Bin 0 -> 50192 bytes .../fonts/bmfonts/RobotoMono72White.fnt | 103 ++++ .../fonts/bmfonts/RobotoMono72White.png | Bin 0 -> 52580 bytes .../fonts/bmfonts/RobotoSlab72White.fnt | 492 ++++++++++++++++++ .../fonts/bmfonts/RobotoSlab72White.png | Bin 0 -> 54282 bytes webpack.config.js | 2 +- 11 files changed, 1828 insertions(+), 2 deletions(-) create mode 100644 src/core/operations/AddTextToImage.mjs create mode 100644 src/web/static/fonts/bmfonts/Roboto72White.fnt create mode 100644 src/web/static/fonts/bmfonts/Roboto72White.png create mode 100644 src/web/static/fonts/bmfonts/RobotoBlack72White.fnt create mode 100644 src/web/static/fonts/bmfonts/RobotoBlack72White.png create mode 100644 src/web/static/fonts/bmfonts/RobotoMono72White.fnt create mode 100644 src/web/static/fonts/bmfonts/RobotoMono72White.png create mode 100644 src/web/static/fonts/bmfonts/RobotoSlab72White.fnt create mode 100644 src/web/static/fonts/bmfonts/RobotoSlab72White.png diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 92f74212..4bd40aa4 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -376,7 +376,8 @@ "Cover Image", "Image Hue/Saturation/Lightness", "Sharpen Image", - "Convert image format" + "Convert Image Format", + "Add Text To Image" ] }, { diff --git a/src/core/operations/AddTextToImage.mjs b/src/core/operations/AddTextToImage.mjs new file mode 100644 index 00000000..f8ee3485 --- /dev/null +++ b/src/core/operations/AddTextToImage.mjs @@ -0,0 +1,257 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import { isImage } from "../lib/FileType"; +import { toBase64 } from "../lib/Base64"; +import jimp from "jimp"; + +/** + * Add Text To Image operation + */ +class AddTextToImage extends Operation { + + /** + * AddTextToImage constructor + */ + constructor() { + super(); + + this.name = "Add Text To Image"; + this.module = "Image"; + this.description = "Adds text onto an image.

Text can be horizontally or vertically aligned, or the position can be manually specified.
Variants of the Roboto font face are available in any size or colour.

Note: This may cause a degradation in image quality, especially when using font sizes larger than 72."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Text", + type: "string", + value: "" + }, + { + name: "Horizontal align", + type: "option", + value: ["None", "Left", "Center", "Right"] + }, + { + name: "Vertical align", + type: "option", + value: ["None", "Top", "Middle", "Bottom"] + }, + { + name: "X position", + type: "number", + value: 0 + }, + { + name: "Y position", + type: "number", + value: 0 + }, + { + name: "Size", + type: "number", + value: 32, + min: 8 + }, + { + name: "Font face", + type: "option", + value: [ + "Roboto", + "Roboto Black", + "Roboto Mono", + "Roboto Slab" + ] + }, + { + name: "Red", + type: "number", + value: 255, + min: 0, + max: 255 + }, + { + name: "Green", + type: "number", + value: 255, + min: 0, + max: 255 + }, + { + name: "Blue", + type: "number", + value: 255, + min: 0, + max: 255 + }, + { + name: "Alpha", + type: "number", + value: 255, + min: 0, + max: 255 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const text = args[0], + hAlign = args[1], + vAlign = args[2], + size = args[5], + fontFace = args[6], + red = args[7], + green = args[8], + blue = args[9], + alpha = args[10]; + + let xPos = args[3], + yPos = args[4]; + + if (!isImage(input)) { + throw new OperationError("Invalid file type."); + } + + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Adding text to image..."); + + const fontsMap = { + "Roboto": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"), + "Roboto Black": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"), + "Roboto Mono": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"), + "Roboto Slab": await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt") + }; + + // Make Webpack load the png font images + await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"); + await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png"); + await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png"); + await import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png"); + + const font = fontsMap[fontFace]; + + // LoadFont needs an absolute url, so append the font name to self.docURL + const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default); + + jimpFont.pages.forEach(function(page) { + if (page.bitmap) { + // Adjust the RGB values of the image pages to change the font colour. + const pageWidth = page.bitmap.width; + const pageHeight = page.bitmap.height; + for (let ix = 0; ix < pageWidth; ix++) { + for (let iy = 0; iy < pageHeight; iy++) { + const idx = (iy * pageWidth + ix) << 2; + + const newRed = page.bitmap.data[idx] - (255 - red); + const newGreen = page.bitmap.data[idx + 1] - (255 - green); + const newBlue = page.bitmap.data[idx + 2] - (255 - blue); + const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha); + + // Make sure the bitmap values don't go below 0 as that makes jimp very unhappy + page.bitmap.data[idx] = (newRed > 0) ? newRed : 0; + page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0; + page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0; + page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0; + } + } + } + }); + + // Scale the image to a factor of 72, so we can print the text at any size + const scaleFactor = 72 / size; + if (size !== 72) { + // Use bicubic for decreasing size + if (size > 72) { + image.scale(scaleFactor, jimp.RESIZE_BICUBIC); + } else { + image.scale(scaleFactor, jimp.RESIZE_BILINEAR); + } + } + + // If using the alignment options, calculate the pixel values AFTER the image has been scaled + switch (hAlign) { + case "Left": + xPos = 0; + break; + case "Center": + xPos = (image.getWidth() / 2) - (jimp.measureText(jimpFont, text) / 2); + break; + case "Right": + xPos = image.getWidth() - jimp.measureText(jimpFont, text); + break; + default: + // Adjust x position for the scaled image + xPos = xPos * scaleFactor; + } + + switch (vAlign) { + case "Top": + yPos = 0; + break; + case "Middle": + yPos = (image.getHeight() / 2) - (jimp.measureTextHeight(jimpFont, text) / 2); + break; + case "Bottom": + yPos = image.getHeight() - jimp.measureTextHeight(jimpFont, text); + break; + default: + // Adjust y position for the scaled image + yPos = yPos * scaleFactor; + } + + image.print(jimpFont, xPos, yPos, text); + + if (size !== 72) { + if (size > 72) { + image.scale(1 / scaleFactor, jimp.RESIZE_BILINEAR); + } else { + image.scale(1 / scaleFactor, jimp.RESIZE_BICUBIC); + } + } + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error adding text to image. (${err})`); + } + } + + /** + * Displays the blurred image using HTML for web apps + * + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + const type = isImage(data); + if (!type) { + throw new OperationError("Invalid file type."); + } + + return ``; + } + +} + +export default AddTextToImage; diff --git a/src/web/static/fonts/bmfonts/Roboto72White.fnt b/src/web/static/fonts/bmfonts/Roboto72White.fnt new file mode 100644 index 00000000..fd186892 --- /dev/null +++ b/src/web/static/fonts/bmfonts/Roboto72White.fnt @@ -0,0 +1,485 @@ +info face="Roboto" size=72 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=1,1,1,1 spacing=-2,-2 +common lineHeight=85 base=67 scaleW=512 scaleH=512 pages=1 packed=0 +page id=0 file="images/Roboto72White.png" +chars count=98 +char id=0 x=0 y=0 width=0 height=0 xoffset=-1 yoffset=66 xadvance=0 page=0 chnl=0 +char id=10 x=0 y=0 width=70 height=99 xoffset=2 yoffset=-11 xadvance=74 page=0 chnl=0 +char id=32 x=0 y=0 width=0 height=0 xoffset=-1 yoffset=66 xadvance=18 page=0 chnl=0 +char id=33 x=493 y=99 width=10 height=55 xoffset=5 yoffset=14 xadvance=19 page=0 chnl=0 +char id=34 x=446 y=319 width=16 height=19 xoffset=4 yoffset=12 xadvance=23 page=0 chnl=0 +char id=35 x=204 y=265 width=41 height=54 xoffset=3 yoffset=14 xadvance=44 page=0 chnl=0 +char id=36 x=269 y=0 width=35 height=69 xoffset=3 yoffset=6 xadvance=40 page=0 chnl=0 +char id=37 x=31 y=155 width=48 height=56 xoffset=3 yoffset=13 xadvance=53 page=0 chnl=0 +char id=38 x=79 y=155 width=43 height=56 xoffset=3 yoffset=13 xadvance=45 page=0 chnl=0 +char id=39 x=503 y=99 width=7 height=19 xoffset=3 yoffset=12 xadvance=13 page=0 chnl=0 +char id=40 x=70 y=0 width=21 height=78 xoffset=4 yoffset=7 xadvance=25 page=0 chnl=0 +char id=41 x=91 y=0 width=22 height=78 xoffset=-1 yoffset=7 xadvance=25 page=0 chnl=0 +char id=42 x=342 y=319 width=32 height=32 xoffset=-1 yoffset=14 xadvance=31 page=0 chnl=0 +char id=43 x=242 y=319 width=37 height=40 xoffset=2 yoffset=23 xadvance=41 page=0 chnl=0 +char id=44 x=433 y=319 width=13 height=21 xoffset=-1 yoffset=58 xadvance=14 page=0 chnl=0 +char id=45 x=27 y=360 width=19 height=8 xoffset=0 yoffset=41 xadvance=19 page=0 chnl=0 +char id=46 x=17 y=360 width=10 height=11 xoffset=4 yoffset=58 xadvance=19 page=0 chnl=0 +char id=47 x=355 y=0 width=30 height=58 xoffset=-1 yoffset=14 xadvance=30 page=0 chnl=0 +char id=48 x=449 y=99 width=34 height=56 xoffset=3 yoffset=13 xadvance=40 page=0 chnl=0 +char id=49 x=474 y=211 width=22 height=54 xoffset=5 yoffset=14 xadvance=40 page=0 chnl=0 +char id=50 x=195 y=155 width=37 height=55 xoffset=2 yoffset=13 xadvance=41 page=0 chnl=0 +char id=51 x=379 y=99 width=35 height=56 xoffset=2 yoffset=13 xadvance=40 page=0 chnl=0 +char id=52 x=128 y=265 width=39 height=54 xoffset=1 yoffset=14 xadvance=41 page=0 chnl=0 +char id=53 x=232 y=155 width=35 height=55 xoffset=4 yoffset=14 xadvance=40 page=0 chnl=0 +char id=54 x=267 y=155 width=35 height=55 xoffset=4 yoffset=14 xadvance=41 page=0 chnl=0 +char id=55 x=167 y=265 width=37 height=54 xoffset=2 yoffset=14 xadvance=41 page=0 chnl=0 +char id=56 x=414 y=99 width=35 height=56 xoffset=3 yoffset=13 xadvance=40 page=0 chnl=0 +char id=57 x=302 y=155 width=34 height=55 xoffset=3 yoffset=13 xadvance=41 page=0 chnl=0 +char id=58 x=495 y=265 width=10 height=41 xoffset=4 yoffset=28 xadvance=18 page=0 chnl=0 +char id=59 x=496 y=211 width=13 height=52 xoffset=0 yoffset=28 xadvance=15 page=0 chnl=0 +char id=60 x=279 y=319 width=31 height=35 xoffset=2 yoffset=27 xadvance=37 page=0 chnl=0 +char id=61 x=402 y=319 width=31 height=23 xoffset=4 yoffset=31 xadvance=39 page=0 chnl=0 +char id=62 x=310 y=319 width=32 height=35 xoffset=4 yoffset=27 xadvance=38 page=0 chnl=0 +char id=63 x=0 y=155 width=31 height=56 xoffset=2 yoffset=13 xadvance=34 page=0 chnl=0 +char id=64 x=210 y=0 width=59 height=69 xoffset=3 yoffset=15 xadvance=65 page=0 chnl=0 +char id=65 x=336 y=155 width=49 height=54 xoffset=-1 yoffset=14 xadvance=47 page=0 chnl=0 +char id=66 x=385 y=155 width=37 height=54 xoffset=5 yoffset=14 xadvance=45 page=0 chnl=0 +char id=67 x=0 y=99 width=42 height=56 xoffset=3 yoffset=13 xadvance=46 page=0 chnl=0 +char id=68 x=422 y=155 width=39 height=54 xoffset=5 yoffset=14 xadvance=47 page=0 chnl=0 +char id=69 x=461 y=155 width=35 height=54 xoffset=5 yoffset=14 xadvance=41 page=0 chnl=0 +char id=70 x=0 y=211 width=34 height=54 xoffset=5 yoffset=14 xadvance=40 page=0 chnl=0 +char id=71 x=42 y=99 width=42 height=56 xoffset=3 yoffset=13 xadvance=49 page=0 chnl=0 +char id=72 x=34 y=211 width=41 height=54 xoffset=5 yoffset=14 xadvance=51 page=0 chnl=0 +char id=73 x=496 y=155 width=9 height=54 xoffset=5 yoffset=14 xadvance=19 page=0 chnl=0 +char id=74 x=122 y=155 width=34 height=55 xoffset=1 yoffset=14 xadvance=40 page=0 chnl=0 +char id=75 x=75 y=211 width=41 height=54 xoffset=5 yoffset=14 xadvance=45 page=0 chnl=0 +char id=76 x=116 y=211 width=33 height=54 xoffset=5 yoffset=14 xadvance=39 page=0 chnl=0 +char id=77 x=149 y=211 width=53 height=54 xoffset=5 yoffset=14 xadvance=63 page=0 chnl=0 +char id=78 x=202 y=211 width=41 height=54 xoffset=5 yoffset=14 xadvance=51 page=0 chnl=0 +char id=79 x=84 y=99 width=43 height=56 xoffset=3 yoffset=13 xadvance=49 page=0 chnl=0 +char id=80 x=243 y=211 width=39 height=54 xoffset=5 yoffset=14 xadvance=45 page=0 chnl=0 +char id=81 x=304 y=0 width=44 height=64 xoffset=3 yoffset=13 xadvance=49 page=0 chnl=0 +char id=82 x=282 y=211 width=40 height=54 xoffset=5 yoffset=14 xadvance=45 page=0 chnl=0 +char id=83 x=127 y=99 width=39 height=56 xoffset=2 yoffset=13 xadvance=43 page=0 chnl=0 +char id=84 x=322 y=211 width=42 height=54 xoffset=1 yoffset=14 xadvance=44 page=0 chnl=0 +char id=85 x=156 y=155 width=39 height=55 xoffset=4 yoffset=14 xadvance=47 page=0 chnl=0 +char id=86 x=364 y=211 width=47 height=54 xoffset=-1 yoffset=14 xadvance=46 page=0 chnl=0 +char id=87 x=411 y=211 width=63 height=54 xoffset=1 yoffset=14 xadvance=64 page=0 chnl=0 +char id=88 x=0 y=265 width=44 height=54 xoffset=1 yoffset=14 xadvance=45 page=0 chnl=0 +char id=89 x=44 y=265 width=45 height=54 xoffset=-1 yoffset=14 xadvance=43 page=0 chnl=0 +char id=90 x=89 y=265 width=39 height=54 xoffset=2 yoffset=14 xadvance=43 page=0 chnl=0 +char id=91 x=161 y=0 width=16 height=72 xoffset=4 yoffset=7 xadvance=19 page=0 chnl=0 +char id=92 x=385 y=0 width=30 height=58 xoffset=0 yoffset=14 xadvance=30 page=0 chnl=0 +char id=93 x=177 y=0 width=16 height=72 xoffset=0 yoffset=7 xadvance=20 page=0 chnl=0 +char id=94 x=374 y=319 width=28 height=28 xoffset=1 yoffset=14 xadvance=30 page=0 chnl=0 +char id=95 x=46 y=360 width=34 height=8 xoffset=0 yoffset=65 xadvance=34 page=0 chnl=0 +char id=96 x=0 y=360 width=17 height=13 xoffset=1 yoffset=11 xadvance=22 page=0 chnl=0 +char id=97 x=268 y=265 width=34 height=42 xoffset=3 yoffset=27 xadvance=39 page=0 chnl=0 +char id=98 x=415 y=0 width=34 height=57 xoffset=4 yoffset=12 xadvance=40 page=0 chnl=0 +char id=99 x=302 y=265 width=34 height=42 xoffset=2 yoffset=27 xadvance=38 page=0 chnl=0 +char id=100 x=449 y=0 width=34 height=57 xoffset=2 yoffset=12 xadvance=40 page=0 chnl=0 +char id=101 x=336 y=265 width=34 height=42 xoffset=2 yoffset=27 xadvance=38 page=0 chnl=0 +char id=102 x=483 y=0 width=25 height=57 xoffset=1 yoffset=11 xadvance=26 page=0 chnl=0 +char id=103 x=166 y=99 width=34 height=56 xoffset=2 yoffset=27 xadvance=40 page=0 chnl=0 +char id=104 x=200 y=99 width=32 height=56 xoffset=4 yoffset=12 xadvance=40 page=0 chnl=0 +char id=105 x=483 y=99 width=10 height=55 xoffset=4 yoffset=13 xadvance=18 page=0 chnl=0 +char id=106 x=193 y=0 width=17 height=71 xoffset=-4 yoffset=13 xadvance=17 page=0 chnl=0 +char id=107 x=232 y=99 width=34 height=56 xoffset=4 yoffset=12 xadvance=37 page=0 chnl=0 +char id=108 x=266 y=99 width=9 height=56 xoffset=4 yoffset=12 xadvance=17 page=0 chnl=0 +char id=109 x=439 y=265 width=56 height=41 xoffset=4 yoffset=27 xadvance=64 page=0 chnl=0 +char id=110 x=0 y=319 width=32 height=41 xoffset=4 yoffset=27 xadvance=40 page=0 chnl=0 +char id=111 x=370 y=265 width=37 height=42 xoffset=2 yoffset=27 xadvance=41 page=0 chnl=0 +char id=112 x=275 y=99 width=34 height=56 xoffset=4 yoffset=27 xadvance=40 page=0 chnl=0 +char id=113 x=309 y=99 width=34 height=56 xoffset=2 yoffset=27 xadvance=41 page=0 chnl=0 +char id=114 x=32 y=319 width=21 height=41 xoffset=4 yoffset=27 xadvance=25 page=0 chnl=0 +char id=115 x=407 y=265 width=32 height=42 xoffset=2 yoffset=27 xadvance=37 page=0 chnl=0 +char id=116 x=245 y=265 width=23 height=51 xoffset=0 yoffset=18 xadvance=25 page=0 chnl=0 +char id=117 x=53 y=319 width=32 height=41 xoffset=4 yoffset=28 xadvance=40 page=0 chnl=0 +char id=118 x=85 y=319 width=35 height=40 xoffset=0 yoffset=28 xadvance=35 page=0 chnl=0 +char id=119 x=120 y=319 width=54 height=40 xoffset=0 yoffset=28 xadvance=54 page=0 chnl=0 +char id=120 x=174 y=319 width=36 height=40 xoffset=0 yoffset=28 xadvance=36 page=0 chnl=0 +char id=121 x=343 y=99 width=36 height=56 xoffset=-1 yoffset=28 xadvance=34 page=0 chnl=0 +char id=122 x=210 y=319 width=32 height=40 xoffset=2 yoffset=28 xadvance=35 page=0 chnl=0 +char id=123 x=113 y=0 width=24 height=73 xoffset=1 yoffset=9 xadvance=25 page=0 chnl=0 +char id=124 x=348 y=0 width=7 height=63 xoffset=5 yoffset=14 xadvance=17 page=0 chnl=0 +char id=125 x=137 y=0 width=24 height=73 xoffset=-1 yoffset=9 xadvance=24 page=0 chnl=0 +char id=126 x=462 y=319 width=42 height=16 xoffset=4 yoffset=38 xadvance=50 page=0 chnl=0 +char id=127 x=0 y=0 width=70 height=99 xoffset=2 yoffset=-11 xadvance=74 page=0 chnl=0 +kernings count=382 +kerning first=70 second=74 amount=-9 +kerning first=34 second=97 amount=-2 +kerning first=34 second=101 amount=-2 +kerning first=34 second=113 amount=-2 +kerning first=34 second=99 amount=-2 +kerning first=70 second=99 amount=-1 +kerning first=88 second=113 amount=-1 +kerning first=84 second=46 amount=-8 +kerning first=84 second=119 amount=-2 +kerning first=87 second=97 amount=-1 +kerning first=90 second=117 amount=-1 +kerning first=39 second=97 amount=-2 +kerning first=69 second=111 amount=-1 +kerning first=87 second=41 amount=1 +kerning first=76 second=86 amount=-6 +kerning first=121 second=34 amount=1 +kerning first=40 second=86 amount=1 +kerning first=85 second=65 amount=-1 +kerning first=89 second=89 amount=1 +kerning first=72 second=65 amount=1 +kerning first=104 second=39 amount=-4 +kerning first=114 second=102 amount=1 +kerning first=89 second=42 amount=-2 +kerning first=114 second=34 amount=1 +kerning first=84 second=115 amount=-4 +kerning first=84 second=71 amount=-1 +kerning first=89 second=101 amount=-2 +kerning first=89 second=45 amount=-2 +kerning first=122 second=99 amount=-1 +kerning first=78 second=88 amount=1 +kerning first=68 second=89 amount=-2 +kerning first=122 second=103 amount=-1 +kerning first=78 second=84 amount=-1 +kerning first=86 second=103 amount=-2 +kerning first=89 second=67 amount=-1 +kerning first=89 second=79 amount=-1 +kerning first=75 second=111 amount=-1 +kerning first=111 second=120 amount=-1 +kerning first=87 second=44 amount=-4 +kerning first=91 second=74 amount=-1 +kerning first=120 second=111 amount=-1 +kerning first=84 second=111 amount=-3 +kerning first=102 second=113 amount=-1 +kerning first=80 second=88 amount=-1 +kerning first=66 second=84 amount=-1 +kerning first=65 second=87 amount=-2 +kerning first=86 second=100 amount=-2 +kerning first=122 second=100 amount=-1 +kerning first=75 second=118 amount=-1 +kerning first=70 second=118 amount=-1 +kerning first=73 second=88 amount=1 +kerning first=70 second=121 amount=-1 +kerning first=65 second=34 amount=-4 +kerning first=39 second=101 amount=-2 +kerning first=75 second=101 amount=-1 +kerning first=84 second=99 amount=-3 +kerning first=84 second=65 amount=-3 +kerning first=112 second=39 amount=-1 +kerning first=76 second=39 amount=-12 +kerning first=78 second=65 amount=1 +kerning first=88 second=45 amount=-2 +kerning first=65 second=121 amount=-2 +kerning first=34 second=111 amount=-2 +kerning first=89 second=85 amount=-3 +kerning first=114 second=99 amount=-1 +kerning first=86 second=125 amount=1 +kerning first=70 second=111 amount=-1 +kerning first=89 second=120 amount=-1 +kerning first=90 second=119 amount=-1 +kerning first=120 second=99 amount=-1 +kerning first=89 second=117 amount=-1 +kerning first=82 second=89 amount=-2 +kerning first=75 second=117 amount=-1 +kerning first=34 second=34 amount=-4 +kerning first=89 second=110 amount=-1 +kerning first=88 second=101 amount=-1 +kerning first=107 second=103 amount=-1 +kerning first=34 second=115 amount=-3 +kerning first=98 second=39 amount=-1 +kerning first=70 second=65 amount=-6 +kerning first=70 second=46 amount=-8 +kerning first=98 second=34 amount=-1 +kerning first=70 second=84 amount=1 +kerning first=114 second=100 amount=-1 +kerning first=88 second=79 amount=-1 +kerning first=39 second=113 amount=-2 +kerning first=114 second=103 amount=-1 +kerning first=77 second=65 amount=1 +kerning first=120 second=103 amount=-1 +kerning first=114 second=121 amount=1 +kerning first=89 second=100 amount=-2 +kerning first=80 second=65 amount=-5 +kerning first=121 second=111 amount=-1 +kerning first=84 second=74 amount=-8 +kerning first=122 second=111 amount=-1 +kerning first=114 second=118 amount=1 +kerning first=102 second=41 amount=1 +kerning first=122 second=113 amount=-1 +kerning first=89 second=122 amount=-1 +kerning first=89 second=38 amount=-1 +kerning first=81 second=89 amount=-1 +kerning first=114 second=111 amount=-1 +kerning first=46 second=34 amount=-6 +kerning first=84 second=112 amount=-4 +kerning first=112 second=34 amount=-1 +kerning first=76 second=34 amount=-12 +kerning first=102 second=125 amount=1 +kerning first=39 second=115 amount=-3 +kerning first=76 second=118 amount=-5 +kerning first=86 second=99 amount=-2 +kerning first=84 second=84 amount=1 +kerning first=86 second=65 amount=-3 +kerning first=87 second=101 amount=-1 +kerning first=67 second=125 amount=-1 +kerning first=120 second=113 amount=-1 +kerning first=118 second=46 amount=-4 +kerning first=88 second=103 amount=-1 +kerning first=111 second=122 amount=-1 +kerning first=77 second=84 amount=-1 +kerning first=114 second=46 amount=-4 +kerning first=34 second=39 amount=-4 +kerning first=114 second=44 amount=-4 +kerning first=69 second=84 amount=1 +kerning first=89 second=46 amount=-7 +kerning first=97 second=39 amount=-2 +kerning first=34 second=100 amount=-2 +kerning first=70 second=100 amount=-1 +kerning first=84 second=120 amount=-3 +kerning first=90 second=118 amount=-1 +kerning first=70 second=114 amount=-1 +kerning first=34 second=112 amount=-1 +kerning first=109 second=34 amount=-4 +kerning first=86 second=113 amount=-2 +kerning first=88 second=71 amount=-1 +kerning first=66 second=89 amount=-2 +kerning first=102 second=103 amount=-1 +kerning first=88 second=67 amount=-1 +kerning first=39 second=110 amount=-1 +kerning first=75 second=110 amount=-1 +kerning first=88 second=117 amount=-1 +kerning first=89 second=118 amount=-1 +kerning first=97 second=118 amount=-1 +kerning first=87 second=65 amount=-2 +kerning first=73 second=89 amount=-1 +kerning first=89 second=74 amount=-3 +kerning first=102 second=101 amount=-1 +kerning first=86 second=111 amount=-2 +kerning first=65 second=119 amount=-1 +kerning first=84 second=100 amount=-3 +kerning first=104 second=34 amount=-4 +kerning first=86 second=41 amount=1 +kerning first=111 second=34 amount=-5 +kerning first=40 second=89 amount=1 +kerning first=121 second=39 amount=1 +kerning first=68 second=90 amount=-1 +kerning first=114 second=113 amount=-1 +kerning first=68 second=88 amount=-1 +kerning first=98 second=120 amount=-1 +kerning first=110 second=34 amount=-4 +kerning first=119 second=44 amount=-4 +kerning first=119 second=46 amount=-4 +kerning first=118 second=44 amount=-4 +kerning first=84 second=114 amount=-3 +kerning first=86 second=97 amount=-2 +kerning first=68 second=86 amount=-1 +kerning first=86 second=93 amount=1 +kerning first=97 second=34 amount=-2 +kerning first=34 second=65 amount=-4 +kerning first=84 second=118 amount=-3 +kerning first=76 second=84 amount=-10 +kerning first=107 second=99 amount=-1 +kerning first=121 second=46 amount=-4 +kerning first=123 second=85 amount=-1 +kerning first=65 second=63 amount=-2 +kerning first=89 second=44 amount=-7 +kerning first=80 second=118 amount=1 +kerning first=112 second=122 amount=-1 +kerning first=79 second=65 amount=-1 +kerning first=80 second=121 amount=1 +kerning first=118 second=34 amount=1 +kerning first=87 second=45 amount=-2 +kerning first=69 second=100 amount=-1 +kerning first=87 second=103 amount=-1 +kerning first=112 second=120 amount=-1 +kerning first=68 second=44 amount=-4 +kerning first=86 second=45 amount=-1 +kerning first=39 second=34 amount=-4 +kerning first=68 second=46 amount=-4 +kerning first=65 second=89 amount=-3 +kerning first=69 second=118 amount=-1 +kerning first=88 second=99 amount=-1 +kerning first=87 second=46 amount=-4 +kerning first=47 second=47 amount=-8 +kerning first=73 second=65 amount=1 +kerning first=123 second=74 amount=-1 +kerning first=69 second=102 amount=-1 +kerning first=87 second=111 amount=-1 +kerning first=39 second=112 amount=-1 +kerning first=89 second=116 amount=-1 +kerning first=70 second=113 amount=-1 +kerning first=77 second=88 amount=1 +kerning first=84 second=32 amount=-1 +kerning first=90 second=103 amount=-1 +kerning first=65 second=86 amount=-3 +kerning first=75 second=112 amount=-1 +kerning first=39 second=109 amount=-1 +kerning first=75 second=81 amount=-1 +kerning first=89 second=115 amount=-2 +kerning first=84 second=83 amount=-1 +kerning first=89 second=87 amount=1 +kerning first=114 second=101 amount=-1 +kerning first=116 second=111 amount=-1 +kerning first=90 second=100 amount=-1 +kerning first=84 second=122 amount=-2 +kerning first=68 second=84 amount=-1 +kerning first=32 second=84 amount=-1 +kerning first=84 second=117 amount=-3 +kerning first=74 second=65 amount=-1 +kerning first=107 second=101 amount=-1 +kerning first=75 second=109 amount=-1 +kerning first=80 second=46 amount=-11 +kerning first=89 second=93 amount=1 +kerning first=89 second=65 amount=-3 +kerning first=87 second=117 amount=-1 +kerning first=89 second=81 amount=-1 +kerning first=39 second=103 amount=-2 +kerning first=86 second=101 amount=-2 +kerning first=86 second=117 amount=-1 +kerning first=84 second=113 amount=-3 +kerning first=34 second=110 amount=-1 +kerning first=89 second=84 amount=1 +kerning first=84 second=110 amount=-4 +kerning first=39 second=99 amount=-2 +kerning first=88 second=121 amount=-1 +kerning first=65 second=39 amount=-4 +kerning first=110 second=39 amount=-4 +kerning first=75 second=67 amount=-1 +kerning first=88 second=118 amount=-1 +kerning first=86 second=114 amount=-1 +kerning first=80 second=74 amount=-7 +kerning first=84 second=97 amount=-4 +kerning first=82 second=84 amount=-3 +kerning first=91 second=85 amount=-1 +kerning first=102 second=99 amount=-1 +kerning first=66 second=86 amount=-1 +kerning first=120 second=101 amount=-1 +kerning first=102 second=93 amount=1 +kerning first=75 second=100 amount=-1 +kerning first=84 second=79 amount=-1 +kerning first=111 second=121 amount=-1 +kerning first=75 second=121 amount=-1 +kerning first=81 second=87 amount=-1 +kerning first=107 second=113 amount=-1 +kerning first=120 second=100 amount=-1 +kerning first=90 second=79 amount=-1 +kerning first=89 second=114 amount=-1 +kerning first=122 second=101 amount=-1 +kerning first=111 second=118 amount=-1 +kerning first=82 second=86 amount=-1 +kerning first=67 second=84 amount=-1 +kerning first=70 second=101 amount=-1 +kerning first=89 second=83 amount=-1 +kerning first=114 second=97 amount=-1 +kerning first=70 second=97 amount=-1 +kerning first=89 second=102 amount=-1 +kerning first=78 second=89 amount=-1 +kerning first=70 second=44 amount=-8 +kerning first=44 second=39 amount=-6 +kerning first=84 second=45 amount=-8 +kerning first=89 second=121 amount=-1 +kerning first=84 second=86 amount=1 +kerning first=87 second=99 amount=-1 +kerning first=98 second=122 amount=-1 +kerning first=89 second=112 amount=-1 +kerning first=89 second=103 amount=-2 +kerning first=88 second=81 amount=-1 +kerning first=102 second=34 amount=1 +kerning first=109 second=39 amount=-4 +kerning first=81 second=84 amount=-2 +kerning first=121 second=97 amount=-1 +kerning first=89 second=99 amount=-2 +kerning first=89 second=125 amount=1 +kerning first=81 second=86 amount=-1 +kerning first=114 second=116 amount=2 +kerning first=114 second=119 amount=1 +kerning first=84 second=44 amount=-8 +kerning first=102 second=39 amount=1 +kerning first=44 second=34 amount=-6 +kerning first=34 second=109 amount=-1 +kerning first=75 second=119 amount=-2 +kerning first=76 second=65 amount=1 +kerning first=84 second=81 amount=-1 +kerning first=76 second=121 amount=-5 +kerning first=69 second=101 amount=-1 +kerning first=89 second=111 amount=-2 +kerning first=80 second=90 amount=-1 +kerning first=89 second=97 amount=-3 +kerning first=89 second=109 amount=-1 +kerning first=90 second=99 amount=-1 +kerning first=89 second=86 amount=1 +kerning first=79 second=88 amount=-1 +kerning first=70 second=103 amount=-1 +kerning first=34 second=103 amount=-2 +kerning first=84 second=67 amount=-1 +kerning first=76 second=79 amount=-2 +kerning first=89 second=41 amount=1 +kerning first=65 second=118 amount=-2 +kerning first=75 second=71 amount=-1 +kerning first=76 second=87 amount=-5 +kerning first=77 second=89 amount=-1 +kerning first=90 second=113 amount=-1 +kerning first=79 second=89 amount=-2 +kerning first=118 second=111 amount=-1 +kerning first=118 second=97 amount=-1 +kerning first=88 second=100 amount=-1 +kerning first=90 second=121 amount=-1 +kerning first=89 second=113 amount=-2 +kerning first=84 second=87 amount=1 +kerning first=39 second=111 amount=-2 +kerning first=80 second=44 amount=-11 +kerning first=39 second=100 amount=-2 +kerning first=75 second=113 amount=-1 +kerning first=88 second=111 amount=-1 +kerning first=84 second=89 amount=1 +kerning first=84 second=103 amount=-3 +kerning first=70 second=117 amount=-1 +kerning first=67 second=41 amount=-1 +kerning first=89 second=71 amount=-1 +kerning first=121 second=44 amount=-4 +kerning first=97 second=121 amount=-1 +kerning first=87 second=113 amount=-1 +kerning first=73 second=84 amount=-1 +kerning first=84 second=101 amount=-3 +kerning first=75 second=99 amount=-1 +kerning first=65 second=85 amount=-1 +kerning first=76 second=67 amount=-2 +kerning first=76 second=81 amount=-2 +kerning first=75 second=79 amount=-1 +kerning first=39 second=65 amount=-4 +kerning first=76 second=117 amount=-2 +kerning first=65 second=84 amount=-5 +kerning first=90 second=101 amount=-1 +kerning first=84 second=121 amount=-3 +kerning first=69 second=99 amount=-1 +kerning first=114 second=39 amount=1 +kerning first=84 second=109 amount=-4 +kerning first=76 second=119 amount=-3 +kerning first=76 second=85 amount=-2 +kerning first=65 second=116 amount=-1 +kerning first=76 second=71 amount=-2 +kerning first=79 second=90 amount=-1 +kerning first=107 second=100 amount=-1 +kerning first=90 second=111 amount=-1 +kerning first=79 second=44 amount=-4 +kerning first=75 second=45 amount=-2 +kerning first=40 second=87 amount=1 +kerning first=79 second=86 amount=-1 +kerning first=102 second=100 amount=-1 +kerning first=72 second=89 amount=-1 +kerning first=72 second=88 amount=1 +kerning first=79 second=46 amount=-4 +kerning first=76 second=89 amount=-8 +kerning first=68 second=65 amount=-1 +kerning first=79 second=84 amount=-1 +kerning first=87 second=100 amount=-1 +kerning first=75 second=103 amount=-1 +kerning first=90 second=67 amount=-1 +kerning first=69 second=103 amount=-1 +kerning first=90 second=71 amount=-1 +kerning first=86 second=44 amount=-8 +kerning first=69 second=121 amount=-1 +kerning first=87 second=114 amount=-1 +kerning first=118 second=39 amount=1 +kerning first=46 second=39 amount=-6 +kerning first=72 second=84 amount=-1 +kerning first=86 second=46 amount=-8 +kerning first=69 second=113 amount=-1 +kerning first=69 second=119 amount=-1 +kerning first=39 second=39 amount=-4 +kerning first=69 second=117 amount=-1 +kerning first=111 second=39 amount=-5 +kerning first=90 second=81 amount=-1 diff --git a/src/web/static/fonts/bmfonts/Roboto72White.png b/src/web/static/fonts/bmfonts/Roboto72White.png new file mode 100644 index 0000000000000000000000000000000000000000..423a3a7e942054465e40a69e813b5e1fcf993fbb GIT binary patch literal 52730 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
DJ*`trAiv3hD_b^$+XeDqKt`D1&GH5qx zpMbZ-l_Gytv_I=&7+ce7Q@JRfrP`UxeOlm(-L4I5=nWSp3uS+=d(G&2QsR>pjLnyM zZ^qJbCHheSVF3^$`?t=ef| zwlW3sE>)GH#^orBszwgoC%LAj2AnLO&{^yxY*UiJ4GJb*AY|gMl(-6H$UlMi zVA#P}^dRpoRABi+FXC?p%5%Kr5R{wG-B%4ZyD~`muj*EE*0!j|->T;*o2BCjTfst0 z^!AVc+@mthl|e8%RT6o;lX+!!Xl+PnlIXGz=EM>A$w z6K(5I>{9HBk0JL=Femgd%K@zL%a5J|#v;Tq_C|NgHl4cq2->x>XZJ05O{u=YNa2*acO zEYu=Ec{T+0{SA<}_HrbzLjuXk%~nxE^yj`K`fAQx0<$&Ofs0`V(a-*Z`SX)}zuqUeAqly7qsA~2 z!DpV^I}erOPI9O0V|xy&>WDzUc;ju9R+l0KFk?@1ee|4L=tML2vIApH240zlT9-`8 z%PKUKWs^P^jIpe(*Y*tV>+}~5#R+J6)clc*g&NvU@Rz3ktyMtn%a?!6tdwg>vUh#C zE^13(Ff=7td>eC)=IvBzM5k3Jf!{p^&1wsBkHAwi+Y$ z`?lW*A@gJp#7_Ar7(x1WX+#?wo_#sal?O^%9L+WbAkxwGY1 zv~Wu}--(=lXcq!tA6eWwOUcj(?C8PM{pL|86N^PYl_Re}K%q=M-1#|hn_2)NhahYX zA4R^pB7=18H5f=O#Abz@?^v05;URTLQ*+hTQvAYxt~n%4yjAaD024W5Bq6ZJ#^I*$ z95x!*jxU-}=b;*ZB#;+AW$=XV(kD zIhcjz-HnxzoyN4bLAny|*5N31f^)Kqf0zLc&{^2pRTo--WhL>EZLK!Oiqb|Cse>9V zVSxFoJ4tLN8}?Q^l72g6$$M?FN^UR2a7IFlqk+?x*n3$=GqC%_NN)LWY6P^14HTz@4n)K2 zAD(hKX5hoS%}++SP0d!Rk)22MBjH~w1k;IBX<#lIf;}7Fh#`j(b&~Z{|FFbr)3zWb zs34ZlX@v|ry-2^8?tkN^2n^E1!7Y;ToSAc|Kdzt)$!+7vycYeyix8)%^Vxw)X=G>d zbqFxP%=+-FP5QpfiLSWE?1-D&{WWYmm~8f^GzLj1=4mRc5Ey43-s5J3*Sb5{E<#hb z71;q3w&^1u*W*_peGE8`8d-RxQ^eRZDv-kP7sW=N$B1FEwOnq872h&Ka4oJxcox<@ zw}z2P>WVDtkw;*}sm56_Fqv0o%x}Eb?eSHNR6o_jT9rhIh@%vhw@9@7kfEon`yD z2UR7G{*&-{o23EwTDp$ioN9I)*jLyq`N;4sjbGqQ|LHlGE_~;&WKyF-A8Bs;2~&+C zv7j8n&-*&eV;fny42wXg@jbNUJqsrrS*y#r%_G8q(l3NYZ$P4&R!|69h^ZSTo_ynF z8Q;Cx`f!S+56+v)S9duDj~Xj06+=U8Y>t@@KVmuubQ&FnH_V810TbixDB1&-8Df+Q zUM;Af<_}|Ur^jlH^#kuDLq8c(*NJ)&?qTi)fmvGBo+uC*jy-O^zJ) z%KIbB9OAvPBYm;!gzcxC6&IwA0+G}SsiyMi9cU>EYP)i}Mi=LyK!2&(A9gSTQO)~( znR6M~ZOwZ^y-$Uc%THVq_W32Gi>{Tq0DY84U1Xn7lAA%6^X5n@0D*`-aYrB?o2r(B zaWkxN97jAb+-;itDH^sPBlnm0MqeaR;86h6O6qMz&qsf)l9#B-O>6P`9z#z_8i&hV zht%p*N(~4BmJqj_{YzyQs=3?J^cgRT+|qXle#5Q{7KQqZ>;(j$}G1Fin3deyB1mDn$pWFYamml)p`dbcc> zrCPIUli`J>J-za&+OF1(#r1lZh1$ltQ*n1D&5}f$c{#2TjJSv?Vj0paqj~a>eUl{T z`zZutmIJ6Tmwt;1UNM-8GyS=lHgYP0y_j*&m1JO0>g#W4e(XMrrsmiDL;DP#C%adQ zD&u&sBF{dia-MvlYfB;w_|74IgO46GKd=v-vuTzd_=VtW#vdGZtyjrgb30Jj)f$Iu zD9)Tql{~%4z^wmvZ{J3V)!__Qh$J~saP3vRHg)p2hGTO+{&94|-;&PWn!dhtku93Dm(Q8rLwF~$gAB z>~?icGx?;#ib7xk8shZwj?K{-Z&?{Zh-J3gR)#w})$%W~b8z_h+jBZ^>c zHCt~&vHZGc7Kd)v;F*i0(C?Vg&va5Id_%dKGW~`FrxF3T0_)d#-;+rKky+2c(W^b3*h_#)(_xk(|Y$i z0}TJD{zBABN2qAbc=6YPJ`V0TyWSbaFRc*e9f)80)}c_hPZ&08|9RS628UmxdXt~_ zDN;qAh$kVO8wN!9uZJS-E-Z1>SLDsSJVABv>N?pzFJVANN>$@_p&p=GT!j36yn{M+N!M*6l7*q=b^i}Z#`N^voUj~wmEAOsuANa%doU*3N*}ePQ;Hu9J<)(I--#`hA|Lv7elp-9dfO0vEl= z_N-UPWBWE=eyvEIJS3bo^*hGBV{y*fV+FLnk94y-3;p=ImUHSxDZAma@!iQO9oW8+ z{IfRW3V_Ua|F-QabF)_8ar+?zW_P9Q`@-I{x)fm95;8npAln2fU*C(}SZA`dyaWS0 zzV0$O;To7+jxV_xKwpgygVLvpZsXVCMT7cN6)-bK-7=EL)~mk+qhPR2>QWqvKjgS^ zc&f}2$56N>-dM3R^ZW9kpP?W24Z^CQ4gEO7V14Hk$mTmZYBqqMD&-qM?;SP}J1Uj1 zWGnsX$~d(71r@$8I;cO_+|OOway(EE8L)3M+A8czLt$ zqly3`#mQ&ufqpv&o+!)*{yd9LDq>9FM?E)!8!P%ti2|2{z{q2JtaC~T#BTIV8w-`~ z-XZz!PM%`9qLV4(kH`wAln`pIq40?!o}H(^Dqfj0iap~@2Bpr&QK;J=N)vwNxNY)C zSI0n>`ZU3}C&Gta=$-H`RO>;)`DiR(+k$5FE%&J73=}{mVn z_6o|p30YSG-8`gEI-GY=mn@Q+io~7~VHNjT8Y$UtIF_xf$sH_wKoTuLA6FYE)YYZc zsxS(@NlogC1S9X$QR1>$ zkvhvAxP8MuTc`*|!Nk0f{ZMH*f`a;RL3A7G`{v1+(OCs=FA;hnqj8XRV-@$PZ(mG8 z$^R30aNyPQqB^9b!o{EJi&@L;Swu*jaHxj^){D{Fr|?qG9>`@;`?lY0)qwMOfqE8XaHY30?#uQ+#{dvEt=K_5+WIyf4C*v%{6y*Z_USyMt$4@t53sr`Wkk>K z&S=K}Z8>@t*3#zrz#3&TD9+zK2>PgKuZ6)!_}DP?;9D4btktvY5iwpzJU(`@a}l(h zze#Zl)s6w4!rOVXzsfg^>a*sjnBza|tQ%!$5b!9A=wQIng2Rr#gF8Js|8mGfxu7pu zVq*0vLRp)aS(E9H84tmJ!g+TKy?)BrDb_HNF3!?Rp z2B4E>JWhzPPRxuX_e+6tY@CjQC+E@3UV#(ri*|K?WtFG3t;Q#_hd6|2@3|cB`mlbw z7lf#ltrCu^k)y8az5jdv?vnX#q!hJ%p*=`%f7Xv(&p1oGXtvoyk*(=Ztv6RR4{>%? zh?SyRN_#t1rCP>rV;trP09Yn*nizeMPd zjt%cGx}Gf%5vI(u7F?Kz<~CB{erEV~*7y#fT+aswr(CZfEur~0is6e^=ce?d6vk2! zB7gTv3re9loeUKzCEvSEY29`^*WY|vD|~Xs(rTIMw;j}TeX^421MTJ{qp-rE4EP3k zoDm1c z%5F}=KTqcO?k&c}^F-yV3~Za+dVO%Y4h{=eqQpT}`lV~ku}NOL>sWbn^4jLx9l9q-n6r4hJ^7S@Ir5I+G3?^V~cv2^|*2Cc>;i=2Fg$1&`nkez+s zPc?B9iG@s);tYAZ7W*El{VOYa)O}%j%kW}HJIEg_1 zEn3Ul>qWjXN>+rW#X9;wuLx5ucRtNKy`z-b(mNwRyp_Q^EltxlPer-WlDy<2sqI8B z0*xj1H!4spgKkp(J&bj{(P%vFM8n+<1puYTd_b7>y1Hs@7DJ`rOh#GjuA|s6nU8=5 z7P0LfYMdS+cuUyT2=;vMqhl-$sIb<<)7$@`8GIYRY>P<&*ny0x!wY;}=T#m=N<0(j=*_pRAz`D?dl1l@-8kpK@(wqAU7$hNW+Tz5 zYu@YYg_ASQ_Tmhx$#iD&W!e)ov{jl9p$IvHafypdVmRi3n!yDJtj;GUi!ZRMV5r=X(L@AFXp+FnXxjoLNtMViiZ5l zkfK|Hr=Ti5o@F^SHtuvR_yI8Bl*unO) z@>b`g>iv=@!;l`qXhsW)${8<_- z<&vTIB2`RHDh2g@W~wwo7)m9kIoEMY?H{9D^|3+~J%uu=>MLL{|2MQF`^aLjN-gmMA-uIDrbsn z`m~k^a#!12q4Ti*+ppOoF=!&f3A)7=kiq#H8+4ELp?&K z!L9X6@J%Ta?y4w=vRSV;$(;YDi$@9zN&)(i(zv{vSrluKByb~7Y4ru}vUMR8%pywf z89+eE+mx{5M-k)^KUPBH3rjpE+C!-XD>D`SZ;D_j^_ zMi@_vt-t&s7?^L{hu_f$=Tkp(9`$MWNA?se@#@BF5#+s%{s5yy|pp# zjDiD|N;mj|;Y?I9*$bm%bAmQr!vy0ipFmi3eOplD`yn#=b3!jm#|o_^#sBDFeOE~% z?KZc1zpd9@+NB$fc^m8|$V?ePfsuFdKI? zw;}vS`w~YsNV;|wrrvLo@mc0q5B;-H`RD=tpDaUSUlDt8wEU|zt9HYKt&|I`iVUys zGdAjqp2GK_lb>CgTsI%Sm?^dGO6*1MC|cAUH;!i|YH8}g7C_F~j2%0_r1*JkSDeIG zk+8-3KZPNGPFvbJH&E{%cX=Q*1DQ@-=YxBKrMv!Q7YclBSHo?f~39@ml zPUTwk^E>3i7-=2uJC0K~-M~!bIl}?)D|}93*E!r@=94YFVwgYQH0<#I+I!DvxV!gX zcn~E-glHi~LbM4%^d3S8(L2#1x+p=6-lGIVqDAx;y+jS8lhJ#N=%cq848|yPCU@@p z_q)&kS^?PzQNuWQICu)g26_vz)3|wtZ5T6=LT9o!t zOM-e0JaK;iRvRd?^~BkLeTu+@uE?hFUl34Jt;BlCYBlelAoSlsPBZDgsP}}>J12I= z)7-4GYU(~;mAz38Q7mz&+L+_F8*oFhg9Ah@e$%u!`(x3>Y1`PtEsQ;$-zZo}aO zS;C(ebBmTRw{O&YkBItsa-I9&>`c4l54*h#LcW9ktAl5mrrFm`c|D-ht1(gEW8V~a zO3=Rf#%YYb%Ec@WY@Tcfyze5dksrXG(gGfzCXH;1daC@hhw5RaEsf}L=i(Ocu}pr4 zx;5E&{5D;a*+#IZXySXoxz?z%!*FkiTf=imr}&JqU>Vi){w8mY)}L_t4tb_o)h>P4 z>ZRZsd5PRMTzGO>$~)5RqT+%w}6fTn}itE570;#&xK##q-$spkP z?9Y(tiLajbj!fwX@x;TNR_b}z_Uwg2tb4wkh~^1^JCm2)R&M*>q5V&USCeiI`nJeR z@v1WAqXNbxZ*ptic!^jb4=U~Bdu
S+w%D?H<@a0u$i>hNein+WUgNLCn5Fa+_?<@g;X_oC8zedJ# z&%oOqx3zqgkbul~R?{m@T&wP(i)7m!NnwvbVw9M-xqoYT#?!?y)vJ?ys1qbkz-M>t zPP1PVI?K=yrv5v0`cUBV<1!{cZ?`>u`u)>m=8E3k$_@KAi3Lvv-+0S4shIO$?|K+t zU_!4tg$g;hEK?XnPqlFN0Vtt2@*Vc$G1c4b32p`sX#BC(7r@+EF_QBo99cFY3xq1X z;G6!1FR{O6S>rz5>Q}Uwpyt$OX=HO^O@8YOzh}sA!sR~@%px_(`Xui;DG@9*CrOdL zV4B!c3>A7RxFGRk?rdzV?R)ZJB?wTj^P*Rf@rS%3?e3X*;~)0P1~oIwiEGpsAIuZ* zcK3)2Z&I3(t#0ZXVY@h~=G#MvZ7mT=7I;`YFQmOOcQV)hO@IME;s`Ja465awlnZYG zt?r@V(xq$fr}XV#AM5YL7u2$BDHCj%_H`Bmz?Kmi2|M(+nM^7-Lbm>v_fJ;6<|sY~ zxXp>}_C1QxZ+WTZ;ngGgxbZ3rpQkb&ri_t;KL{HYCxt!w*Bs@rCWjYvMdO}!Z9al2 zWe*%__9ou+9x!Ts^$>E|&zE#3Z2M(=4KZlNbK`bm)@uau_{1Mxt6I@few2YizLfdp7xB?43s}wSUt^Pk-oqWBZp)qpmBAUn-A{&$Ezw zW7BHdTR`ITv|b_iR0gUZWDpJeB`F%>A+1$)cxc#n1RutN`5yiWFpP&0@n0xqcockb z>B$30$M0$j!FL4;33mhyO=fYPv{NJU-2#&B)WZl02A1f;fA4<&nP)Zh_)6v7SqBhx zB^-HYZ~sGr*MfVhM#zSA|I>QFlI3!BH@3?7m5@_so>HFJN_**_SmHdwniLcRE_w-L zmjtSHbSCYDhAFX_;`X1Vj}GcHkH3yB;Y&6)$~uqwpI{Bk|HqID9?Emax4i{7FBPbc z5+>~GK*gf?Zuji++TAkOvL6Dfk)siBzKzC|V(Y<%u6|EG)Edh_Z3@eq++N1F0kOHTfUrpT`lc&0v|GEeFvG&RllNOA^F64Ch!!WWh zl>9On?M@Lf=dvFc-du|y!SoO+N~e7IGbuuFSI>AvCN6El&8KX-mvLDNK`mUYaS~ru^K9wJd${DyV5DeDl zZ>Z#=-X)Yjcn0Hb?AoatKgKv~RYxm1_uzkFb68+JlwL|(sF_c5MP*l#ym4aW^4#gH9vEXR&2`>9&iv* zbn%(pBpAHd)O)&sM2d^?P+fD@f99(kk}(2}Jqn0SD<^}RE4&zR?;wrcdKB4yVbcy{ zeG9VTyZP$M&-BsP@*m}kuv^x8OA9cuR}<2)Qi~zjnTU$KA&L_A9Bw&%y3o(0$kwi~ zm&~hAsRWpifMvyF;g^7!Im;+r|J))?deNMtpT?tV?&e00c}QQo%^k1MN+12uSTpy? z={H|@tb(j0J<_Ho@0J>Lvq^dH>f*JLQ4>nmHJo4;g$x4MG@qlY-l0>%I(F@Pz0MrO z$nhO+o-iPTI^dU75NKw*oEcgwb!UzO-FA6mxq=`qY_EB|>!17xEiSoy!S@Zg@nt!n z*RFP0w_CrYXA!PEW#_WEd^Z=;_|fNMMZ;6hyehqb^?Dd0*sY73)-UaO_gOano@?#3 zXFXe4oDr;UKIPwEZf~#=)sA!NogEW}400dADYR;oX{LT=tdI^Tzi-L#*&!c$_SNwB ze8P9acrD17Y_|6AzO0oOq-JB*H^ksnFbx+Gb9lTD;C+AI* zjM}Wq#}8Z|8%-|k7FZDs7xsOEemi?OHmNgxM;RUH4;Jruht##Lqh|BFxOMZ}ZQDe0 z<50$oeI?21mSmC0%vM-pS^?#PG1iOTL1jgxg5^g$>~tGfO@P3bvrJ#iQ_6@0Sn)hV|;J4Y90~O|D+XWeIa{*PQelVe`7tb%^l}yY}+{zka$_)sT^b2 zK>tnnyfF3NC!o}`hX8~)+hRIz)F^@x9jI}(FowSzv|l=PUP#?CJL2BLAzESI=+L29 zXV{eNoFLws#4%T`7?||>VE{$6Km@G;Bi=*0A)s{5RXxpIz|@&S;{HZb8;K$I34Iz> zkiS&AwR~Z%f_dB!CpN**EH@XgaIBDm+T_>510RaNArgdHx;Uy=`wA`oI>7S_L(yEJ zi?3ub5IlP08bKJ5^es1MwFv;TI9T1PHNw>&tY}kCjM?=ul0ynbvhM*0&ZiGh$126b zN6l1z%l7_y+ZT$mYXCU;w^p(z7SRr|EmtHL>$VE*m(y;o(p<-9e#+emzTVfP<1oaI zLMB-!dE=;(3Bte^X@y|eQG7=~u}Cs(Y}GeVM6gH!t|Taa znL_}z3dJ8;`E*(v&aq=rLF)Qc->p&(W+ml~(gX0lmBc^A*W;6yvGtG?(@o;n%hM$Z zXzG%_Cm8?C>?y#k0}|C7eAnpqXzP4qKxWbm@x<(FVjco;xKYTqMr3}!Kz^J)YSNQIYbH6BTYXlqy0pNC6qx!paNYj{%BHgp6{i(=46wgKgVYGF6cd_4A=~VS-Euoeefwy zw<~y)r~6p8M&ovj!5f8N_fh?dS;w+pKU1T-2W|XJ2A2qn$D=tJssnkMn3%n!<;oUs z9AzY<)0^kx{Zah1d%HZWC2@rP31cxCrL_+;5^+<^5F(#)nRYdV0HtUE&7_Lyyt2O6dtOk3SW~tvg8HU)xE}k~7%4D+ z60b;dAaingAYL;IA0|9nP{P-VvS;2>UvrU8?R6Y#&T-|63l}=2hE<8Dc0~i z)6+Vn>4mrK4=8Q@N96-FYaXqgnYoF{_rANJ-M&WA&3Rjf#66Z7>Fr5!PFo>|MGQuogjsr9Ss{A^u6z;#C zFu<0o{`2sI9A|O1|Cb3n@6aG|U`4#RzgFi_MpWw6!HC`m<=xFehlNfu>aeJA>7g8^ zW~){5-LB@=t~V&uxt2D$yT@x+HjkHEuAUQTK4D``UC7D%JK{n4wen{XsW14CRhe5A zIen!Ta(v~hN|NgFytL5;cpv)|bs}di?FAQdf%NBm-CXLLWU4~_xlifuI3I8*Tmg+i z#kcytkkn{w3Q9#aer(Y)G1KRAXJK>Lf7`p0uV%?L>p^NB7^YUO)!{4{#b|b2T)M<| zTY0824hao6QpGNUJW`vXUab#7OkghQ8#Dlf!#mxO#m!#K+eS7IT*)@K`&?cJe82Dr zX8u5>dGNbY;S&RHUY#`pf^F^*-ZF?3i?jSG0>r%F8z6YOV|hY4Jqc&#Vy2(i2&%C5 z+oeg>vhnE&8k0#=+X&rmu~u>Oov`P7ENw~K$K-oq>*p2UsyH*BNd03OoCTOZ zkm_iH!Hp-5JrdCx_WkI?;RA*aIV}gTlB=qZe$~N*`?7;PWhe;tgOV@eDRNv8(gGd>4l>aMW zAw+TqvGoFY6YfO0V3A15yg;{aI)7_KqRZM4VWL*%1O%HN^Vj$M>Lg@ls?Haq4a=S+ z6h11PfGy_XQX?r4V9>`SSF%O@FVoF=GTI~W{3dXbJLJZg>Jujas+R@CMDMH|Y&IVw z+cw`ZnH6=qRp2Za7n%l@8JoQ>*BhV7pyC`+Nr@ty$MyNKwUQ=x!xWqQEVw8Jx~Ho? zL?1a{cZ(l=5IvPJ+-Jv6EvTs&Cfnsc*AYJLJ?{AV*V8Zxhg9}9&>P)GS*b_D0XX4$ zoQ%MJfiB8{R%^CX61vgr880g;D#N2Lqzv#e%T#JreCb#Ga zvlQ#ZxKnkP%AD%7zHR^&RI%v8N<9f_~ofIFo|o)A<$2&Pet;cj&Nj6D{xbm*E0$-4X~Epvq!K@(;0$G~`<6#U10^zIUyY z?)+#XBj~7d(LQ2YFY3WvT{QiP!Mon02mS*Dh@0dve0f-vD8SelTJ z`BFMbikxL@T<0o+t9p13;bJbr16C@yNp49BO>WY4xheG^WWgInTjh#S=^XF20N1Qi zwoo&|TG;&YYUpOeMHIxES~}Qiq;q6D6ssN4ELTsu%&0rD&lF6Wcppk8c1VbUZgXq& zb|NB4gC;v1i~kJ_m*sJa=JLY5)@p1`#S7on+S|49w#Z8YKZ*O(4vqSJ542Zn{)#uu zQ=58Kb+L6}IM@!UdWJ^CX&sR* z@{yp87+d-C|3c8mL0FQefWGdW{qj||byUg1fj?q%2Pt+yCaxWlA%znMLMi=+?$>%- z-5%Sz_K~M!TV?vDpLhy{wr3RHf3?p_%eKiUQvHTo@%Kf@CAS+C-YcpAmsLhZPl(q; z@fvJK#6uMt`Iv5~!bXo4p=PAaz-6!9{7^`B9P)sj|H7Gi;S;ceSF$#gZ7O%ILqO-D zG<$h3@Kou@be1JjD~UHB_sk~Pu-|nT-0a;h_llozHCnjWZI>^kj@ue9t7tNv4}I(-Vm;s| zX%JU?I(VLsTn;+>iD_thxJ)qbs>}AqnMN9V%CARUdf}Yl84z4Z)^>Z}fjj21LEG!u;pWi+=Y=@le|pU_c+}#+_Mc@ zIVDLt&WzDzodS$!|IaT2lTI!u0C9||~Ph+FbV{jPgQ~pcORieTz zj#7`y7~_9K>--Y`(Zt&86De~muW;sV)_L6X1X(MVqmE(>w-z>lQ*uvyuAfd!Nw!2NBAW^cO^!h*ZNDV+_8Ipb_F0U^505ToOtyW($qxCobq*_ z|H9W=7ah?kUavP$!`ITDi*iqZ@;_fP`QNly0+1)%c~Mf)P{ed$bXm;9*C5Z#Q5KU? z#=c6$U0Xb!mxw&!cr`u_6Gz28fFAdOqGi34Yw*ULu;3jQJPt3P zI4BU8@*)6y32R6rC0DiIee#cFMt5_R{z^M?x@{L89akx?mT?-Dx6#d;F(+bDnu?Kl zUaXs$G^~4BS0QNMbHBt4Bwkn*gOT>!Gq19`0R4Z%N+EsX@Wq_mz7Iohx3@&s!Ej~x z?XtF982n?~1c?4NaWGE`Rp|F^G1x{cJ*9ZQ^hO>*%?Rc8C@oP_pL(zJ^P*Jw*!ek= zKG)VLt1lNV?4;$Yk+%0OwJ^`q?Yr0~-2-``7uTgg!e$!MIqf4ph_3pNhm<^%Ez`8U zrgXPb8W~8{_4H|<+-||KG<>+yv45{cuRwszB~yLy`=s+#Bc9EJfLOQyF=uYc^G6U2lOE)>T14NTtgAIT$r3SP-X76%`1%_ zUq!rx(PTgg;UgalA@e01-Rry`E~Gkqq>8dVjl(4$<=_WSEtmA@=2u5{MmU5&7SHvtKE3r$S+<&hd~>{hmfCw7<4npSc1>Z4%6f+r7?<{ z`KB(nBVr`|f>gaikDo2&bN3J#49~(b1^7Iv$F;QRCT^}hfIW{Y_d-zzt@Dah4UI{GnVnovUWHns#0lN;B}`zD=O4FM7|z^p0OW8k+qt{7>}( zrL@27cAt}8d)LRPDHXy@Q}WpBhN{`GrD?621gl}rQ;l?-(6j3E)C9OY8G*0JpuWYZ z=j2ik-q%w$%lS5%Nitn==<;I){_$=_UXQ@Fm(|0b?g6$1`%gFSVr^-@ z6Y+rR^M`(o8>{!eEC_0kg$$RnI8>%+4+q<(1ncTjk^wn3&Zn*dRYHVJ_KX7HL+ie%@y+sPMMmAR;?F58kM7h$+^zoT^f#$#_J$ z$Ny0gyjWscPfG9Wm3w{O{&~|!ZO@2laX7#3Y?_A=gXI-QJh(}Oz(d4Dp zH^DUtX^mUY!q#rofdaB;XxMaanR2=26G7E{nd6SF41(8v(}oi#>@f=`%#NlA)dFa*}Y0W4w>J2{rSJC8SWG(qF^b&_4_SKEt|Vs7(7&zS3@sr zV@n6o<+H;X_X1p%0v3gL}jjjYm>WDj#Y z#@TYDo*j}Pd^{ZI1WcJriOeD*j`yp$J@Za9i;rD%WIqX`(yqcg?LWiYh>f<5662{> z7k&H|INmU$JFwNn&r`GHZZ$9Rn6i1aBym7@s;BQ{vAiesEs@j1M>Kxv`JH?>hLS!3 zt0t3C1~ueq7)Q4;EL%xBwb_o4*f)q%=o9!G|AjdLHD`}uW-_2DMxNxor0?CK-Ku`| z$rP=K3+$1$ka)?C^951iMnUh^6;|Y0bnP|Cf+>YgMXpj!<`!z=v$&cHMJ`mQjS>E{ zl$N(PLcU8(jxo$L1nyN^7cA=ZX;a$#+jumE`L%!V2ymbRr~9s7oxZ)#k~cI%W$!}S z%IU#CGN?qk$mPMHo$fF22NrV99{996S*`j5sWYA8N~1!@haUvzV>{@CM`zy8|CogY z>}ul`1$cMuj+juUqJVp4MDj*1b?OJ1*1Xx+@3yKP#HpkF8qT~LRkG7hDkwDpXVXL)Zh=Pt^-SG`h@ue~DfPHPIFH>x0yhVSh_Izoku&c#WTuuh9 z>MrmWm57GmGaGHP!VVPADz705ZZdC9MG-03Jkd|x%I_rpVKx8B71;#U+;t^u&HyI1 ztoxCC;7JDZ{8P3?M{NL2swLPRFM*wu$|Vg0nB4TEnwx8 zdWKPfC*;5Iea!hZ!Q&j4Cq3}HG=Lok=XKBg*hQ0|dMP0t6(EXsP#x=41aC?}h05t1 z;T6HT4MS>?_!F+>AA!wL9GDUhM-NB2o+FaWEV>STFL~!rpj0jvf1_=|4^;j>Yp(8u z_v|r>4k|ON^~rageRk4l0&Sfz$mxO7JPrq_%DbiP?!>=7HoCUH{7CC5i5D=38Rf9- zAep)nawVG1o@*wz0=S_wYR4{g5!(arA$@|glXRo%`Ww!Qn=feqFZCEu!bf2s1 zdH&?VC5Pqf=3%j9iKqzNE4VXuhY$1Aj`I84uiqbLjt*zSd>%h zGM~Ja^-%O1{;im#t5ABFsPg-IR`+t!KoRs!sM0ptiA~Z1^EoWGI@DjE{GI=}Yc>_| zzVBN769po-lH{OQe?E+Kd{~=w(5(+u8)p@ad?xwGBee%nC>dJ}kjL$Nsq=}y?`9K} zf6aH(1wi9BIJ?|;m+b(5C%di0~0o0t8_N0m=#jqTS2Jn2Y}Uo(`b$a|Cfg>K6Jw4yb)1k@hTZ`>^wQ zr-g$GXp{ZzPHmSiH!(-3WI$WB38zuiZAs8^|7&>zkO02JNX3|_e|--;g7gV)3vfW~ zZF;&azT|Bru1E+t%IDE&dz1QB547BS7<7geS0e-R6f3)^T!-ZI3EyYfsD33TdVlqd zAY;#=mUj0$$Fa)m4@N#@rL^>qFPsu#lr(`K;DI}KTbsD{)9Pzvjhko-Hc7*V+-))o z3UYD~uXuS;s3K3zdUj$51JvM9O%l=C4A;GOon)EbN&#sIC1|^3>l?s-#WuQNDT=N z3(^tZ3MBVUlzi+LHq)@B8ArCbwDV=N<2&k9`dn!x_uCgC9SGAuqvDQt?IVGcfHA2FH;Ov+_dhxX)(LR^xFuxWuimh!XYtq3J&DJXxwcmA$+V`WJoYwqu?bkWqIPQm@p8q{rIrfr zAu`NuCXGsuds+20h#PfI>3jFC*g21w6MbYIy2IF($h|^XpF|`TmP81~QAMH^XEp0HQ^8vM>sr?pP8sTY zaVO9+B0()c*0%_oB1`Src5TSjq@SOBIoDxEI|59M6%YT<9P0X}%ImjyVo|h zM?tKquIxHt{$zQoy#PIsR zD6+@B`O^L!#}4X0$A}d{#8~E@(1NpU9=es*s`4nwY`%vE;|zKmU0Q1Ax}=X#$)rPF z;+hC;vqxaVn~d=@7PZb#mp9le%M~l;&JDL`65b&bC_!WN$nQBl#zzWbwG2*0X~MM! zb3E=Z3WqL6wRT1wyThU`hSXfjfn|?#ywdIsPnY5!Wzo3xGf05x?5v2lDS7_A9r)| znl;`u9&KKScA4EYulacU7^|)gpYXC8J9aW(^C%0ncg^e={{FeV%1z39bzxWwR!|ed z!Q)$-`^2b@x!1s4Aj4TX453fn;jBj4u^@bAW(DH=>?mVU{!^dn{C zUVmh2%80I&HWCadIg~e~P1Mee*M3OWvOh|7hsRX-$iK)B+;L>FhDBIss=D_Pfhk+% z%E?^We4non#*)f*=_rw}Q(PIS(Ou`cR>qg!99wYA*P>-@j-vH9V?EH=IM?C6(?+QMkohT;do)M1tM z@bx3maOMOaVPV$}T-G+`B~h8UIpos-{f!;J$SRBKL1FP)0{c59EyGXHc>a3{1Fus_ zMdD;ykR3E*R4_?TLICeuiWl&4*ia86>b=xjvS=q6k*ZF1iGxoJ{?BN1<&m|GUHfsZf1)6fIyznRJ`)~qCmppHoG?l@ zU=)3eLZ`)Fi2=c=!Z^hAZG=DjuWr6Br3vr1m!!I6SZwk5YkjjwZVDhh$lO=-Es&PbH?&pz0>|h!78pLVLs3$L&Bt)(yIy8#OM|@^ z{F@vmWa@X1*g*jFD0dQ`4^xNx3yKNfWCK_~M--Zhdo|Be%7P)6$U)iws=1BIfL>wk zjNsfG(3%#PbiI(i&|{V#v$Q~qPcCEXdZPja#d^6+hn|0h`)y=s)dO4ay|vHh-bE8I zhqahwR?^o{9iMhO7Jp{H0lh1$u**&=MA@rsOY7{hkPQbQMW~^o-c?d5{xxk-+0J7j zE+6G}&xkbW;sasr_Lq61P?c>-K?G8VnSmGd^D^qQWAttlsMyqkTji8NX?UIPw+R4T%7x4@DW;LcF^m?}^ zVV@Iv5PV`@i?_dQS>`F_R!Qp#I&+U@|=aP;U`HeYCrPN)j<7Z2=3?oF*R-!nE8 zf7O{BOMq|LHatxE?znH{yFNb)+B1jCLl6=VSFz4`T}8!cj)rCj9BFjRy_8K*$S4bW z2%@uKesv$A0L8rW9P0llqD%F%H$+rNi(6x<)JT`x18sLbQ50g2SuXuYX>TR7*(l}( z77)8Z_#(}LkWZc}oEP3r_cSQ4@HDkw=w8vE&EvO9V4|BT2=5Xn>bT#jE1yqhA7XyU zYQOIK1Q~d4dL*I63=?0h?(30A-IuI=KxZeYm#=md#nP-FRBJ>p&{11&q%XidzGIVb z$ZXRM3|qVX>S$VVHK2>1{5$UeO8`$r1&5=lww&y?=Dd3lU35`pPfeZq>sF#fs*B+x zI^k@yo&KavA+TZDtB|8_tLZztcP#NGH8wZly4xuDmSPt%FM}sj$Ujrk%0MQ5I3D(C z7he19^SG}`ABnMiRsU8YOAO3_$T?JO|J)4%2TiA2X{xzZUxU!9_l3|S>c`4xJ~5mz zfmoeu_6?WP+!+Sn;DX28qp@5Syw;c`>1P#{&t3@nmtRX%bW77sJ+zIIB2{^@_*jeu z%NlFf^IJK_ho+zzQ8}2=VM`@HTn?5rE!Pj~!Xwh>?Ez3_>IlW3g!%W|vk|GOtVQ zS``Q3hSe73-7Y*;{{=c+PswrB3lB`yL@Ff4ZN!dt@9zMpwak@D{yK=1pD5kRI0m%m zB4zE@oYwfm8=cqJdU{cRc<|%QM>|LpFVhnGh~Le~nYHz4{^2viZp-hPQGyIRV&jrI zhmu-oCcgPoTx(G$A^Zp50x6uP<1OppaS+dtu7S7v#&2C%4fT%QR%Us2$Ua-0~y2LFmYkBz~V3Ptmd81iAI=DdhwI45ocT<2{RNrE2G#yI`6HE}} zP%#~3AmhNv)_JJtOhb;Gcj^P~Afwh}!9X-W`rrq>&KFrOL$eiI%#>@{(c!gB!^*N+ zHH7`haB1P<_wOWRHT(fR7!wqD%#tYtT+5EJ)icEna=6HXcyQx&#dBn!R zrMzP-SK_y2aJs2wK&7{)KpqP^9r?~GCCh7D+J{lUv|vLWa^egqlO0iqWg2dyY`8Vf zDCiH$ER8qTt`Az>Dds2UA;$#ee4pv}T}X<$;C-LRG1p|>dWaj8`Th$6lu-%0_>m&n zh)GrEXR4aJ=4tIrc5`&HS9-w0P(xT}3Jckb+o(eciS3uJJpGRtZvbO{JhMXy@mXeg z{!dY=Z+h0Xwc(ymAw3^9ZWt$FG=*+SB>{wOVQ4~te zZrtP^Ii{UqhZ4sjTNd|VUu~j73ye|q`WwPIEQsb%AFQh#ud6hFoQ%<_(LCAbpqfu< z7tN%6AR6&^Q@pgUY$ZO`xYS>C15@^PTSSC-h^l)_D>bd8616#mjj&|sOZ&uu;xICg z3y)1-Q>7qny2)S)#b>IuW+F2rxYZmM4Xwhb_sO!nkND7sd2E-ikGlo+OCtfUbt$!g z=)zoFYXXt!|MRKB72VTiE?ryGkn`(hEV|Y2AW_hIQUTA7b^j3Fo2!|FthEd!pJa@d zxPg>#`sqWx3FBIE+Ou?~jh9a;yN_#Y{%vw!m8K+Ly0Ce0qdu|}Tyrb-*v$#0b1sgo zJSwhdA*DEPY{{VC0v*^TW~ z=!;!hE6P?bH8Kqq5W+P3FzD*6Fvm8?1`~_gKpfW^FEMPR5~DAg51xMJa+q?vNTW_X z!sW`_JhVd* zog~=(-L{mLtU}|*2ThpTZ%ic=Iw4iG;wFV^GW#_KVlfo_#!@W5VGRk%V!Z@6NAU>{ zEMMs>tBQHmhxWan9;7|EuSs(nd%@q?jZdR_BefP>KLIoB?P1oo;~6>D%hx~D1Da$E z|Np`BBHYFuB7|wcr?U9QU2)yS;LBj$>iUnq`SiOv&3VIf%?B8s5-k?}%@#&}T zXuM2In@|JAI^4UY@I4Rfv#X=KZ&ih!48=`*SGv=D48MEXPy25h(^fH98^AZ~vYOjF zZlx>WVP<7(z9^g6-11Z}!%7|WYcZ?zdsl6zcb8rTNY%RJuX-iQ<>wODpSNp`QM8!n ziY?i)PviO?*O_~pGxK9rxwftoJmME=q0>ol#fFDKQHx~DpXKPD@^UJ*YBQ7xMqb=U zK>NjZ3L}gX10<`R*mTzrZytpueE^lQ>Oi>*@OoD7D`JZYH$dD{jYl#oDeoU|OnlU@ zgsap>ck1QQ?vt78jgeu}c=|?>Qz|5IDpXa?Lan?E0DiNNz687Y($!J#5UPB9A`>$C zgCYK-e`Upc*swBFrru+$(*7m4hof?O#KE zxb#K-N7Io-=OCfO18l`8wt@YEv@uB}3hcCk!eEJd6)e`GYPb!$p0$f5BeNJ}7;HPut3{P(cmmTa}iS16DAxK8BkP&|~3hMKugy0#}hUiBa>Ck-1 zjjQ~cOgjXy`v;%(RcM~UqWZ%|GV`bV{c8~yA1tr7+(T1`qi`qrGvLm=9+`(XY{#<1 zZey!S7YCM_o->&OZ!_+YG39-I9l00~(J^YDZ>QF&l97DQ7y)yhhTsTq1wYT(~O!Yu7tSYBRf?x_hJ#H#eb7J`qQbZ!Ak&yba`zK2$g~HTaYiL)5&z+|Cvz)0V zkl$`0}iM98niO?#{|{L?J{TM*8Wd{%UWd9p|IsXYQb= zm9%4yI`%_>-a)K9)F8cXvaBqAGN-k#lZZ^G_TFMwsmE_KHUH3cdf?bm&(@bmEQmWg zPOhy$p#wtmPe93b(qJL276kb#xCJ{Z_%4qtvMQEnQQ%7x1KhGS$zd$lX zc6kmQY!3^|XjZf2uYgn*B17h{N%1*d`@nH|-c0inQaWd&^B2b_So&pv@qZ3B6lH{` zj12xkRkI0v;)b~O?8;C|im8p*A!!G9Qz2BoX(DKXu94M2CWtEf_K4jJv5jM4T<-2x z;o+NS{Pi_^gNUA67nzJm@NfF-uldQ7cqyd--M;)wXVp0hg3nxAtz#~#?TcH{%t3rap9p`{PkOp z0&$zCWY$C8SVPhsQ=gC~Z&~ec5`DjVWgZU0nU$Gv#R{kDQX)lIeQ5R4-`#DTVsj5hpH^siNy>`r%_3(tLZApx2 zr&zQ?Z(OEMu58Q`WMA8s5;-;;qp>OjI=z!9F3cv^^Rxq%^2?pB?fqPGKjj%6Y=DH2 zgm$vc5s%yDX#OomxqG@?Y@>i2+-M#6Cck4Ol{_p+% zt`ESEdd3=Ku32k7VJb?}7^uXk0000(Rz^Y%0Dy&l2@60*gnqhoo4E%7d;zi&Vj6A+ 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 Kkt&xk4*EZn*WASb literal 0 HcmV?d00001 diff --git a/src/web/static/fonts/bmfonts/RobotoMono72White.fnt b/src/web/static/fonts/bmfonts/RobotoMono72White.fnt new file mode 100644 index 00000000..f280314d --- /dev/null +++ b/src/web/static/fonts/bmfonts/RobotoMono72White.fnt @@ -0,0 +1,103 @@ +info face="Roboto Mono" size=72 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=1,1,1,1 spacing=-2,-2 +common lineHeight=96 base=76 scaleW=512 scaleH=512 pages=1 packed=0 +page id=0 file="images/RobotoMono72White.png" +chars count=98 +char id=0 x=0 y=0 width=0 height=0 xoffset=-1 yoffset=75 xadvance=0 page=0 chnl=0 +char id=10 x=0 y=0 width=45 height=99 xoffset=-1 yoffset=-2 xadvance=43 page=0 chnl=0 +char id=32 x=0 y=0 width=0 height=0 xoffset=-1 yoffset=75 xadvance=43 page=0 chnl=0 +char id=33 x=498 y=99 width=10 height=55 xoffset=16 yoffset=23 xadvance=43 page=0 chnl=0 +char id=34 x=434 y=319 width=20 height=19 xoffset=11 yoffset=21 xadvance=43 page=0 chnl=0 +char id=35 x=175 y=265 width=41 height=54 xoffset=1 yoffset=23 xadvance=43 page=0 chnl=0 +char id=36 x=200 y=0 width=35 height=69 xoffset=5 yoffset=15 xadvance=43 page=0 chnl=0 +char id=37 x=0 y=155 width=42 height=56 xoffset=1 yoffset=22 xadvance=44 page=0 chnl=0 +char id=38 x=42 y=155 width=41 height=56 xoffset=3 yoffset=22 xadvance=44 page=0 chnl=0 +char id=39 x=502 y=211 width=7 height=19 xoffset=16 yoffset=21 xadvance=43 page=0 chnl=0 +char id=40 x=45 y=0 width=21 height=78 xoffset=12 yoffset=16 xadvance=44 page=0 chnl=0 +char id=41 x=66 y=0 width=22 height=78 xoffset=9 yoffset=16 xadvance=43 page=0 chnl=0 +char id=42 x=256 y=319 width=37 height=37 xoffset=4 yoffset=32 xadvance=43 page=0 chnl=0 +char id=43 x=219 y=319 width=37 height=40 xoffset=3 yoffset=32 xadvance=43 page=0 chnl=0 +char id=44 x=421 y=319 width=13 height=22 xoffset=11 yoffset=67 xadvance=43 page=0 chnl=0 +char id=45 x=17 y=360 width=29 height=8 xoffset=7 yoffset=49 xadvance=44 page=0 chnl=0 +char id=46 x=496 y=319 width=12 height=13 xoffset=16 yoffset=65 xadvance=43 page=0 chnl=0 +char id=47 x=319 y=0 width=31 height=58 xoffset=7 yoffset=23 xadvance=43 page=0 chnl=0 +char id=48 x=431 y=99 width=35 height=56 xoffset=4 yoffset=22 xadvance=43 page=0 chnl=0 +char id=49 x=36 y=265 width=23 height=54 xoffset=6 yoffset=23 xadvance=44 page=0 chnl=0 +char id=50 x=189 y=155 width=37 height=55 xoffset=2 yoffset=22 xadvance=44 page=0 chnl=0 +char id=51 x=361 y=99 width=35 height=56 xoffset=2 yoffset=22 xadvance=43 page=0 chnl=0 +char id=52 x=59 y=265 width=39 height=54 xoffset=2 yoffset=23 xadvance=44 page=0 chnl=0 +char id=53 x=226 y=155 width=35 height=55 xoffset=5 yoffset=23 xadvance=43 page=0 chnl=0 +char id=54 x=261 y=155 width=35 height=55 xoffset=4 yoffset=23 xadvance=43 page=0 chnl=0 +char id=55 x=98 y=265 width=37 height=54 xoffset=3 yoffset=23 xadvance=44 page=0 chnl=0 +char id=56 x=396 y=99 width=35 height=56 xoffset=5 yoffset=22 xadvance=43 page=0 chnl=0 +char id=57 x=296 y=155 width=34 height=55 xoffset=4 yoffset=22 xadvance=43 page=0 chnl=0 +char id=58 x=490 y=211 width=12 height=43 xoffset=18 yoffset=35 xadvance=43 page=0 chnl=0 +char id=59 x=486 y=0 width=14 height=55 xoffset=16 yoffset=35 xadvance=43 page=0 chnl=0 +char id=60 x=293 y=319 width=32 height=35 xoffset=5 yoffset=36 xadvance=43 page=0 chnl=0 +char id=61 x=388 y=319 width=33 height=23 xoffset=5 yoffset=41 xadvance=43 page=0 chnl=0 +char id=62 x=325 y=319 width=33 height=35 xoffset=5 yoffset=36 xadvance=43 page=0 chnl=0 +char id=63 x=466 y=99 width=32 height=56 xoffset=6 yoffset=22 xadvance=43 page=0 chnl=0 +char id=64 x=135 y=265 width=40 height=54 xoffset=1 yoffset=23 xadvance=42 page=0 chnl=0 +char id=65 x=330 y=155 width=42 height=54 xoffset=1 yoffset=23 xadvance=43 page=0 chnl=0 +char id=66 x=372 y=155 width=35 height=54 xoffset=5 yoffset=23 xadvance=43 page=0 chnl=0 +char id=67 x=448 y=0 width=38 height=56 xoffset=3 yoffset=22 xadvance=43 page=0 chnl=0 +char id=68 x=407 y=155 width=37 height=54 xoffset=4 yoffset=23 xadvance=43 page=0 chnl=0 +char id=69 x=444 y=155 width=34 height=54 xoffset=5 yoffset=23 xadvance=43 page=0 chnl=0 +char id=70 x=0 y=211 width=34 height=54 xoffset=6 yoffset=23 xadvance=44 page=0 chnl=0 +char id=71 x=0 y=99 width=38 height=56 xoffset=3 yoffset=22 xadvance=44 page=0 chnl=0 +char id=72 x=34 y=211 width=36 height=54 xoffset=4 yoffset=23 xadvance=43 page=0 chnl=0 +char id=73 x=478 y=155 width=33 height=54 xoffset=5 yoffset=23 xadvance=43 page=0 chnl=0 +char id=74 x=83 y=155 width=36 height=55 xoffset=2 yoffset=23 xadvance=43 page=0 chnl=0 +char id=75 x=70 y=211 width=38 height=54 xoffset=5 yoffset=23 xadvance=43 page=0 chnl=0 +char id=76 x=108 y=211 width=34 height=54 xoffset=6 yoffset=23 xadvance=43 page=0 chnl=0 +char id=77 x=142 y=211 width=36 height=54 xoffset=4 yoffset=23 xadvance=43 page=0 chnl=0 +char id=78 x=178 y=211 width=35 height=54 xoffset=4 yoffset=23 xadvance=43 page=0 chnl=0 +char id=79 x=38 y=99 width=38 height=56 xoffset=3 yoffset=22 xadvance=43 page=0 chnl=0 +char id=80 x=213 y=211 width=36 height=54 xoffset=6 yoffset=23 xadvance=43 page=0 chnl=0 +char id=81 x=242 y=0 width=40 height=64 xoffset=2 yoffset=22 xadvance=43 page=0 chnl=0 +char id=82 x=249 y=211 width=36 height=54 xoffset=5 yoffset=23 xadvance=43 page=0 chnl=0 +char id=83 x=76 y=99 width=38 height=56 xoffset=3 yoffset=22 xadvance=44 page=0 chnl=0 +char id=84 x=285 y=211 width=40 height=54 xoffset=2 yoffset=23 xadvance=44 page=0 chnl=0 +char id=85 x=119 y=155 width=36 height=55 xoffset=4 yoffset=23 xadvance=43 page=0 chnl=0 +char id=86 x=325 y=211 width=41 height=54 xoffset=1 yoffset=23 xadvance=43 page=0 chnl=0 +char id=87 x=366 y=211 width=42 height=54 xoffset=1 yoffset=23 xadvance=43 page=0 chnl=0 +char id=88 x=408 y=211 width=41 height=54 xoffset=2 yoffset=23 xadvance=43 page=0 chnl=0 +char id=89 x=449 y=211 width=41 height=54 xoffset=1 yoffset=23 xadvance=43 page=0 chnl=0 +char id=90 x=0 y=265 width=36 height=54 xoffset=3 yoffset=23 xadvance=43 page=0 chnl=0 +char id=91 x=88 y=0 width=16 height=72 xoffset=14 yoffset=16 xadvance=43 page=0 chnl=0 +char id=92 x=350 y=0 width=30 height=58 xoffset=7 yoffset=23 xadvance=43 page=0 chnl=0 +char id=93 x=104 y=0 width=17 height=72 xoffset=13 yoffset=16 xadvance=44 page=0 chnl=0 +char id=94 x=358 y=319 width=30 height=30 xoffset=7 yoffset=23 xadvance=43 page=0 chnl=0 +char id=95 x=46 y=360 width=34 height=8 xoffset=4 yoffset=74 xadvance=43 page=0 chnl=0 +char id=96 x=0 y=360 width=17 height=12 xoffset=13 yoffset=22 xadvance=43 page=0 chnl=0 +char id=97 x=251 y=265 width=35 height=42 xoffset=4 yoffset=36 xadvance=43 page=0 chnl=0 +char id=98 x=380 y=0 width=34 height=57 xoffset=5 yoffset=21 xadvance=43 page=0 chnl=0 +char id=99 x=286 y=265 width=35 height=42 xoffset=4 yoffset=36 xadvance=43 page=0 chnl=0 +char id=100 x=414 y=0 width=34 height=57 xoffset=4 yoffset=21 xadvance=43 page=0 chnl=0 +char id=101 x=321 y=265 width=36 height=42 xoffset=4 yoffset=36 xadvance=43 page=0 chnl=0 +char id=102 x=282 y=0 width=37 height=58 xoffset=4 yoffset=19 xadvance=43 page=0 chnl=0 +char id=103 x=114 y=99 width=34 height=56 xoffset=4 yoffset=36 xadvance=43 page=0 chnl=0 +char id=104 x=148 y=99 width=34 height=56 xoffset=5 yoffset=21 xadvance=43 page=0 chnl=0 +char id=105 x=155 y=155 width=34 height=55 xoffset=6 yoffset=22 xadvance=43 page=0 chnl=0 +char id=106 x=121 y=0 width=26 height=71 xoffset=6 yoffset=22 xadvance=44 page=0 chnl=0 +char id=107 x=182 y=99 width=36 height=56 xoffset=5 yoffset=21 xadvance=43 page=0 chnl=0 +char id=108 x=218 y=99 width=34 height=56 xoffset=6 yoffset=21 xadvance=43 page=0 chnl=0 +char id=109 x=428 y=265 width=39 height=41 xoffset=2 yoffset=36 xadvance=43 page=0 chnl=0 +char id=110 x=467 y=265 width=34 height=41 xoffset=5 yoffset=36 xadvance=43 page=0 chnl=0 +char id=111 x=357 y=265 width=37 height=42 xoffset=3 yoffset=36 xadvance=43 page=0 chnl=0 +char id=112 x=252 y=99 width=34 height=56 xoffset=5 yoffset=36 xadvance=43 page=0 chnl=0 +char id=113 x=286 y=99 width=34 height=56 xoffset=4 yoffset=36 xadvance=43 page=0 chnl=0 +char id=114 x=0 y=319 width=29 height=41 xoffset=11 yoffset=36 xadvance=44 page=0 chnl=0 +char id=115 x=394 y=265 width=34 height=42 xoffset=5 yoffset=36 xadvance=43 page=0 chnl=0 +char id=116 x=216 y=265 width=35 height=51 xoffset=4 yoffset=27 xadvance=43 page=0 chnl=0 +char id=117 x=29 y=319 width=33 height=41 xoffset=5 yoffset=37 xadvance=43 page=0 chnl=0 +char id=118 x=62 y=319 width=39 height=40 xoffset=2 yoffset=37 xadvance=43 page=0 chnl=0 +char id=119 x=101 y=319 width=43 height=40 xoffset=0 yoffset=37 xadvance=43 page=0 chnl=0 +char id=120 x=144 y=319 width=40 height=40 xoffset=2 yoffset=37 xadvance=43 page=0 chnl=0 +char id=121 x=320 y=99 width=41 height=56 xoffset=1 yoffset=37 xadvance=43 page=0 chnl=0 +char id=122 x=184 y=319 width=35 height=40 xoffset=5 yoffset=37 xadvance=44 page=0 chnl=0 +char id=123 x=147 y=0 width=26 height=71 xoffset=10 yoffset=19 xadvance=43 page=0 chnl=0 +char id=124 x=235 y=0 width=7 height=68 xoffset=18 yoffset=23 xadvance=43 page=0 chnl=0 +char id=125 x=173 y=0 width=27 height=71 xoffset=10 yoffset=19 xadvance=44 page=0 chnl=0 +char id=126 x=454 y=319 width=42 height=16 xoffset=1 yoffset=47 xadvance=44 page=0 chnl=0 +char id=127 x=0 y=0 width=45 height=99 xoffset=-1 yoffset=-2 xadvance=43 page=0 chnl=0 +kernings count=0 diff --git a/src/web/static/fonts/bmfonts/RobotoMono72White.png b/src/web/static/fonts/bmfonts/RobotoMono72White.png new file mode 100644 index 0000000000000000000000000000000000000000..ed192363be386b4fc5f532b629a71b37802323fe GIT binary patch literal 52580 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 I*(&&d0pzTe1^@s6 literal 0 HcmV?d00001 diff --git a/src/web/static/fonts/bmfonts/RobotoSlab72White.fnt b/src/web/static/fonts/bmfonts/RobotoSlab72White.fnt new file mode 100644 index 00000000..71e1b131 --- /dev/null +++ b/src/web/static/fonts/bmfonts/RobotoSlab72White.fnt @@ -0,0 +1,492 @@ +info face="Roboto Slab Regular" size=72 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=1,1,1,1 spacing=-2,-2 +common lineHeight=96 base=76 scaleW=512 scaleH=512 pages=1 packed=0 +page id=0 file="images/RobotoSlab72White.png" +chars count=98 +char id=0 x=0 y=0 width=0 height=0 xoffset=-1 yoffset=75 xadvance=0 page=0 chnl=0 +char id=10 x=0 y=0 width=70 height=98 xoffset=0 yoffset=-1 xadvance=70 page=0 chnl=0 +char id=32 x=0 y=0 width=0 height=0 xoffset=-1 yoffset=75 xadvance=18 page=0 chnl=0 +char id=33 x=497 y=156 width=9 height=54 xoffset=4 yoffset=23 xadvance=17 page=0 chnl=0 +char id=34 x=191 y=362 width=19 height=20 xoffset=5 yoffset=20 xadvance=28 page=0 chnl=0 +char id=35 x=406 y=266 width=41 height=54 xoffset=1 yoffset=23 xadvance=43 page=0 chnl=0 +char id=36 x=212 y=0 width=35 height=69 xoffset=2 yoffset=15 xadvance=39 page=0 chnl=0 +char id=37 x=174 y=156 width=48 height=56 xoffset=2 yoffset=22 xadvance=52 page=0 chnl=0 +char id=38 x=222 y=156 width=44 height=56 xoffset=2 yoffset=22 xadvance=46 page=0 chnl=0 +char id=39 x=210 y=362 width=8 height=20 xoffset=5 yoffset=20 xadvance=17 page=0 chnl=0 +char id=40 x=70 y=0 width=21 height=77 xoffset=3 yoffset=17 xadvance=23 page=0 chnl=0 +char id=41 x=91 y=0 width=21 height=77 xoffset=-1 yoffset=17 xadvance=23 page=0 chnl=0 +char id=42 x=100 y=362 width=31 height=33 xoffset=1 yoffset=23 xadvance=33 page=0 chnl=0 +char id=43 x=0 y=362 width=37 height=40 xoffset=2 yoffset=32 xadvance=41 page=0 chnl=0 +char id=44 x=492 y=320 width=13 height=21 xoffset=-1 yoffset=67 xadvance=14 page=0 chnl=0 +char id=45 x=287 y=362 width=19 height=8 xoffset=4 yoffset=50 xadvance=27 page=0 chnl=0 +char id=46 x=278 y=362 width=9 height=9 xoffset=4 yoffset=68 xadvance=17 page=0 chnl=0 +char id=47 x=470 y=0 width=30 height=58 xoffset=-1 yoffset=23 xadvance=29 page=0 chnl=0 +char id=48 x=139 y=156 width=35 height=56 xoffset=3 yoffset=22 xadvance=41 page=0 chnl=0 +char id=49 x=305 y=266 width=25 height=54 xoffset=3 yoffset=23 xadvance=30 page=0 chnl=0 +char id=50 x=357 y=156 width=36 height=55 xoffset=2 yoffset=22 xadvance=40 page=0 chnl=0 +char id=51 x=0 y=156 width=34 height=56 xoffset=2 yoffset=22 xadvance=39 page=0 chnl=0 +char id=52 x=330 y=266 width=39 height=54 xoffset=1 yoffset=23 xadvance=42 page=0 chnl=0 +char id=53 x=393 y=156 width=33 height=55 xoffset=2 yoffset=23 xadvance=37 page=0 chnl=0 +char id=54 x=34 y=156 width=35 height=56 xoffset=3 yoffset=22 xadvance=40 page=0 chnl=0 +char id=55 x=369 y=266 width=37 height=54 xoffset=2 yoffset=23 xadvance=40 page=0 chnl=0 +char id=56 x=69 y=156 width=35 height=56 xoffset=2 yoffset=22 xadvance=39 page=0 chnl=0 +char id=57 x=104 y=156 width=35 height=56 xoffset=2 yoffset=22 xadvance=41 page=0 chnl=0 +char id=58 x=500 y=0 width=9 height=40 xoffset=4 yoffset=37 xadvance=15 page=0 chnl=0 +char id=59 x=447 y=266 width=13 height=52 xoffset=0 yoffset=37 xadvance=15 page=0 chnl=0 +char id=60 x=37 y=362 width=31 height=35 xoffset=2 yoffset=39 xadvance=36 page=0 chnl=0 +char id=61 x=160 y=362 width=31 height=23 xoffset=4 yoffset=40 xadvance=39 page=0 chnl=0 +char id=62 x=68 y=362 width=32 height=35 xoffset=3 yoffset=39 xadvance=37 page=0 chnl=0 +char id=63 x=480 y=98 width=31 height=55 xoffset=1 yoffset=22 xadvance=33 page=0 chnl=0 +char id=64 x=247 y=0 width=60 height=68 xoffset=1 yoffset=25 xadvance=64 page=0 chnl=0 +char id=65 x=426 y=156 width=51 height=54 xoffset=1 yoffset=23 xadvance=53 page=0 chnl=0 +char id=66 x=0 y=212 width=44 height=54 xoffset=1 yoffset=23 xadvance=47 page=0 chnl=0 +char id=67 x=191 y=98 width=42 height=56 xoffset=1 yoffset=22 xadvance=46 page=0 chnl=0 +char id=68 x=44 y=212 width=46 height=54 xoffset=1 yoffset=23 xadvance=50 page=0 chnl=0 +char id=69 x=90 y=212 width=42 height=54 xoffset=1 yoffset=23 xadvance=46 page=0 chnl=0 +char id=70 x=132 y=212 width=42 height=54 xoffset=1 yoffset=23 xadvance=44 page=0 chnl=0 +char id=71 x=233 y=98 width=43 height=56 xoffset=1 yoffset=22 xadvance=49 page=0 chnl=0 +char id=72 x=174 y=212 width=52 height=54 xoffset=1 yoffset=23 xadvance=55 page=0 chnl=0 +char id=73 x=477 y=156 width=20 height=54 xoffset=1 yoffset=23 xadvance=22 page=0 chnl=0 +char id=74 x=266 y=156 width=39 height=55 xoffset=1 yoffset=23 xadvance=41 page=0 chnl=0 +char id=75 x=226 y=212 width=48 height=54 xoffset=1 yoffset=23 xadvance=50 page=0 chnl=0 +char id=76 x=274 y=212 width=39 height=54 xoffset=1 yoffset=23 xadvance=42 page=0 chnl=0 +char id=77 x=313 y=212 width=64 height=54 xoffset=1 yoffset=23 xadvance=66 page=0 chnl=0 +char id=78 x=377 y=212 width=52 height=54 xoffset=1 yoffset=23 xadvance=54 page=0 chnl=0 +char id=79 x=276 y=98 width=47 height=56 xoffset=2 yoffset=22 xadvance=51 page=0 chnl=0 +char id=80 x=429 y=212 width=43 height=54 xoffset=1 yoffset=23 xadvance=45 page=0 chnl=0 +char id=81 x=307 y=0 width=48 height=64 xoffset=2 yoffset=22 xadvance=51 page=0 chnl=0 +char id=82 x=0 y=266 width=46 height=54 xoffset=1 yoffset=23 xadvance=48 page=0 chnl=0 +char id=83 x=323 y=98 width=38 height=56 xoffset=3 yoffset=22 xadvance=43 page=0 chnl=0 +char id=84 x=46 y=266 width=45 height=54 xoffset=0 yoffset=23 xadvance=45 page=0 chnl=0 +char id=85 x=305 y=156 width=52 height=55 xoffset=1 yoffset=23 xadvance=54 page=0 chnl=0 +char id=86 x=91 y=266 width=50 height=54 xoffset=1 yoffset=23 xadvance=52 page=0 chnl=0 +char id=87 x=141 y=266 width=67 height=54 xoffset=0 yoffset=23 xadvance=67 page=0 chnl=0 +char id=88 x=208 y=266 width=49 height=54 xoffset=1 yoffset=23 xadvance=51 page=0 chnl=0 +char id=89 x=257 y=266 width=48 height=54 xoffset=1 yoffset=23 xadvance=50 page=0 chnl=0 +char id=90 x=472 y=212 width=38 height=54 xoffset=2 yoffset=23 xadvance=42 page=0 chnl=0 +char id=91 x=180 y=0 width=16 height=72 xoffset=5 yoffset=16 xadvance=21 page=0 chnl=0 +char id=92 x=0 y=98 width=31 height=58 xoffset=0 yoffset=23 xadvance=30 page=0 chnl=0 +char id=93 x=196 y=0 width=16 height=72 xoffset=-1 yoffset=16 xadvance=19 page=0 chnl=0 +char id=94 x=131 y=362 width=29 height=28 xoffset=1 yoffset=23 xadvance=30 page=0 chnl=0 +char id=95 x=306 y=362 width=34 height=8 xoffset=3 yoffset=74 xadvance=40 page=0 chnl=0 +char id=96 x=260 y=362 width=18 height=12 xoffset=1 yoffset=22 xadvance=20 page=0 chnl=0 +char id=97 x=0 y=320 width=36 height=42 xoffset=3 yoffset=36 xadvance=41 page=0 chnl=0 +char id=98 x=363 y=0 width=41 height=58 xoffset=-2 yoffset=20 xadvance=42 page=0 chnl=0 +char id=99 x=36 y=320 width=34 height=42 xoffset=2 yoffset=36 xadvance=39 page=0 chnl=0 +char id=100 x=404 y=0 width=40 height=58 xoffset=2 yoffset=20 xadvance=43 page=0 chnl=0 +char id=101 x=70 y=320 width=34 height=42 xoffset=2 yoffset=36 xadvance=39 page=0 chnl=0 +char id=102 x=444 y=0 width=26 height=58 xoffset=1 yoffset=19 xadvance=25 page=0 chnl=0 +char id=103 x=31 y=98 width=34 height=57 xoffset=2 yoffset=36 xadvance=40 page=0 chnl=0 +char id=104 x=65 y=98 width=44 height=57 xoffset=1 yoffset=20 xadvance=46 page=0 chnl=0 +char id=105 x=109 y=98 width=20 height=57 xoffset=2 yoffset=20 xadvance=23 page=0 chnl=0 +char id=106 x=112 y=0 width=18 height=73 xoffset=-2 yoffset=20 xadvance=20 page=0 chnl=0 +char id=107 x=129 y=98 width=42 height=57 xoffset=1 yoffset=20 xadvance=44 page=0 chnl=0 +char id=108 x=171 y=98 width=20 height=57 xoffset=1 yoffset=20 xadvance=22 page=0 chnl=0 +char id=109 x=171 y=320 width=66 height=41 xoffset=1 yoffset=36 xadvance=68 page=0 chnl=0 +char id=110 x=237 y=320 width=44 height=41 xoffset=1 yoffset=36 xadvance=46 page=0 chnl=0 +char id=111 x=104 y=320 width=36 height=42 xoffset=2 yoffset=36 xadvance=40 page=0 chnl=0 +char id=112 x=361 y=98 width=40 height=56 xoffset=1 yoffset=36 xadvance=43 page=0 chnl=0 +char id=113 x=401 y=98 width=39 height=56 xoffset=2 yoffset=36 xadvance=40 page=0 chnl=0 +char id=114 x=484 y=266 width=27 height=41 xoffset=2 yoffset=36 xadvance=30 page=0 chnl=0 +char id=115 x=140 y=320 width=31 height=42 xoffset=3 yoffset=36 xadvance=36 page=0 chnl=0 +char id=116 x=460 y=266 width=24 height=51 xoffset=1 yoffset=27 xadvance=26 page=0 chnl=0 +char id=117 x=281 y=320 width=43 height=41 xoffset=0 yoffset=37 xadvance=44 page=0 chnl=0 +char id=118 x=324 y=320 width=39 height=40 xoffset=0 yoffset=37 xadvance=40 page=0 chnl=0 +char id=119 x=363 y=320 width=57 height=40 xoffset=1 yoffset=37 xadvance=59 page=0 chnl=0 +char id=120 x=420 y=320 width=40 height=40 xoffset=1 yoffset=37 xadvance=42 page=0 chnl=0 +char id=121 x=440 y=98 width=40 height=56 xoffset=0 yoffset=37 xadvance=41 page=0 chnl=0 +char id=122 x=460 y=320 width=32 height=40 xoffset=3 yoffset=37 xadvance=38 page=0 chnl=0 +char id=123 x=130 y=0 width=25 height=73 xoffset=1 yoffset=18 xadvance=25 page=0 chnl=0 +char id=124 x=355 y=0 width=8 height=63 xoffset=4 yoffset=23 xadvance=16 page=0 chnl=0 +char id=125 x=155 y=0 width=25 height=73 xoffset=-1 yoffset=18 xadvance=25 page=0 chnl=0 +char id=126 x=218 y=362 width=42 height=16 xoffset=3 yoffset=47 xadvance=49 page=0 chnl=0 +char id=127 x=0 y=0 width=70 height=98 xoffset=0 yoffset=-1 xadvance=70 page=0 chnl=0 +kernings count=389 +kerning first=86 second=45 amount=-1 +kerning first=114 second=46 amount=-4 +kerning first=40 second=87 amount=1 +kerning first=70 second=99 amount=-1 +kerning first=84 second=110 amount=-3 +kerning first=114 second=116 amount=1 +kerning first=39 second=65 amount=-4 +kerning first=104 second=34 amount=-1 +kerning first=89 second=71 amount=-1 +kerning first=107 second=113 amount=-1 +kerning first=78 second=88 amount=1 +kerning first=109 second=39 amount=-1 +kerning first=120 second=100 amount=-1 +kerning first=84 second=100 amount=-3 +kerning first=68 second=90 amount=-1 +kerning first=68 second=44 amount=-4 +kerning first=84 second=103 amount=-3 +kerning first=34 second=97 amount=-2 +kerning first=70 second=97 amount=-1 +kerning first=76 second=81 amount=-2 +kerning first=73 second=89 amount=-1 +kerning first=84 second=44 amount=-8 +kerning first=68 second=65 amount=-3 +kerning first=97 second=34 amount=-2 +kerning first=111 second=121 amount=-1 +kerning first=79 second=90 amount=-1 +kerning first=75 second=121 amount=-1 +kerning first=75 second=118 amount=-1 +kerning first=111 second=118 amount=-1 +kerning first=89 second=65 amount=-9 +kerning first=75 second=71 amount=-4 +kerning first=39 second=99 amount=-2 +kerning first=75 second=99 amount=-1 +kerning first=90 second=121 amount=-1 +kerning first=44 second=39 amount=-6 +kerning first=89 second=46 amount=-7 +kerning first=89 second=74 amount=-7 +kerning first=34 second=103 amount=-2 +kerning first=70 second=103 amount=-1 +kerning first=112 second=39 amount=-1 +kerning first=122 second=113 amount=-1 +kerning first=86 second=113 amount=-2 +kerning first=68 second=84 amount=-1 +kerning first=89 second=110 amount=-1 +kerning first=34 second=100 amount=-2 +kerning first=68 second=86 amount=-1 +kerning first=87 second=45 amount=-2 +kerning first=39 second=34 amount=-4 +kerning first=114 second=100 amount=-1 +kerning first=84 second=81 amount=-1 +kerning first=70 second=101 amount=-1 +kerning first=68 second=89 amount=-2 +kerning first=88 second=117 amount=-1 +kerning first=112 second=34 amount=-1 +kerning first=76 second=67 amount=-2 +kerning first=76 second=34 amount=-5 +kerning first=88 second=111 amount=-1 +kerning first=66 second=86 amount=-1 +kerning first=66 second=89 amount=-2 +kerning first=122 second=101 amount=-1 +kerning first=86 second=101 amount=-2 +kerning first=76 second=121 amount=-5 +kerning first=84 second=119 amount=-2 +kerning first=84 second=112 amount=-3 +kerning first=87 second=111 amount=-1 +kerning first=69 second=118 amount=-1 +kerning first=65 second=117 amount=-2 +kerning first=65 second=89 amount=-9 +kerning first=72 second=89 amount=-1 +kerning first=119 second=44 amount=-4 +kerning first=69 second=121 amount=-1 +kerning first=84 second=109 amount=-3 +kerning first=84 second=122 amount=-2 +kerning first=89 second=99 amount=-2 +kerning first=76 second=118 amount=-5 +kerning first=90 second=99 amount=-1 +kerning first=90 second=103 amount=-1 +kerning first=79 second=89 amount=-2 +kerning first=90 second=79 amount=-1 +kerning first=84 second=115 amount=-4 +kerning first=76 second=65 amount=1 +kerning first=90 second=100 amount=-1 +kerning first=118 second=46 amount=-4 +kerning first=87 second=117 amount=-1 +kerning first=118 second=34 amount=1 +kerning first=69 second=103 amount=-1 +kerning first=97 second=121 amount=-1 +kerning first=39 second=111 amount=-2 +kerning first=72 second=88 amount=1 +kerning first=76 second=87 amount=-5 +kerning first=69 second=119 amount=-1 +kerning first=121 second=97 amount=-1 +kerning first=75 second=45 amount=-8 +kerning first=65 second=86 amount=-9 +kerning first=46 second=34 amount=-6 +kerning first=76 second=84 amount=-10 +kerning first=116 second=111 amount=-1 +kerning first=87 second=113 amount=-1 +kerning first=69 second=100 amount=-1 +kerning first=97 second=118 amount=-1 +kerning first=65 second=85 amount=-2 +kerning first=90 second=71 amount=-1 +kerning first=68 second=46 amount=-4 +kerning first=65 second=79 amount=-3 +kerning first=98 second=122 amount=-1 +kerning first=86 second=41 amount=1 +kerning first=84 second=118 amount=-3 +kerning first=70 second=118 amount=-1 +kerning first=121 second=111 amount=-1 +kerning first=81 second=87 amount=-1 +kerning first=70 second=100 amount=-1 +kerning first=102 second=93 amount=1 +kerning first=114 second=101 amount=-1 +kerning first=88 second=45 amount=-2 +kerning first=39 second=103 amount=-2 +kerning first=75 second=103 amount=-1 +kerning first=88 second=101 amount=-1 +kerning first=89 second=103 amount=-2 +kerning first=110 second=39 amount=-1 +kerning first=89 second=89 amount=1 +kerning first=87 second=65 amount=-2 +kerning first=119 second=46 amount=-4 +kerning first=34 second=34 amount=-4 +kerning first=88 second=79 amount=-2 +kerning first=79 second=86 amount=-1 +kerning first=76 second=119 amount=-3 +kerning first=75 second=111 amount=-1 +kerning first=65 second=116 amount=-4 +kerning first=86 second=65 amount=-9 +kerning first=70 second=84 amount=1 +kerning first=75 second=117 amount=-1 +kerning first=80 second=65 amount=-9 +kerning first=34 second=112 amount=-1 +kerning first=102 second=99 amount=-1 +kerning first=118 second=97 amount=-1 +kerning first=89 second=81 amount=-1 +kerning first=118 second=111 amount=-1 +kerning first=102 second=101 amount=-1 +kerning first=114 second=44 amount=-4 +kerning first=90 second=119 amount=-1 +kerning first=75 second=81 amount=-4 +kerning first=88 second=121 amount=-1 +kerning first=34 second=110 amount=-1 +kerning first=86 second=100 amount=-2 +kerning first=122 second=100 amount=-1 +kerning first=89 second=67 amount=-1 +kerning first=90 second=118 amount=-1 +kerning first=84 second=84 amount=1 +kerning first=121 second=34 amount=1 +kerning first=91 second=74 amount=-1 +kerning first=88 second=113 amount=-1 +kerning first=77 second=88 amount=1 +kerning first=75 second=119 amount=-2 +kerning first=114 second=104 amount=-1 +kerning first=68 second=88 amount=-2 +kerning first=121 second=44 amount=-4 +kerning first=81 second=89 amount=-1 +kerning first=102 second=39 amount=1 +kerning first=74 second=65 amount=-2 +kerning first=114 second=118 amount=1 +kerning first=84 second=46 amount=-8 +kerning first=111 second=34 amount=-1 +kerning first=88 second=71 amount=-2 +kerning first=88 second=99 amount=-1 +kerning first=84 second=74 amount=-8 +kerning first=39 second=109 amount=-1 +kerning first=98 second=34 amount=-1 +kerning first=86 second=114 amount=-1 +kerning first=88 second=81 amount=-2 +kerning first=70 second=74 amount=-11 +kerning first=89 second=83 amount=-1 +kerning first=87 second=41 amount=1 +kerning first=89 second=97 amount=-3 +kerning first=89 second=87 amount=1 +kerning first=67 second=125 amount=-1 +kerning first=89 second=93 amount=1 +kerning first=80 second=118 amount=1 +kerning first=107 second=100 amount=-1 +kerning first=114 second=34 amount=1 +kerning first=89 second=109 amount=-1 +kerning first=89 second=45 amount=-2 +kerning first=70 second=44 amount=-8 +kerning first=34 second=39 amount=-4 +kerning first=88 second=67 amount=-2 +kerning first=70 second=46 amount=-8 +kerning first=102 second=41 amount=1 +kerning first=89 second=117 amount=-1 +kerning first=89 second=111 amount=-4 +kerning first=89 second=115 amount=-4 +kerning first=114 second=102 amount=1 +kerning first=89 second=125 amount=1 +kerning first=89 second=121 amount=-1 +kerning first=114 second=108 amount=-1 +kerning first=47 second=47 amount=-8 +kerning first=65 second=63 amount=-2 +kerning first=75 second=67 amount=-4 +kerning first=87 second=100 amount=-1 +kerning first=111 second=104 amount=-1 +kerning first=111 second=107 amount=-1 +kerning first=75 second=109 amount=-1 +kerning first=87 second=114 amount=-1 +kerning first=111 second=120 amount=-1 +kerning first=69 second=99 amount=-1 +kerning first=65 second=84 amount=-6 +kerning first=39 second=97 amount=-2 +kerning first=121 second=46 amount=-4 +kerning first=89 second=85 amount=-3 +kerning first=75 second=79 amount=-4 +kerning first=107 second=99 amount=-1 +kerning first=102 second=100 amount=-1 +kerning first=102 second=103 amount=-1 +kerning first=75 second=110 amount=-1 +kerning first=39 second=110 amount=-1 +kerning first=69 second=84 amount=1 +kerning first=84 second=111 amount=-3 +kerning first=120 second=111 amount=-1 +kerning first=84 second=114 amount=-3 +kerning first=112 second=120 amount=-1 +kerning first=79 second=84 amount=-1 +kerning first=84 second=117 amount=-3 +kerning first=89 second=79 amount=-1 +kerning first=75 second=113 amount=-1 +kerning first=39 second=113 amount=-2 +kerning first=80 second=44 amount=-11 +kerning first=79 second=88 amount=-2 +kerning first=98 second=39 amount=-1 +kerning first=65 second=118 amount=-4 +kerning first=65 second=34 amount=-4 +kerning first=88 second=103 amount=-1 +kerning first=77 second=89 amount=-1 +kerning first=39 second=101 amount=-2 +kerning first=75 second=101 amount=-1 +kerning first=88 second=100 amount=-1 +kerning first=78 second=65 amount=-3 +kerning first=87 second=44 amount=-4 +kerning first=67 second=41 amount=-1 +kerning first=86 second=93 amount=1 +kerning first=84 second=83 amount=-1 +kerning first=102 second=113 amount=-1 +kerning first=34 second=111 amount=-2 +kerning first=70 second=111 amount=-1 +kerning first=86 second=99 amount=-2 +kerning first=84 second=86 amount=1 +kerning first=122 second=99 amount=-1 +kerning first=84 second=89 amount=1 +kerning first=70 second=114 amount=-1 +kerning first=86 second=74 amount=-8 +kerning first=89 second=38 amount=-1 +kerning first=87 second=97 amount=-1 +kerning first=76 second=86 amount=-9 +kerning first=40 second=86 amount=1 +kerning first=90 second=113 amount=-1 +kerning first=39 second=39 amount=-4 +kerning first=111 second=39 amount=-1 +kerning first=90 second=117 amount=-1 +kerning first=89 second=41 amount=1 +kerning first=65 second=121 amount=-4 +kerning first=89 second=100 amount=-2 +kerning first=89 second=42 amount=-2 +kerning first=76 second=117 amount=-2 +kerning first=69 second=111 amount=-1 +kerning first=46 second=39 amount=-6 +kerning first=118 second=39 amount=1 +kerning first=91 second=85 amount=-1 +kerning first=80 second=90 amount=-1 +kerning first=90 second=81 amount=-1 +kerning first=69 second=117 amount=-1 +kerning first=76 second=39 amount=-5 +kerning first=90 second=67 amount=-1 +kerning first=87 second=103 amount=-1 +kerning first=84 second=120 amount=-3 +kerning first=89 second=101 amount=-2 +kerning first=102 second=125 amount=1 +kerning first=76 second=85 amount=-2 +kerning first=79 second=65 amount=-3 +kerning first=65 second=71 amount=-3 +kerning first=79 second=44 amount=-4 +kerning first=97 second=39 amount=-2 +kerning first=90 second=101 amount=-1 +kerning first=65 second=87 amount=-5 +kerning first=79 second=46 amount=-4 +kerning first=87 second=99 amount=-1 +kerning first=34 second=101 amount=-2 +kerning first=40 second=89 amount=1 +kerning first=76 second=89 amount=-8 +kerning first=69 second=113 amount=-1 +kerning first=120 second=103 amount=-1 +kerning first=69 second=101 amount=-1 +kerning first=69 second=102 amount=-1 +kerning first=104 second=39 amount=-1 +kerning first=80 second=121 amount=1 +kerning first=86 second=46 amount=-8 +kerning first=65 second=81 amount=-3 +kerning first=86 second=44 amount=-8 +kerning first=120 second=99 amount=-1 +kerning first=98 second=120 amount=-1 +kerning first=39 second=115 amount=-3 +kerning first=121 second=39 amount=1 +kerning first=88 second=118 amount=-1 +kerning first=84 second=65 amount=-6 +kerning first=65 second=39 amount=-4 +kerning first=84 second=79 amount=-1 +kerning first=65 second=119 amount=-4 +kerning first=70 second=117 amount=-1 +kerning first=75 second=100 amount=-1 +kerning first=86 second=111 amount=-2 +kerning first=122 second=111 amount=-1 +kerning first=81 second=84 amount=-2 +kerning first=107 second=103 amount=-1 +kerning first=118 second=44 amount=-4 +kerning first=87 second=46 amount=-4 +kerning first=87 second=101 amount=-1 +kerning first=70 second=79 amount=-2 +kerning first=87 second=74 amount=-2 +kerning first=123 second=74 amount=-1 +kerning first=76 second=71 amount=-2 +kerning first=39 second=100 amount=-2 +kerning first=80 second=88 amount=-1 +kerning first=84 second=121 amount=-3 +kerning first=112 second=122 amount=-1 +kerning first=84 second=71 amount=-1 +kerning first=89 second=86 amount=1 +kerning first=84 second=113 amount=-3 +kerning first=120 second=113 amount=-1 +kerning first=89 second=44 amount=-7 +kerning first=84 second=99 amount=-3 +kerning first=34 second=113 amount=-2 +kerning first=80 second=46 amount=-11 +kerning first=86 second=117 amount=-1 +kerning first=110 second=34 amount=-1 +kerning first=80 second=74 amount=-7 +kerning first=120 second=101 amount=-1 +kerning first=73 second=88 amount=1 +kerning first=108 second=111 amount=-1 +kerning first=34 second=115 amount=-3 +kerning first=89 second=113 amount=-2 +kerning first=82 second=86 amount=-3 +kerning first=114 second=39 amount=1 +kerning first=34 second=109 amount=-1 +kerning first=84 second=101 amount=-3 +kerning first=70 second=121 amount=-1 +kerning first=123 second=85 amount=-1 +kerning first=122 second=103 amount=-1 +kerning first=86 second=97 amount=-2 +kerning first=82 second=89 amount=-4 +kerning first=66 second=84 amount=-1 +kerning first=84 second=97 amount=-4 +kerning first=86 second=103 amount=-2 +kerning first=70 second=113 amount=-1 +kerning first=84 second=87 amount=1 +kerning first=75 second=112 amount=-1 +kerning first=114 second=111 amount=-1 +kerning first=39 second=112 amount=-1 +kerning first=107 second=101 amount=-1 +kerning first=82 second=84 amount=-3 +kerning first=114 second=121 amount=1 +kerning first=34 second=99 amount=-2 +kerning first=70 second=81 amount=-2 +kerning first=111 second=122 amount=-1 +kerning first=84 second=67 amount=-1 +kerning first=111 second=108 amount=-1 +kerning first=89 second=84 amount=1 +kerning first=76 second=79 amount=-2 +kerning first=85 second=65 amount=-2 +kerning first=44 second=34 amount=-6 +kerning first=65 second=67 amount=-3 +kerning first=109 second=34 amount=-1 +kerning first=114 second=103 amount=-1 +kerning first=78 second=89 amount=-1 +kerning first=89 second=114 amount=-1 +kerning first=89 second=112 amount=-1 +kerning first=34 second=65 amount=-4 +kerning first=70 second=65 amount=-11 +kerning first=81 second=86 amount=-1 +kerning first=114 second=119 amount=1 +kerning first=89 second=102 amount=-1 +kerning first=84 second=45 amount=-8 +kerning first=86 second=125 amount=1 +kerning first=70 second=67 amount=-2 +kerning first=89 second=116 amount=-1 +kerning first=102 second=34 amount=1 +kerning first=114 second=99 amount=-1 +kerning first=67 second=84 amount=-1 +kerning first=114 second=113 amount=-1 +kerning first=89 second=122 amount=-1 +kerning first=89 second=118 amount=-1 +kerning first=70 second=71 amount=-2 +kerning first=114 second=107 amount=-1 +kerning first=89 second=120 amount=-1 diff --git a/src/web/static/fonts/bmfonts/RobotoSlab72White.png b/src/web/static/fonts/bmfonts/RobotoSlab72White.png new file mode 100644 index 0000000000000000000000000000000000000000..5aa1bf064c4d545993bd838050b81ccf624a5c64 GIT binary patch literal 54282 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 Date: Tue, 19 Mar 2019 13:54:26 +0000 Subject: [PATCH 385/801] Add new implementation of gaussian blur. Changed SharpenImage to use the new algorithm. --- src/core/lib/ImageManipulation.mjs | 252 +++++++++++++++++++++++++++ src/core/operations/BlurImage.mjs | 7 +- src/core/operations/SharpenImage.mjs | 5 +- 3 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 src/core/lib/ImageManipulation.mjs diff --git a/src/core/lib/ImageManipulation.mjs b/src/core/lib/ImageManipulation.mjs new file mode 100644 index 00000000..1f1b85ae --- /dev/null +++ b/src/core/lib/ImageManipulation.mjs @@ -0,0 +1,252 @@ +/** + * Image manipulation resources + * + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError"; + +/** + * Gaussian blurs an image. + * + * @param {jimp} input + * @param {int} radius + * @param {boolean} fast + * @returns {jimp} + */ +export function gaussianBlur (input, radius) { + try { + // From http://blog.ivank.net/fastest-gaussian-blur.html + const boxes = boxesForGauss(radius, 3); + for (let i = 0; i < 3; i++) { + input = boxBlur(input, (boxes[i] - 1) / 2); + } + } catch (err) { + log.error(err); + throw new OperationError(`Error blurring image. (${err})`); + } + + return input; +} + +/** + * + * @param {int} radius + * @param {int} numBoxes + * @returns {Array} + */ +function boxesForGauss(radius, numBoxes) { + const idealWidth = Math.sqrt((12 * radius * radius / numBoxes) + 1); + + let wl = Math.floor(idealWidth); + + if (wl % 2 === 0) { + wl--; + } + + const wu = wl + 2; + + const mIdeal = (12 * radius * radius - numBoxes * wl * wl - 4 * numBoxes * wl - 3 * numBoxes) / (-4 * wl - 4); + const m = Math.round(mIdeal); + + const sizes = []; + for (let i = 0; i < numBoxes; i++) { + sizes.push(i < m ? wl : wu); + } + return sizes; +} + +/** + * Applies a box blur effect to the image + * + * @param {jimp} source + * @param {number} radius + * @returns {jimp} + */ +function boxBlur (source, radius) { + const width = source.bitmap.width; + const height = source.bitmap.height; + let output = source.clone(); + output = boxBlurH(source, output, width, height, radius); + source = boxBlurV(output, source, width, height, radius); + + return source; +} + +/** + * Applies the horizontal blur + * + * @param {jimp} source + * @param {jimp} output + * @param {number} width + * @param {number} height + * @param {number} radius + * @returns {jimp} + */ +function boxBlurH (source, output, width, height, radius) { + const iarr = 1 / (radius + radius + 1); + for (let i = 0; i < height; i++) { + let ti = 0, + li = ti, + ri = ti + radius; + const idx = source.getPixelIndex(ti, i); + const firstValRed = source.bitmap.data[idx], + firstValGreen = source.bitmap.data[idx + 1], + firstValBlue = source.bitmap.data[idx + 2], + firstValAlpha = source.bitmap.data[idx + 3]; + + const lastIdx = source.getPixelIndex(width - 1, i), + lastValRed = source.bitmap.data[lastIdx], + lastValGreen = source.bitmap.data[lastIdx + 1], + lastValBlue = source.bitmap.data[lastIdx + 2], + lastValAlpha = source.bitmap.data[lastIdx + 3]; + + let red = (radius + 1) * firstValRed; + let green = (radius + 1) * firstValGreen; + let blue = (radius + 1) * firstValBlue; + let alpha = (radius + 1) * firstValAlpha; + + for (let j = 0; j < radius; j++) { + const jIdx = source.getPixelIndex(ti + j, i); + red += source.bitmap.data[jIdx]; + green += source.bitmap.data[jIdx + 1]; + blue += source.bitmap.data[jIdx + 2]; + alpha += source.bitmap.data[jIdx + 3]; + } + + for (let j = 0; j <= radius; j++) { + const jIdx = source.getPixelIndex(ri++, i); + red += source.bitmap.data[jIdx] - firstValRed; + green += source.bitmap.data[jIdx + 1] - firstValGreen; + blue += source.bitmap.data[jIdx + 2] - firstValBlue; + alpha += source.bitmap.data[jIdx + 3] - firstValAlpha; + + const tiIdx = source.getPixelIndex(ti++, i); + output.bitmap.data[tiIdx] = Math.round(red * iarr); + output.bitmap.data[tiIdx + 1] = Math.round(green * iarr); + output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr); + output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr); + } + + for (let j = radius + 1; j < width - radius; j++) { + const riIdx = source.getPixelIndex(ri++, i); + const liIdx = source.getPixelIndex(li++, i); + red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx]; + green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1]; + blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2]; + alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3]; + + const tiIdx = source.getPixelIndex(ti++, i); + output.bitmap.data[tiIdx] = Math.round(red * iarr); + output.bitmap.data[tiIdx + 1] = Math.round(green * iarr); + output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr); + output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr); + } + + for (let j = width - radius; j < width; j++) { + const liIdx = source.getPixelIndex(li++, i); + red += lastValRed - source.bitmap.data[liIdx]; + green += lastValGreen - source.bitmap.data[liIdx + 1]; + blue += lastValBlue - source.bitmap.data[liIdx + 2]; + alpha += lastValAlpha - source.bitmap.data[liIdx + 3]; + + const tiIdx = source.getPixelIndex(ti++, i); + output.bitmap.data[tiIdx] = Math.round(red * iarr); + output.bitmap.data[tiIdx + 1] = Math.round(green * iarr); + output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr); + output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr); + } + } + return output; +} + +/** + * Applies the vertical blur + * + * @param {jimp} source + * @param {jimp} output + * @param {int} width + * @param {int} height + * @param {int} radius + * @returns {jimp} + */ +function boxBlurV (source, output, width, height, radius) { + const iarr = 1 / (radius + radius + 1); + for (let i = 0; i < width; i++) { + let ti = 0, + li = ti, + ri = ti + radius; + + const idx = source.getPixelIndex(i, ti); + + const firstValRed = source.bitmap.data[idx], + firstValGreen = source.bitmap.data[idx + 1], + firstValBlue = source.bitmap.data[idx + 2], + firstValAlpha = source.bitmap.data[idx + 3]; + + const lastIdx = source.getPixelIndex(i, height - 1), + lastValRed = source.bitmap.data[lastIdx], + lastValGreen = source.bitmap.data[lastIdx + 1], + lastValBlue = source.bitmap.data[lastIdx + 2], + lastValAlpha = source.bitmap.data[lastIdx + 3]; + + let red = (radius + 1) * firstValRed; + let green = (radius + 1) * firstValGreen; + let blue = (radius + 1) * firstValBlue; + let alpha = (radius + 1) * firstValAlpha; + + for (let j = 0; j < radius; j++) { + const jIdx = source.getPixelIndex(i, ti + j); + red += source.bitmap.data[jIdx]; + green += source.bitmap.data[jIdx + 1]; + blue += source.bitmap.data[jIdx + 2]; + alpha += source.bitmap.data[jIdx + 3]; + } + + for (let j = 0; j <= radius; j++) { + const riIdx = source.getPixelIndex(i, ri++); + red += source.bitmap.data[riIdx] - firstValRed; + green += source.bitmap.data[riIdx + 1] - firstValGreen; + blue += source.bitmap.data[riIdx + 2] - firstValBlue; + alpha += source.bitmap.data[riIdx + 3] - firstValAlpha; + + const tiIdx = source.getPixelIndex(i, ti++); + output.bitmap.data[tiIdx] = Math.round(red * iarr); + output.bitmap.data[tiIdx + 1] = Math.round(green * iarr); + output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr); + output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr); + } + + for (let j = radius + 1; j < height - radius; j++) { + const riIdx = source.getPixelIndex(i, ri++); + const liIdx = source.getPixelIndex(i, li++); + red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx]; + green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1]; + blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2]; + alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3]; + + const tiIdx = source.getPixelIndex(i, ti++); + output.bitmap.data[tiIdx] = Math.round(red * iarr); + output.bitmap.data[tiIdx + 1] = Math.round(green * iarr); + output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr); + output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr); + } + + for (let j = height - radius; j < height; j++) { + const liIdx = source.getPixelIndex(i, li++); + red += lastValRed - source.bitmap.data[liIdx]; + green += lastValGreen - source.bitmap.data[liIdx + 1]; + blue += lastValBlue - source.bitmap.data[liIdx + 2]; + alpha += lastValAlpha - source.bitmap.data[liIdx + 3]; + + const tiIdx = source.getPixelIndex(i, ti++); + output.bitmap.data[tiIdx] = Math.round(red * iarr); + output.bitmap.data[tiIdx + 1] = Math.round(green * iarr); + output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr); + output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr); + } + } + return output; +} diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs index e1a52710..e0b5c919 100644 --- a/src/core/operations/BlurImage.mjs +++ b/src/core/operations/BlurImage.mjs @@ -9,6 +9,7 @@ import OperationError from "../errors/OperationError"; import { isImage } from "../lib/FileType"; import { toBase64 } from "../lib/Base64"; import jimp from "jimp"; +import { gaussianBlur } from "../lib/ImageManipulation"; /** * Blur Image operation @@ -64,12 +65,14 @@ class BlurImage extends Operation { try { switch (blurType){ case "Fast": + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Fast blurring image..."); image.blur(blurAmount); break; case "Gaussian": if (ENVIRONMENT_IS_WORKER()) - self.sendStatusMessage("Gaussian blurring image. This may take a while..."); - image.gaussian(blurAmount); + self.sendStatusMessage("Gaussian blurring image..."); + image = gaussianBlur(image, blurAmount); break; } diff --git a/src/core/operations/SharpenImage.mjs b/src/core/operations/SharpenImage.mjs index db0e7bb7..3ef1912e 100644 --- a/src/core/operations/SharpenImage.mjs +++ b/src/core/operations/SharpenImage.mjs @@ -8,6 +8,7 @@ import Operation from "../Operation"; import OperationError from "../errors/OperationError"; import { isImage } from "../lib/FileType"; import { toBase64 } from "../lib/Base64"; +import { gaussianBlur } from "../lib/ImageManipulation"; import jimp from "jimp"; /** @@ -74,12 +75,12 @@ class SharpenImage extends Operation { try { if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Sharpening image... (Cloning image)"); - const blurImage = image.clone(); const blurMask = image.clone(); if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Sharpening image... (Blurring cloned image)"); - blurImage.gaussian(radius); + const blurImage = gaussianBlur(image.clone(), radius, 3); + if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Sharpening image... (Creating unsharp mask)"); From b312e179047d3f6bd374226eec975248e5833f41 Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 19 Mar 2019 13:54:39 +0000 Subject: [PATCH 386/801] Change title to title case --- src/core/operations/ConvertImageFormat.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/ConvertImageFormat.mjs b/src/core/operations/ConvertImageFormat.mjs index c8152908..5900fbbc 100644 --- a/src/core/operations/ConvertImageFormat.mjs +++ b/src/core/operations/ConvertImageFormat.mjs @@ -21,7 +21,7 @@ class ConvertImageFormat extends Operation { constructor() { super(); - this.name = "Convert image format"; + this.name = "Convert Image Format"; this.module = "Image"; this.description = "Converts an image between different formats. Supported formats:

  • Joint Photographic Experts Group (JPEG)
  • Portable Network Graphics (PNG)
  • Bitmap (BMP)
  • Tagged Image File Format (TIFF)

Note: GIF files are supported for input, but cannot be outputted."; this.infoURL = "https://wikipedia.org/wiki/Image_file_formats"; From d09ab4a153b9eaadd15005dff14065e0de975def Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 19 Mar 2019 14:37:46 +0000 Subject: [PATCH 387/801] Add new solarized light and dark themes. Add more elements to be controlled by theme css: - Preloader spinner colours - Operation disable / breakpoint icons - Auto bake checkbox - Search highlight colour - Categories header colour --- src/web/html/index.html | 11 +- src/web/stylesheets/components/_operation.css | 8 +- src/web/stylesheets/index.css | 2 + src/web/stylesheets/layout/_controls.css | 6 + src/web/stylesheets/layout/_operations.css | 4 +- src/web/stylesheets/preloader.css | 10 +- src/web/stylesheets/themes/_classic.css | 15 ++ src/web/stylesheets/themes/_dark.css | 15 ++ src/web/stylesheets/themes/_geocities.css | 15 ++ src/web/stylesheets/themes/_solarizedDark.css | 143 +++++++++++++++++ .../stylesheets/themes/_solarizedDarkOld.css | 127 +++++++++++++++ .../stylesheets/themes/_solarizedLight.css | 145 ++++++++++++++++++ src/web/stylesheets/utils/_overrides.css | 7 +- 13 files changed, 494 insertions(+), 14 deletions(-) create mode 100755 src/web/stylesheets/themes/_solarizedDark.css create mode 100755 src/web/stylesheets/themes/_solarizedDarkOld.css create mode 100755 src/web/stylesheets/themes/_solarizedLight.css diff --git a/src/web/html/index.html b/src/web/html/index.html index 119e8ada..d7fbdd05 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -250,7 +250,7 @@
- +
Name:
@@ -436,6 +436,8 @@ + +
@@ -509,6 +511,13 @@ Attempt to detect encoded data automagically
+ +
+ +