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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%0DL@xBkd8O}>j%3OJQ&V_^E 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 literal 0 HcmV?d00001 diff --git a/src/web/static/fonts/bmfonts/RobotoBlack72White.fnt b/src/web/static/fonts/bmfonts/RobotoBlack72White.fnt new file mode 100644 index 00000000..3d08fe95 --- /dev/null +++ b/src/web/static/fonts/bmfonts/RobotoBlack72White.fnt @@ -0,0 +1,488 @@ +info face="Roboto Black" 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/RobotoBlack72White.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=460 y=156 width=15 height=55 xoffset=3 yoffset=14 xadvance=20 page=0 chnl=0 +char id=34 x=207 y=362 width=22 height=22 xoffset=0 yoffset=12 xadvance=23 page=0 chnl=0 +char id=35 x=404 y=266 width=41 height=54 xoffset=0 yoffset=14 xadvance=42 page=0 chnl=0 +char id=36 x=220 y=0 width=38 height=69 xoffset=2 yoffset=7 xadvance=42 page=0 chnl=0 +char id=37 x=167 y=156 width=49 height=56 xoffset=2 yoffset=13 xadvance=53 page=0 chnl=0 +char id=38 x=216 y=156 width=48 height=56 xoffset=1 yoffset=13 xadvance=48 page=0 chnl=0 +char id=39 x=499 y=320 width=10 height=22 xoffset=1 yoffset=12 xadvance=11 page=0 chnl=0 +char id=40 x=70 y=0 width=22 height=75 xoffset=3 yoffset=9 xadvance=25 page=0 chnl=0 +char id=41 x=92 y=0 width=23 height=75 xoffset=0 yoffset=9 xadvance=25 page=0 chnl=0 +char id=42 x=103 y=362 width=36 height=34 xoffset=-1 yoffset=14 xadvance=33 page=0 chnl=0 +char id=43 x=0 y=362 width=37 height=40 xoffset=1 yoffset=23 xadvance=39 page=0 chnl=0 +char id=44 x=483 y=320 width=16 height=25 xoffset=0 yoffset=57 xadvance=20 page=0 chnl=0 +char id=45 x=308 y=362 width=23 height=12 xoffset=4 yoffset=38 xadvance=32 page=0 chnl=0 +char id=46 x=270 y=362 width=15 height=15 xoffset=3 yoffset=54 xadvance=22 page=0 chnl=0 +char id=47 x=374 y=0 width=29 height=58 xoffset=-3 yoffset=14 xadvance=25 page=0 chnl=0 +char id=48 x=77 y=156 width=38 height=56 xoffset=2 yoffset=13 xadvance=42 page=0 chnl=0 +char id=49 x=299 y=266 width=26 height=54 xoffset=4 yoffset=14 xadvance=41 page=0 chnl=0 +char id=50 x=383 y=156 width=39 height=55 xoffset=1 yoffset=13 xadvance=42 page=0 chnl=0 +char id=51 x=434 y=99 width=39 height=56 xoffset=1 yoffset=13 xadvance=42 page=0 chnl=0 +char id=52 x=325 y=266 width=40 height=54 xoffset=1 yoffset=14 xadvance=42 page=0 chnl=0 +char id=53 x=422 y=156 width=38 height=55 xoffset=2 yoffset=14 xadvance=42 page=0 chnl=0 +char id=54 x=0 y=156 width=39 height=56 xoffset=2 yoffset=13 xadvance=42 page=0 chnl=0 +char id=55 x=365 y=266 width=39 height=54 xoffset=1 yoffset=14 xadvance=42 page=0 chnl=0 +char id=56 x=473 y=99 width=38 height=56 xoffset=2 yoffset=13 xadvance=42 page=0 chnl=0 +char id=57 x=39 y=156 width=38 height=56 xoffset=2 yoffset=13 xadvance=42 page=0 chnl=0 +char id=58 x=471 y=266 width=15 height=43 xoffset=3 yoffset=26 xadvance=21 page=0 chnl=0 +char id=59 x=150 y=156 width=17 height=56 xoffset=1 yoffset=26 xadvance=21 page=0 chnl=0 +char id=60 x=37 y=362 width=33 height=38 xoffset=1 yoffset=26 xadvance=37 page=0 chnl=0 +char id=61 x=172 y=362 width=35 height=27 xoffset=3 yoffset=31 xadvance=42 page=0 chnl=0 +char id=62 x=70 y=362 width=33 height=38 xoffset=3 yoffset=26 xadvance=37 page=0 chnl=0 +char id=63 x=115 y=156 width=35 height=56 xoffset=0 yoffset=13 xadvance=36 page=0 chnl=0 +char id=64 x=258 y=0 width=61 height=68 xoffset=1 yoffset=16 xadvance=64 page=0 chnl=0 +char id=65 x=0 y=212 width=53 height=54 xoffset=-2 yoffset=14 xadvance=49 page=0 chnl=0 +char id=66 x=53 y=212 width=42 height=54 xoffset=3 yoffset=14 xadvance=47 page=0 chnl=0 +char id=67 x=37 y=99 width=46 height=56 xoffset=1 yoffset=13 xadvance=47 page=0 chnl=0 +char id=68 x=95 y=212 width=42 height=54 xoffset=3 yoffset=14 xadvance=47 page=0 chnl=0 +char id=69 x=137 y=212 width=38 height=54 xoffset=3 yoffset=14 xadvance=41 page=0 chnl=0 +char id=70 x=475 y=156 width=36 height=54 xoffset=3 yoffset=14 xadvance=39 page=0 chnl=0 +char id=71 x=83 y=99 width=45 height=56 xoffset=2 yoffset=13 xadvance=49 page=0 chnl=0 +char id=72 x=175 y=212 width=45 height=54 xoffset=3 yoffset=14 xadvance=51 page=0 chnl=0 +char id=73 x=220 y=212 width=14 height=54 xoffset=4 yoffset=14 xadvance=22 page=0 chnl=0 +char id=74 x=264 y=156 width=37 height=55 xoffset=0 yoffset=14 xadvance=40 page=0 chnl=0 +char id=75 x=234 y=212 width=45 height=54 xoffset=3 yoffset=14 xadvance=46 page=0 chnl=0 +char id=76 x=279 y=212 width=36 height=54 xoffset=3 yoffset=14 xadvance=39 page=0 chnl=0 +char id=77 x=315 y=212 width=58 height=54 xoffset=3 yoffset=14 xadvance=63 page=0 chnl=0 +char id=78 x=373 y=212 width=45 height=54 xoffset=3 yoffset=14 xadvance=51 page=0 chnl=0 +char id=79 x=128 y=99 width=47 height=56 xoffset=1 yoffset=13 xadvance=50 page=0 chnl=0 +char id=80 x=418 y=212 width=43 height=54 xoffset=3 yoffset=14 xadvance=48 page=0 chnl=0 +char id=81 x=319 y=0 width=47 height=65 xoffset=2 yoffset=13 xadvance=50 page=0 chnl=0 +char id=82 x=461 y=212 width=43 height=54 xoffset=3 yoffset=14 xadvance=46 page=0 chnl=0 +char id=83 x=175 y=99 width=42 height=56 xoffset=1 yoffset=13 xadvance=44 page=0 chnl=0 +char id=84 x=0 y=266 width=45 height=54 xoffset=0 yoffset=14 xadvance=45 page=0 chnl=0 +char id=85 x=301 y=156 width=42 height=55 xoffset=3 yoffset=14 xadvance=48 page=0 chnl=0 +char id=86 x=45 y=266 width=51 height=54 xoffset=-2 yoffset=14 xadvance=48 page=0 chnl=0 +char id=87 x=96 y=266 width=64 height=54 xoffset=-1 yoffset=14 xadvance=63 page=0 chnl=0 +char id=88 x=160 y=266 width=48 height=54 xoffset=-1 yoffset=14 xadvance=46 page=0 chnl=0 +char id=89 x=208 y=266 width=49 height=54 xoffset=-2 yoffset=14 xadvance=45 page=0 chnl=0 +char id=90 x=257 y=266 width=42 height=54 xoffset=1 yoffset=14 xadvance=44 page=0 chnl=0 +char id=91 x=115 y=0 width=18 height=75 xoffset=3 yoffset=5 xadvance=21 page=0 chnl=0 +char id=92 x=403 y=0 width=37 height=58 xoffset=-2 yoffset=14 xadvance=31 page=0 chnl=0 +char id=93 x=133 y=0 width=18 height=75 xoffset=0 yoffset=5 xadvance=21 page=0 chnl=0 +char id=94 x=139 y=362 width=33 height=28 xoffset=0 yoffset=14 xadvance=32 page=0 chnl=0 +char id=95 x=331 y=362 width=34 height=12 xoffset=-1 yoffset=65 xadvance=33 page=0 chnl=0 +char id=96 x=285 y=362 width=23 height=13 xoffset=0 yoffset=12 xadvance=24 page=0 chnl=0 +char id=97 x=0 y=320 width=37 height=42 xoffset=1 yoffset=27 xadvance=38 page=0 chnl=0 +char id=98 x=440 y=0 width=37 height=57 xoffset=2 yoffset=12 xadvance=40 page=0 chnl=0 +char id=99 x=37 y=320 width=36 height=42 xoffset=1 yoffset=27 xadvance=38 page=0 chnl=0 +char id=100 x=0 y=99 width=37 height=57 xoffset=1 yoffset=12 xadvance=40 page=0 chnl=0 +char id=101 x=73 y=320 width=38 height=42 xoffset=1 yoffset=27 xadvance=39 page=0 chnl=0 +char id=102 x=477 y=0 width=28 height=57 xoffset=0 yoffset=11 xadvance=27 page=0 chnl=0 +char id=103 x=217 y=99 width=38 height=56 xoffset=1 yoffset=27 xadvance=41 page=0 chnl=0 +char id=104 x=255 y=99 width=36 height=56 xoffset=2 yoffset=12 xadvance=40 page=0 chnl=0 +char id=105 x=291 y=99 width=15 height=56 xoffset=2 yoffset=12 xadvance=19 page=0 chnl=0 +char id=106 x=197 y=0 width=23 height=71 xoffset=-5 yoffset=12 xadvance=20 page=0 chnl=0 +char id=107 x=306 y=99 width=40 height=56 xoffset=2 yoffset=12 xadvance=39 page=0 chnl=0 +char id=108 x=346 y=99 width=14 height=56 xoffset=3 yoffset=12 xadvance=20 page=0 chnl=0 +char id=109 x=186 y=320 width=58 height=41 xoffset=2 yoffset=27 xadvance=63 page=0 chnl=0 +char id=110 x=244 y=320 width=36 height=41 xoffset=2 yoffset=27 xadvance=40 page=0 chnl=0 +char id=111 x=111 y=320 width=39 height=42 xoffset=1 yoffset=27 xadvance=41 page=0 chnl=0 +char id=112 x=360 y=99 width=37 height=56 xoffset=2 yoffset=27 xadvance=40 page=0 chnl=0 +char id=113 x=397 y=99 width=37 height=56 xoffset=1 yoffset=27 xadvance=40 page=0 chnl=0 +char id=114 x=486 y=266 width=25 height=41 xoffset=2 yoffset=27 xadvance=27 page=0 chnl=0 +char id=115 x=150 y=320 width=36 height=42 xoffset=0 yoffset=27 xadvance=37 page=0 chnl=0 +char id=116 x=445 y=266 width=26 height=51 xoffset=0 yoffset=18 xadvance=25 page=0 chnl=0 +char id=117 x=280 y=320 width=36 height=41 xoffset=2 yoffset=28 xadvance=40 page=0 chnl=0 +char id=118 x=316 y=320 width=39 height=40 xoffset=-1 yoffset=28 xadvance=37 page=0 chnl=0 +char id=119 x=355 y=320 width=54 height=40 xoffset=-1 yoffset=28 xadvance=52 page=0 chnl=0 +char id=120 x=409 y=320 width=40 height=40 xoffset=-1 yoffset=28 xadvance=37 page=0 chnl=0 +char id=121 x=343 y=156 width=40 height=55 xoffset=-1 yoffset=28 xadvance=37 page=0 chnl=0 +char id=122 x=449 y=320 width=34 height=40 xoffset=1 yoffset=28 xadvance=36 page=0 chnl=0 +char id=123 x=151 y=0 width=23 height=72 xoffset=0 yoffset=9 xadvance=23 page=0 chnl=0 +char id=124 x=366 y=0 width=8 height=63 xoffset=5 yoffset=14 xadvance=18 page=0 chnl=0 +char id=125 x=174 y=0 width=23 height=72 xoffset=0 yoffset=9 xadvance=23 page=0 chnl=0 +char id=126 x=229 y=362 width=41 height=19 xoffset=2 yoffset=36 xadvance=45 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=385 +kerning first=84 second=74 amount=-8 +kerning first=86 second=100 amount=-2 +kerning first=114 second=113 amount=-1 +kerning first=70 second=121 amount=-1 +kerning first=34 second=99 amount=-2 +kerning first=70 second=99 amount=-1 +kerning first=69 second=99 amount=-1 +kerning first=88 second=113 amount=-1 +kerning first=84 second=46 amount=-9 +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=121 second=34 amount=1 +kerning first=40 second=86 amount=1 +kerning first=85 second=65 amount=-1 +kerning first=72 second=65 amount=1 +kerning first=114 second=102 amount=1 +kerning first=89 second=42 amount=-2 +kerning first=114 second=34 amount=1 +kerning first=75 second=67 amount=-1 +kerning first=89 second=85 amount=-3 +kerning first=77 second=88 amount=1 +kerning first=84 second=115 amount=-3 +kerning first=84 second=71 amount=-1 +kerning first=89 second=101 amount=-2 +kerning first=89 second=45 amount=-5 +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=79 amount=-1 +kerning first=75 second=111 amount=-1 +kerning first=111 second=120 amount=-1 +kerning first=87 second=44 amount=-5 +kerning first=67 second=84 amount=-1 +kerning first=84 second=111 amount=-7 +kerning first=84 second=83 amount=-1 +kerning first=102 second=113 amount=-1 +kerning first=39 second=101 amount=-2 +kerning first=80 second=88 amount=-2 +kerning first=66 second=84 amount=-1 +kerning first=65 second=87 amount=-1 +kerning first=122 second=100 amount=-1 +kerning first=75 second=118 amount=-1 +kerning first=73 second=65 amount=1 +kerning first=70 second=118 amount=-1 +kerning first=73 second=88 amount=1 +kerning first=82 second=89 amount=-2 +kerning first=65 second=34 amount=-4 +kerning first=120 second=99 amount=-1 +kerning first=84 second=99 amount=-3 +kerning first=84 second=65 amount=-4 +kerning first=112 second=39 amount=-1 +kerning first=76 second=39 amount=-10 +kerning first=78 second=65 amount=1 +kerning first=88 second=45 amount=-5 +kerning first=34 second=111 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=89 second=89 amount=1 +kerning first=89 second=117 amount=-1 +kerning first=75 second=117 amount=-1 +kerning first=76 second=65 amount=1 +kerning first=34 second=34 amount=-1 +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=80 second=44 amount=-14 +kerning first=98 second=39 amount=-1 +kerning first=70 second=65 amount=-7 +kerning first=89 second=116 amount=-1 +kerning first=70 second=46 amount=-10 +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=65 second=118 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=65 second=110 amount=-2 +kerning first=114 second=121 amount=1 +kerning first=89 second=100 amount=-2 +kerning first=80 second=65 amount=-6 +kerning first=121 second=111 amount=-1 +kerning first=34 second=101 amount=-2 +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=68 second=88 amount=-1 +kerning first=81 second=89 amount=-1 +kerning first=114 second=111 amount=-1 +kerning first=46 second=34 amount=-10 +kerning first=84 second=112 amount=-3 +kerning first=76 second=34 amount=-10 +kerning first=39 second=115 amount=-3 +kerning first=76 second=118 amount=-4 +kerning first=86 second=99 amount=-2 +kerning first=84 second=84 amount=1 +kerning first=120 second=111 amount=-1 +kerning first=65 second=79 amount=-1 +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=-6 +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=-6 +kerning first=34 second=39 amount=-1 +kerning first=65 second=121 amount=-2 +kerning first=114 second=44 amount=-6 +kerning first=69 second=84 amount=1 +kerning first=89 second=46 amount=-8 +kerning first=97 second=39 amount=-1 +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=89 second=86 amount=1 +kerning first=86 second=113 amount=-2 +kerning first=88 second=71 amount=-1 +kerning first=122 second=99 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=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=89 second=67 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=120 second=100 amount=-1 +kerning first=104 second=34 amount=-3 +kerning first=86 second=41 amount=1 +kerning first=111 second=34 amount=-3 +kerning first=40 second=89 amount=1 +kerning first=121 second=39 amount=1 +kerning first=70 second=74 amount=-7 +kerning first=68 second=90 amount=-1 +kerning first=98 second=120 amount=-1 +kerning first=110 second=34 amount=-3 +kerning first=119 second=46 amount=-4 +kerning first=69 second=102 amount=-1 +kerning first=118 second=44 amount=-6 +kerning first=84 second=114 amount=-2 +kerning first=86 second=97 amount=-2 +kerning first=40 second=87 amount=1 +kerning first=65 second=109 amount=-2 +kerning first=68 second=86 amount=-1 +kerning first=86 second=93 amount=1 +kerning first=65 second=67 amount=-1 +kerning first=97 second=34 amount=-1 +kerning first=34 second=65 amount=-4 +kerning first=84 second=118 amount=-3 +kerning first=112 second=34 amount=-1 +kerning first=76 second=84 amount=-7 +kerning first=107 second=99 amount=-1 +kerning first=123 second=85 amount=-1 +kerning first=102 second=125 amount=1 +kerning first=65 second=63 amount=-3 +kerning first=89 second=44 amount=-8 +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=86 second=65 amount=-3 +kerning first=65 second=81 amount=-1 +kerning first=68 second=44 amount=-4 +kerning first=86 second=45 amount=-6 +kerning first=39 second=34 amount=-1 +kerning first=72 second=88 amount=1 +kerning first=68 second=46 amount=-4 +kerning first=65 second=89 amount=-5 +kerning first=69 second=118 amount=-1 +kerning first=89 second=38 amount=-1 +kerning first=88 second=99 amount=-1 +kerning first=65 second=71 amount=-1 +kerning first=91 second=74 amount=-1 +kerning first=75 second=101 amount=-1 +kerning first=39 second=112 amount=-1 +kerning first=70 second=113 amount=-1 +kerning first=119 second=44 amount=-4 +kerning first=72 second=89 amount=-1 +kerning first=90 second=103 amount=-1 +kerning first=65 second=86 amount=-3 +kerning first=84 second=119 amount=-2 +kerning first=34 second=110 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=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=79 second=89 amount=-2 +kerning first=84 second=122 amount=-2 +kerning first=68 second=84 amount=-3 +kerning first=76 second=86 amount=-7 +kerning first=74 second=65 amount=-1 +kerning first=107 second=101 amount=-1 +kerning first=80 second=46 amount=-14 +kerning first=89 second=93 amount=1 +kerning first=89 second=65 amount=-5 +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=87 second=46 amount=-5 +kerning first=47 second=47 amount=-9 +kerning first=75 second=103 amount=-1 +kerning first=89 second=84 amount=1 +kerning first=84 second=110 amount=-3 +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=-3 +kerning first=88 second=118 amount=-1 +kerning first=86 second=114 amount=-1 +kerning first=80 second=74 amount=-6 +kerning first=84 second=97 amount=-6 +kerning first=82 second=84 amount=-2 +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=44 second=39 amount=-10 +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=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=70 second=101 amount=-1 +kerning first=114 second=97 amount=-1 +kerning first=70 second=97 amount=-1 +kerning first=34 second=97 amount=-2 +kerning first=89 second=102 amount=-1 +kerning first=78 second=89 amount=-1 +kerning first=70 second=44 amount=-10 +kerning first=104 second=39 amount=-3 +kerning first=84 second=45 amount=-10 +kerning first=89 second=121 amount=-1 +kerning first=109 second=34 amount=-3 +kerning first=84 second=86 amount=1 +kerning first=87 second=99 amount=-1 +kerning first=32 second=84 amount=-2 +kerning first=98 second=122 amount=-1 +kerning first=89 second=112 amount=-1 +kerning first=89 second=103 amount=-2 +kerning first=65 second=116 amount=-1 +kerning first=88 second=81 amount=-1 +kerning first=102 second=34 amount=1 +kerning first=109 second=39 amount=-3 +kerning first=81 second=84 amount=-1 +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=-9 +kerning first=102 second=39 amount=1 +kerning first=44 second=34 amount=-10 +kerning first=34 second=109 amount=-1 +kerning first=84 second=101 amount=-3 +kerning first=75 second=119 amount=-2 +kerning first=84 second=81 amount=-1 +kerning first=76 second=121 amount=-4 +kerning first=69 second=101 amount=-1 +kerning first=80 second=90 amount=-1 +kerning first=89 second=97 amount=-2 +kerning first=89 second=109 amount=-1 +kerning first=90 second=99 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=34 second=113 amount=-2 +kerning first=89 second=41 amount=1 +kerning first=75 second=71 amount=-1 +kerning first=76 second=87 amount=-3 +kerning first=77 second=89 amount=-1 +kerning first=90 second=113 amount=-1 +kerning first=118 second=111 amount=-1 +kerning first=118 second=97 amount=-1 +kerning first=88 second=100 amount=-1 +kerning first=89 second=111 amount=-2 +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=-3 +kerning first=39 second=100 amount=-2 +kerning first=75 second=113 amount=-1 +kerning first=88 second=111 amount=-1 +kerning first=87 second=111 amount=-1 +kerning first=89 second=83 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=-6 +kerning first=97 second=121 amount=-1 +kerning first=87 second=113 amount=-1 +kerning first=73 second=84 amount=-1 +kerning first=121 second=46 amount=-6 +kerning first=75 second=99 amount=-1 +kerning first=65 second=112 amount=-2 +kerning first=65 second=85 amount=-1 +kerning first=76 second=67 amount=-2 +kerning first=76 second=81 amount=-2 +kerning first=102 second=100 amount=-1 +kerning first=75 second=79 amount=-1 +kerning first=39 second=65 amount=-4 +kerning first=65 second=84 amount=-4 +kerning first=90 second=101 amount=-1 +kerning first=84 second=121 amount=-3 +kerning first=114 second=39 amount=1 +kerning first=84 second=109 amount=-3 +kerning first=123 second=74 amount=-1 +kerning first=76 second=119 amount=-2 +kerning first=84 second=117 amount=-2 +kerning first=76 second=85 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=-6 +kerning first=79 second=86 amount=-1 +kerning first=79 second=46 amount=-4 +kerning first=76 second=89 amount=-10 +kerning first=68 second=65 amount=-1 +kerning first=79 second=84 amount=-3 +kerning first=87 second=100 amount=-1 +kerning first=84 second=32 amount=-2 +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=-10 +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=73 second=89 amount=-1 +kerning first=39 second=39 amount=-1 +kerning first=69 second=117 amount=-1 +kerning first=111 second=39 amount=-3 +kerning first=90 second=81 amount=-1 diff --git a/src/web/static/fonts/bmfonts/RobotoBlack72White.png b/src/web/static/fonts/bmfonts/RobotoBlack72White.png new file mode 100644 index 0000000000000000000000000000000000000000..f3bbba30064241e25c4597769c73ec05c7faeee3 GIT binary patch literal 50192 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 >vQS1z_=Sp^I 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 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 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4mJh`hA$OYelajKFnGE+hE&A8nalekH#+j2 zz91L~r3)(-X(~Ah2AZ*}F6wX*QSB)faMhfm-Ib^@w^zKUr>9BC{=pm-ovx4*8b_-w zJiEN2rUd;c{C4ir?edkich`Kr_w~%yXLsJ$9RIy1c~5fR)^GP8|2cTr^7sB@GLIvR z&srY8Zhh>J{Qu;1FnF#lEckQNMN6Cc%KjctZy&w(nE%g|Q^`~IJzwy@^g=iPulb+v zpAR^9{q)8je%UXr&HsKcSU#)h->(J#%P*w!SABN$FZuNH+Jg5_5-*1=-6O*D;pbL% z+kXq5%YC`j99MavU-k<#``xEY{4*Qu{o_^L&)b(@m@oT%^ku)4v)6uD z5O49~H~%j-OWArg|0s?7pT4ud+;l%(c+QV7&Cj=WLiHXmKVq4;WlFy{$NiJ34gKq_ zUvRS9#`b+b?LX(=@5Ai2VSU$E*e+Xj;5s+oFKx@X`xoM6zAXN>!^F=qJgnKK{;Xqt z!G+Bq^3~7ppZfUldAaG6PHpdAd;IhdwgdCMPrCEX>fZRWx@GpO??;|*T_)47f9hrV z-BphFPaa;j(yLPQ#g|U6|I7J*r9ZjI@NT-g=S=_5x&LNdFxV6S>*xCBy0Q)GtCx44 z_vX9woca0X$Gt(%c1Es>Id+d>#cMRomcC5`;!}&(`y2zHcJ~m z>gW5FrFl975!foVi$4B7@bZG3!K8^^Q+mrn@ItN{Xp({c{<9{>pPj31WplF`>C%{wN213RUyp^3KG3 z>1L(CpJf}rR!H7vI3S??xSRRZ!3&YS_2EwwW~^niS-d>^bk?PY_bin+OhPsFSvluR ztozcotolc8rpzOsAIVNfcYb}iy7C``zt4_)HuvB(hyaA;|cPCM_+rrAX@k_Rrlk2zB#qouIrOnuxAd!B!k41UUsI6AZ13a_+$SqU;LhN z<>S$Gx1Nc-4H@fx+&7!j%+(V6V8xB_m(4}{BL6%L`JkqGIb3wg)QRoB!Bam4U3g#| z{PXd=AL0-9cd}+4c5`Un@9}=B)&If^yZNhHWtb%e_ctd7_h>!b?mnkW?&ptRz4Mf6 zmwdA2iuv;A2$C z#|;-w>+#wf2Wp$$G@3qV*~Fju*r(r&o|%(_2m zlT5f)yj#U#!T5O+%F+xK2kuXr)Gt5f)i zPi~7#Ze#>K5ax|}|7-q1qb=9so|Sva{QNUJNX}UEL%6WpU5WP-jcPZWRdjtR`*^T) z-;&}}$F{!v>nBmVIkm)l>S>Po?u(z){4egZ9wk z-m~^)|Eo^7eDSpU&IF^kOWKM)7^Jp1Uk&3;ja)eCi0NgijP`lPa%xw92t8e@@S(Hx z5D$yl%`c`0)vqZU-$`w1-0zVi!gptiQLWe9c}g3-WcDA_($$pK?z53oPmPR)2KB`E z^?Iq&3>P(&>h8avx~XSYL2iwgMTc?3CE@rN8HWE{t`ue#e3;%XG_T&ZW}f0so4s)y zdM&9ejh;_WbL8*n($_s^DaEkP>h!@`IsKvio~t*?7AIWJelO|zdfv;g7gzcDPrPAJC-~9#RN7_s=(^QY zLw5Fjs5J3<@_WAbO*RLUFZHpf{zSLBHHga_eHO7?s=9B%^pl2qj9>Y`SO%Wok~gPm z-5M#|#Z#X(@}}k7ikf%7%<5W^_o*#9pkxqQlWDlFh`GV`!v_8a-`y)V%AA>7_2pQB z!NX;B`+Jz3_pQFTGV+u+Vt@_Snq{aHR%B*Zh$zIGZE*S!1EK(1nNzAq=CwK-GVbXv1+qokQ`=u#aYbp?)G zlMIH17fNRZV->6k>o01v* z*@gQ~bdsC2C?xU09K@I*7a^5-Suabcz4sP_e%AHS0I_xKV(~^yG2xdFCrtYk^x(&z z7Ehz%;8X7}tzI$7P3Op@Yahdf+oB||FY%cl*1p^$$9TS~o13op?b6AIk>ZUlv9CTHIkIf_yU9jx zmw4UGdEb29bEcUpH1MAO?hngmK2Uv8Qo`t0(wEJLUfS8;z0tF76Vv&M7u9?JC;45{ zI&rznHs-!YYSdhPPnoUpLIKs9(%?~xn=~6haIrroBMdALIKcH?htDd=S-M1?N+qPS=&E=o1 z2Xa%D+(Pl5V|-Eoclc8PN)Cu3oY#rJ-NxYt8Div_-k*wCN7e_Gjw0ii?-80l3YYTdn_uN zA@ST!eML-8RJdPc&SNEm+>R@XA&V|b3+#`+7RPlZ{h}Plcdy8A9F?b27|Z(RT+*mt zyNEwm^K#kB!;{sI{I96-Iklx*>eMlf8ON6~_=V~nzE{1&?Ba@4Z7-7-84rIvvdp;p z;qTTB@<---$(qc%r}k7Ruu4}`KF3w)L)nQtT4Jvm>ntkk#7dKmcb($0sa~5Fa_#EF z8w&pZImHTFIwJS+R3F~>I2-QRbbIxnWu;H2h`v3s^{J!aS^Gz8t8aNX{(7_Vo3ZQ0 zO$ifjyeq#^U+=Z}&(W{BmZ?`N>bSs8Q<`^^yJlH?y041A z+O{weZ~n-AJsXU?5TOWj*ikcyL;D^a{mAL6?0wm!;Zx88yKX6sZD#_Ldc}`a_+%=* z7f<`R)+TqN_+i0!$&9Ac&faVA2$#LOnJx?S##iz4J|2x8=y|_i+EyS>2MXSBD z@LRD`Dkw(RxIfA>m@0CcORQ!q$SKLu+Ty*J8@rwvFXvMDRF^1r|5Wh$cS7>tRl}ls zQ#ZGXN>!TWP7c}0<74}0@!!X60&nA3Ji_5?*d-v;~$(n!X#!mh0 ze6{y%#smSGnpG60cDP(Iq{lzl-xr2gde|o z?s96&x;dLZOj7EfSNnBsgp2JDzo$zbYL88ukXRiYeNSWK!z=?|YnOZaXC6gYz7oBi z^=Ho~IOV#Y?s4O6O?!=VALq@n$xd=qKD|MG$*0tosey;Y7cWoh zJMu!a9Uf#ostI$Zi&L(tENuLA zu+K4Iq3-iHO3#j8%?<(G-u6|x@qh&Np(#;yl#MCJt zj=oqqL)ozERAS3}2mdLDGwxkIt=Vm>aY0(A&!6i%=i{BHyc>V@^adp4GezIiaNW1O zmIs=~zIN=*;LJazm~ zo%0NeKCv%Pc9J^B9yaafpYwV}b*o+-OWP7S!~EhN@7isV$BMVSS@~<*zR0=HA38t! zT&4LrwPvXq!y>Q8ahjL)y`~)2ddg{^BEed!i@`e*A88x?TYs1TN2%uyz#`E;qm zuQx|{3qQ$d9NQw}^D|rY9e0~AsNC3mL+agk@0r&^H|2y`K8nn~+!plIO@ZSA-}miY z!NP)poaK^f3pu1L6`m<9l+B#t17%`-yxId#5!`qkXM^{=LAnn%tsNkLMhIbIaTD z%gxRYR?-T`Le_n{zH0RyhJ<^E+E#~c$`MPR*cTA~)Gg=j8b!Ibho&4ih4KXUNvv_K z;JX~y+4i~c-S1;lE$z>~I+k`ta~uDEpUZzDe2*DN?`t$?5<6~`Yr)_1QVmq6a_c)lYRi=NoMY?wn32Qq zKx*rp>LVY8A5E_E?A4mSv8pq8xdHooRu85gy&Lb(Pb+9%_It~s-z^sk?9SER-(mE8 zdF0(2Jys&TFV7{^6@4~X`LV~wc3pR|&gHhx2d4G={64$7M)D!+v=2v5gq<-pGZ-#r$oaQz4 zv_$FLKf4O{`Tb1rmOUu%H8a2e#nzI8pC?ZGG!@h?4DOt;{LiLibls556Jl^%`W61A?k0OJf-BzyBjr~xwWJ%4^E4^nm zR%X~;Pp;bOxqiy?7fRZF-CG{oY!Dalon5*D77ih2bd$DB6aLDxfwRLme#OQ)kr%tm zHh4YQ7$kQ&NbGLxnTFVhLA`-$;Izwf&0V~bPbB%(LM=qyEWEmP1zrIv3=W=%-vTWzH~pz zUcWulxpWHe#_|a!d*8EwyGwyVacfw9Z1{BW-r9+EZS0G#2Oa*o!~fKub9zvhZVb!p zE!aHWS4EcYU5#ckdbo zrsi46&Q|;RwH8T5?CTWd+LJA_IO=|oCU-BJRHDy;3>G)mORr~vOa0qDtoK)+D4wvY z+DZc{Oy_CKy;LohO+EifYHr3C)5-u)H##tA#^LWj)_iK57}WoTtKH#CRF;Lj@8U^| zmx5z^Mc^9OTTFVlUe&Bue=0e(oBdxL*trLq=M+u{_eMG6d#o=qoM>owdG$6;df$0| zuK9c)GAzqi{Q39ko1*5050!mWDt85e8e?x<_1=rBpLa|;eT89KMdtgpkfxaCOU~jy zayQhjFI%e>xlOD(_wAO)NxrLM+^z6>Y>CRglYB@jHanFA41*(b8ygOvs%-hpt!>)Pj&r!YhT+!ML z@0r`YJ^kN&f7_MrHEnI{M5F%bpk=yCPNlY-U%bTN?URp7+2r@@q}J_SbbZpNgWDAC z(htw{`gv*5edjZUna_BHeicr!Vt7-U``Vbv;(fwcx+_;WeJ@8t_`~LbL-Nle_HCgzkSBzOJ^iaxYk~94|v6J>Ahz7yqoKqUy56l%wyN!YN*-< z3%wlGFk$7(3^NW+x=^y@{jvAW4F|4;&-^xTcec>`)OhCJ?Agoz-D7>Vc|~H}6HAY) zMpL0~`fy~M^nS(ETWYGOzLjkM5i-9T5~;7+1f*&nL_p(n&*8eVMY|_%cRe*-?5|u> zpSN-P1r46=o+np>ra3OP+5Gd)e8J~CJmx-66noqX9VWr zQFZ>qX;&u-r5aZjU%PsjBNQ}}FhxW<_4j%Oz7;hUypje*SGo31o5H{C zu(C&KfK=;&=l(*rj^Om?m$_@EafZO`^7~xZns052(LCSt#r001+!l%Tw_ce(>`v5v zwl?X?DaWHnm{u}O<1$;zwQ|a1PEmd4;>SCFzBSyNx|L(%{~5os7htd zcJmJ2+4yx+=#R4!5v7JpjsHc3KwPTB`E^E^`Il82-+1JRU$LKH^i~PnCsjQfmiaQZ z;Z5({#oHL$KpEJD_j$P2%;#YX4stUAmm$yH|JfgimLxrbg7q$%t!K%B_)jXnhB%pv=&Hp1GZ0Gh@{Psnat{rx)aT zrT#71v}Vl)akkxU=^H99O^K`2G_bpwy{hr+hR~d1nGHJcP5woR*kA$k^XGfK*S@@<;MBn%v%dZ+ z-Nb6+ybR=emac2Rm~>vQe!I5M^z-}K`~@raaqij$n1h zqxarckJr0?S*V+OOV6|-sk$aXS$($I(zZ>}CF_4&*uHKRIKMAk;r>_ZsdAOrl5gys zF6&jFGIupa&R)Jkx-_`PuKYsIX}QnaLQmb+>0O%ljE89>yJpnk>q~v)Ilcc!FvM^D zzRqT|SXvct<|CU3SMgt$L-+N3X!HJ&l%n-a%+ToRgE<@TGOm_xP&swb_jHETHSZ}~ zT_xQ5lbAR3Zul1T;75w+=M(pMZp*N**>=42&s7)sTS3d-1~sd1TVk}gdvU1qP2HsP z^HQ&z@B$}cPFLmHs`@Dz>Q_^_G8cnVhQq}Yv8wxK_v2ij`kYsNoGlg|x#i8ri)ZIB zUJQOdNoH2OOQJ}woX|d3-^ydH?-ukGDe!o%TNWl$Zwjr^ADn!>B*E*z zGVj$3--P|0_akPq(OSLff_D8vU14`>)_yuDWyrAjsM&qFi`kw(XB-Wyc@ULRm1gpy z_fY4zvp;r&!u7QM?wQ5~E%Gn!KcB?&-E*d$@;4>fBLX+JbWcgYcDgcP&SNFhIqYgY z4~_OkgU3c&^xd{4t?Qhq%dXF!)UrOzcgo?YGh5nIrfOuj$O)Z$JY_o5qse=FSOb5X z<%V4}dysKw?Iq#*i7%X^Pi=`^RdMLIj*Wmw#^q(9n%2r&AI&bef{PvMyG5t->}=MD&VFE}QX zCE;*Y!|Z?M_9>a_n&)%8ulIKr$1G|4d_W}D;QoqF;2zTlC0RDJ*Bf7otDZ{K4*TF& zKKt5KE$+)P<<_g-?VEPt?xCM{pnlUb(O#yI0;x+ByV4b+!?asf@M{|J)axv*1U6E>@YR`+9q%Q%c19- zm%LD14e8+pbQ&D``l_=iV$Q+YOMG5?O+D(7ZzbCC_A}o+`O3igk$0n1VmHS7a@PN-USNHbA%XF{gxt2w-6B)&E|z{2tP>jY)Ggu3itWYWvro;E3OM&Y zo!9Ha7OS3)wMH`vY8(RhwsV!gEL1=0rgHGN>(bJiy7LfPLg^zFt)QA>vs3D^HkpIXm;Py`@tAMUWOY~`##HZ9zKQXG(&de+8+>ms zUGPS3W5V^M%}Swpz5X8i1R3P-SqrC4ySL}_-}&3UcCua9Pdb)UX}Yjb?$M7KUw4Hb znsnyf))|+2UIo?I_jKqTx7c^1wtTw9@ue4M&S`n-BzTd(X{XTHvzPDBRO(U;dn)Oy z))R8Ttw1iTs{6+Swo5;azOgx+ym`KJN@ncJO&=D?^-t92?sRjVw|bArit{##v#gj7 zAH7j^wOUY6i?OF@qi?Lq3(K32)kFVO?wZWYu--iCZkqYx1#jXuCceHZvrB6`E6d!i z`?hfJO}gg6w&KaOHnpGs<_EsiwfJ{4XxeMe?}FTYF^)B74(*j~e9cmwS7GSABY)lL zjTeKN9lx!9+^a9v5^qzz%TV$mi<`Zg12e;6%XxN7FEKtzJ$>QRqTKYZpEXY zOHWj02=4ZHH@y@(7WO6N%tQ&>rv~Th{J`m?b>60S>+82xW~81e1*Pa?N6NN*KJd3o zR_Nn}Tvo@q+s?T1{#)}dgqb0Dy4;PUmzKR1)2!|`xbwcyIm9Y7udV#_$2}FwOh2r@ zHLTotYj#6Ty@^?tf*JGmS=SF9{@WawZ1y$Wq2`Q}Wc}(%*AJY2c_H}x9rJe?%Rheg z*V?-*V@_$Z(I(p;f;Wt|+=%|07Ur0E%vf6L;Fkv7_O3bYVjI8S?oa}C{7z{+?7kRm z=$`at%3+J&mJ^NEYKMx=e#Pi7_WTCl#tMlK5+KKmX#Gb7sm~qV z&8u8a-P2mH+{$oJwO4EU!>0=mdT=ylXP?jEeY^73e)%tpp02RkEWXWat=EHOF)3UA z`*ukhKh|yA<9SMaH~)sydbWDKr=HvjI%4rbSu|~GmabY%j`!=!6=}}4XB*=t6w5`4 z@ZR(b7T1b*i;x60+O(c|^vo-gfDAS+J=Ha>I?4X?qZ@n+ufH|yU7)GY`fBqmgZ}xI z71s~!jppt0`Z4>cT+M-HZznzOyn9|^mR{d|rR8q~lVrC)b&XyXbFB2?>h!7~Yb^GD zJ@q~P;N86m`|d4$zj6ii<-b6QZv$Bg`!)Tz|%_0~+(I{kF)&a|j*LYSQ-* z*mC=3bzd;F@;*BDzTYyhBVX)%t|1S-~p%S*7scCu}?8U#Te*CY#vGPa3 z4A;{qH@oI+o*+NbDE`<^?cE^fKE5jZbi;i)?}D6LuAM~^7dKs*I%ku?AF;-aU)Q8% z`J?a13*LK|z}k0y%>!2Fqa8nv|NO~m`R&k$e4ly+-nRCe`DV?5lNKGk|M2}KUIw$H z#=ZM`!jAvU5N&#E^Ul^|`socn7k+YaXSY2sHN(n*f4jt|?Ot{DZnE|Oxm~MtA*R44L1eD?k!=uxK*$3NObkLCWYP?Ng0t!3msk8+nztP z%tda7_omccMW1p64@bW{^rI+?VL?*zo;8bQt`!{qumwC#Xf~gF8N*pI%hXtBHqi?c z{)t-laV#*JBhB)mMb-JmNB8Qb+0#7YpDsP{u~Kj0_rrDDC3#Bg`d2)*`V+g0@xU^d zoKzzh+2@Nxb5gIw%Wl~k6H+$go6F%3Pm6L?_Br&4mSmm2=O@3TUrzlvLq5yXb5nKO zeip{w`V#tfj>TMooQYPGGQ2&v<)}Vtcm133%g!w@9H=lClyQ}&>PrNAKaeliV>-#quPbwbv%wemTIYs=L4vR_3>6vZ+6(3iM zo#yA)p1n=rq_I%d@ZWxR;|8QXm#z=hAH3)%s5Ww>pOklYiVd#U4FpP zmB|H~n6A`YX#P!cr7`P*w{PCWBo<7bXQK40=;o)19dB2OZCrD>)W&&@lb+m?c^>m7 z-k3Ih^(ohAcQ{4{L*#<(|FV<$5#e=VV_{18JuT!=mkx3z<$=TurL>PYAb} zT^8hQnUl5fn_rg`Z>nKp$>Uu=7HrDt^1T=o$7S|IM$=@AphbHq!}Txe*4EbH63^4z z`diKxyWgnU5@jZHQAD4qiRt`=?Y*j1$`^}{M9qsbKf><4FDGiQ{^QdN84omlWiUHD z<5KCg=GQ%E7wiAl%m_W@Sa;+9oyTocUOI=K%9_~jds*e_(u9WdflkajB?@ofyt(tx z4e4)7<~{W@N!yc2wS`M%%!*o9s?6TD$}3X$(}v7hOZJH9$iI8@=1s~$;}(w7@87XhtX=#y zNxsGBo)&Wt(}kz;yp>Kjo}Owxr@gvHa(W&^!s9;aqC59xc85On&D(sT;?Oav8*|S| zdv5$Q@k`~`BV{)Zb1+vg-+A@*^u=DwGP~9^ZHjuDc-h3WQZvHt&dr-QPd24<2$t#X zHhIV_{IyqwYH7{KnSzJS80_+dOPabWP1h!! zI?1(B`NhX-g&jPOjo)~?1#A+OSFBhlwYNv>%CYw>l{ZX$gW|ZR@t2pCm2G+;DWvf# za&GhPrC%P{awu=C%r38g)@E~4uQTw%;&Y&dXx=v&67;!Gey}{re|9^^?ZC~vbJPC$ zPHon8%udKJi1ypeR-?{8dzt4cZnxgBD2UUOd@3*)C3h z@iy|Gf6%hEPfu=>I@WET6E!VgJi34SCdLOx4N`va7Vc9|IrUQX+rIu~@^&ju?L1wV zkx}@(JNImR_&dQf?elB}<+pF%ys>$;k>767 zw7vV^8b-yrt~!{l{?@Q{(!Bb}ny_dV_R1=6@sGCC%>TItfmfVqN;72qN%$=LEqcKv z*CT(=Tn{Fa~z3RS;Hi4DlR;KYSHw?B$zKOL7|Lc|7B*QA8&K5t*x#5 ztKKyCb9~-s`o=J7%_YXuRi9=}ny0v}_3d4u=O?9XBIMF9?}Au!^LaZcZpxI7tlZOa zBWQ=@>^SW#h6_te!er-NoczcAV`bNm$|Ub_{ynGfKJ(wY83c-#ZWIl7Y^FKl0pD&41g^v)xP2m|p?q@^j~=9t(>RO`JEi(K1raZQ~N1pylZt zpTEVm{Cw;)Z~hj}Cp#MZ&aPiI7Ywv}*Y1$|k|)U=*O>L0Z|gzhmIgCfVS`-3>`yiG ze#tl%me2n;?;65fUZ?zBsj>AeO1pFSh30-(^VPWM3Ga{XCyVPI%e(1ziO){i0&?zV zuzMHZSRE66Fgq}JUTE%@HLYhmmu-FPH}6K=^=1Cq=^*gjD_MJ8%MI_CtUvpvRGNa4 z07zJ1+azC(>euN9_gF3ZlpE)J>n0dTwyaH=e5kKbGEOm7?kuvPQ;Kc$o?bWiEo-?Q$6HQxm3w*IQwS=m43_!3Ba&XIrbcX{OJ@5|4(Wq=%-o}MnSZPMKp%TLbwztZmZ z8gP)#o$GkRtE|CJ%l+Qdnr)s)W-`SNo^JYkXm0j}gZ5=qdzIpS8=TO+D z@JJ+0q0rC=d8v9TlEdRtK`ER(EpO0(Npkzm$-nq4_c4B$*If4qwEVW1zp6>*K4-$^ zaMJ_Vx66H*(p>js!F2gAhnnjiEOM7h_c}9^vC&Z_U9))HvV;c&X>^K4^7YI z?gez;m$`n?f8M|D*If0-pMRSXFgI?WYu|p>ng!>M7)$IwAN(|8`K>P@t7>e7CA@br z)yxpca|scVcVEX_^KwD?iH%L0D|f#7(t7G|`O}EF-q}TKJ_Om9E!W>8z-j+(zTB6N zD?hh4*F9Zu+-v6a)l1f=%Y4yX**x9SR^d4J)n^~?Yu#=7bkHL_$`;{!shS(fA0GTW z+Waq8UFpwa8Tn;b#Tzu`b$=~v{^#@R!K^>IALZxi9b;ct&Zeb5`#HzMdr1!>KZc$x zZe_ca?{(W);LlE#dCi{s6>|H7`_6Bc`tjCI(WvBT$ZskSsD(3%6wGR%ik{8lD#jYHi`Sr!BA2Y5#Sl<4nJmZ7m zefA&k*Uvg1buB$(ix30>gzMW0}MdpI=U+cd$^Vun;m^l1QsCqr+ zu-S(LwToolON&2lj=Qe@;&WE>qIthc_M6XH9lyW!MB0PrAA_bDu6)GWd+ffmTJsNe z&SmUcaSA;Sf9xfhKRMQI>zO5fW%l0Q2>zJMuI&L+>!-0D@XmW~C{i=?bL!J;8Q@85@# zHyU*}|0N##*!ZxR&5djAbzP@7Opgy2c|EU~(|YXI|Gl**0t}93pYGoC!2I8zmp3jd z>^n0}ys}XsyMS-Pdfo2IgE9YhI>@w!|NFpp;QR`ntOo}hubr#E$*-j#kP@2LyuN>K zUoNAvc%b*~pdT|sKeis&>C-Jd??S|h3g!;=jIUq6JzZh-p>ei^?O(kq{BFB0y}B6s z(a`&+<7-=Ow_nyKbEb2c{4u%Q$ z&ZL_DIhQQu6u)fyAGdUUW3|-zgJ<4{m9#PG*Kd^xWj}x7O-9kh-)eCRKk`^hL%&Jh zDT*_=d+St9d}_VCi9ya!XK9A>*WHB;%zN~+`&;F-kMHcCHo5!3)TcMP^ruKRE>zw! zVb1|ho|9Y7S_O{PS2Ti-PKh0iphX1>`+ma|PrI_#Y_;sx| zEjiF}Xz!!@w=)`)3s&!B^saFc-F%z*jleQtiT?^(x*sE6Gc0sEcw+5$nZtYg(!V)- zXYsk2WXIiGdZ~Ir({I&}&Blgj8Ru_R2!68h*V#_-C*r1TA9bAXurB)hcB8;QzV(4} zKXk10E%tBS^WL<-i{&nsa(swg_I#UGeFQVZX%{7df=9L@HVa!jCQCJP z+))+K{}3vDt8(g;;}uQcXCHTt`4F)AxXV6)nr*CDr zWFOJh+q7)n%un#bNz>joX~}-)!<85R%(AEyxLaxX=uYQkED4p7 z7f|{qc{Iaq`2~ff2lwJHS;>8@JW?Ap?Rd%bBZ0p-lKS?<1ZUa`unVz?X(e{4J}OuF z#$>zZQnbK{m!>kd3x(ViG#_6+wAY1K=?d$M88<)8xDe3ocICdp>rd^b3<4s@PuNIo zxp28>#y%dGnuqS4Rg2%n1iH@1P8KPyI}%>vyeRedt$M7ZE@uxOLiImn%D_*;2!Nk)Cinl+%wX*cs{-~+;OBfq`IVB>x5-z8Ugid2P zTyf?)^Xe(t8*aAx-q+IcP1(X@y;0Hq&jXX9{ec-QF1>O&ZArBc6&p86USF!SJoLXi z6Bci9AIOL;%vo%(O2qK4|FfD4sa5I*E;*+&LnRL6)MU7x*JRWhw^{*FO)UNMrDNhhKHld~*B`aiTbU8UJNw*? z&w@v#5|{R$wb$Oob?nE6u4O7!+U6X41KkW=a&0zw@x@Koyy90IR#ULwKeI>tjn0V! z`B|UyMbGKJKCmEf)+I@O4xy9;%@w-Ji&qr3%rJV}n%fuN^F~F~bkhB$r|#cj=&1U? zxa@G3R?MFd^Yq`i9eW|0biK;K; z^m#gltHrJK166F>uD$*FV~*$Im$x=u>#!5)&s}7x27NBG%vp{Ze3V) z?DbJgvy~ZJQmXQ1d~WR%Km2U&RpvuKuQjg`|0Z*6YwG8Bg1g=BmreL~XjxF$Mv=Kr zcN&k!um1FMYhA*}$OeA@q~detOHH=8ZV9b>ux1nU{pQmS$=PR{ zwKLAv^}Eot)3?6pc3DNlP5F}}_+MUEcXrhx>DEt@m%a3h_eIRP(GfU1V-|~d@>=mQ z`-CZnp50acw)>`FM9CkUu9b87Hm;obKj`V@?7s&dZDx>{`x5(&!_~jQb4f<+XX)Pc zyK|XXqt9Jj{Jq;>BQB_IsA!z>^V^E2RjFOmWRf#(EL-2nD*kUOXu_%ALcZsk$8V3o=I8gCw}^Ly zmaSNkb1%}B_i!fXmQPIzuE#nqCTd7+{U=r&cTUwxY3Zru*LU6V3v1$U%@^;MzcuTo z7EiyiU209D+&$(-HM9G=U0VM696U*DuG#Vn-TwO8`;<6SqeS#G-K{TNm(>`!g&Hgb zwG~eJ_e;EdUvTiV5O3-Ju%8{vzIFZl;I}nqy-&xjG@aE_QmuRaB!VK9R3F(;sPg>=i@MwO=rxe>g} z4^3Z|pxI(-?35Gt+rC#{tjGB5ex8sk8Z)nbl#Un3X6^sRmY6AjuJvlYM!XIK_x`nK z*N1;m3bvEb73FzW`*Fiz8|}6a0+Dsw=RErtWpd)U0i^6c+2W(|hC@@?E|jsN9q(ZdxUyddy@>-kuFyg1uI=^TRG~zR|~!u;nWA#sIVX zx>XT1Z#us2Y>^P`*Ixlno_=9UA183j9lLghBk=^Mvv%116*aET`iqyVDu{7fn>I~Z zADQ$2wdReNK|%XAm>gZ4?yD!f_rw%_w$%v+$0b-r&nB$n54`el&T}^TwTHNal{QG8 zm=yODl3EMmw*2#`v0GiU;kBss-J5@^t`_Zet2*^onJZ2`waWkEr90u?GwT8u9~bN@ zQ?L2vcf&#akiwR;tQF{xYb^$n~1nLPW!krh7-IFzGqEWG%5F*vBdUOf&A>WPol zta=0|KGxKXzxa&r`G>C!KYSeRPhDTbe*I)vLT1<5|2jeYZe(!YlL(W!u30rvQ=TXK z;DYVvmzibV5vpGuTx%zB&E0M5sr?<#!XV88GZ0cFJtO`Nd)N)mfIir|6q4uhomcv8M53=&9>#UN{}?>JmI? z8LAo2b4FhDcxhK$3a?!W>*vYOHp>;r`Uswi@|2gns#RaUwDr?vWw&3}b3X2=NGhIY zmDXDL^7U3VA4a2Pa+$6ipFSV>WIDTSY0L4oPW?BNBM;~8artfTGxZDG#+5$#{!G_8 z?rbu=r1R!!&|$yG`OX(t%{mTG12uJtDGi>Rr>t*3l78ivpSH_4qdghtGajon^oE|h z#}TkkV!hz=9ex+P*Bm?^bMQ~w-vs5=nvE^}J&UrLA1qAmpSc856{K6~`FWO)4uYgZ`j3rxFaWbJ$u`WJ-o9eZg%PI;$(cL z_|~v{%6vJ)P4^$lJXaOcDxA(HZ@%Qr`f%Or+1)oXR64XinXfL9ViCW!rFD{Ab?esO zJ*lg%e*APT^pq*6_I7flkgW>-VJj=!=0$(b$k( zOb@jg?rAPvxq0=$>slwW78GnWp0~Cp@@m|9&I`^*HYruVNz;t)S^Ct*;fqrChlh9X ztk|=$pd(M>4F!{0P$``he7p12g zY}?a+VAa#A+&~cx*`qf$wB-b|&iryidC&dUjZ$vqdlp*gjm@$r6)6@B@(I>qi zBjR=Isp+C^KNmX)o;_T2Mb!1!m-$kDeuDia}(A+`ydiLsw)K0I%yZLV1 zQ~Ixz8Wk=p7v1{6Y@3L*$mKOhxTTB)BlgL#S)DRun8+H%^k(DvJqDYuf0FKR4~$6P z^4u!xVy)2U2+O6RPcLVKLV9-4$0*I-xv{+a+;Ytxc77~+dRq9|#T#L#cz9%!=l))H ztl`tqyo^2EyDioQgG$(}2R5ebUxy#6>^k>x^^;7v$9j_DgooWxtfPugHTG{4I?}>Q-&-oO) z)H1n;&n!H*#;ljLt+Cm4V4v<(U-d;!me02~Kh2Ncq3ZXPKXgyR{uvh&7HfQu(bVsm zmMwYka89l5fqW}{Q@L7ML)Yj}%sz|P^##J6Ic>5<49Jvh9@xY&Z#e$!3y7#%Na)?6D zbq)CnPp-v3f308fwCb$JEo0aHvso5Tkz4ulG++N-UIr7NLldvE>F?N{f3oZY&k>Hs zv-dwAS`m9%fA`!gPZcBWjostj=LskuTeg=`O8e-$$*XE!1%~WmnjI8q-I;z-_S?}D zCJYKTJU7lYt$mQDS686@^!2-+4(;nMvbuv3g2du$<&Pg8_|eBE|K``q(0@00bz%)c z)$E4%4qG4YNh$3wn75?HWvXUdS1!1nwfWa|&1NCl?89x__IsZCv%<>en+Zd}%aHgr z*Iuyh%`*FS`BZJtv(tMFZfx!G`dh0tmp!cP=HqjPvY87%ip(`Px$th%CyQFW74CsA zxpKT%tve?slnAGPx~k>2jnUxst{)fQuYFLHq%1W1z0y-{uB4jn7RSpVZNMw<#m*l6 z=dip#>S_PO10_%SLC&nWuNd;Y@@3!_t!LR&`RDi_o9{6*zdvf-^)&|V?;E>B<|h|x z@$+Qf-s4nS@g|euOoA(S6Lao7-MA0Yn>0_zZTQoh_jBzYgFV?h#XNdes(<@?zEJk8 z)8Y+bKlL5!!{CAWaPE;=-`E=TV zkS)0{MKtrzvCEz55nLVqqBgB2<><8D+Drc)vMlS*kqa%YIAUWok1@*M{sn7Mjjv_Z zOWmoaVLR6(#de6B%v=1M{ef@CjV)}lGb-a`&kKF|u#wkxcF;^sDV0;)&iCc{XPnp3 z{k`PEsi||Ohq->|*7~0vB)55z*8D1wsp}3b3)$l~he>-?EKjIZ&EK9WPy2+N!*v$7 z%w{;;nX|s&SZUx-i+zh8E-BG>d8sV406g}k(3ZEtFirkN;wD=IhG&I$1E=?Z>bFbJ zH$B|bP_obKyGPI6<%wY;KfdH>p3W52vgvxPtSt8Q`~mL99J$ckFA|&=pH*y5JCdbc zah$tt-QxT2-d?hM)$v*Qs_YM4TYa9G;}!cBDIRY4es!~nz}lF(pD(Ra_m?bqv0?u- zh^rlbIDZg7qPbzw3-=tSpGo4^{{~I7cDg82wMFm($HT^^x!Z5#FFa_u&)e^~)xJgI zZy(J0#HYUATDaBUvpvHZ+`NMJ#)*NsaTyj43aOKRGoCj7P4ba)$ zW%Zm{wwL3JQEsKFtN!Yl1vM$5s~yq{bZ;ilHaU>&>p$hk%*{D$&npst_c6?=j}G(Q zaeqU~%#AVZr?{D`JzZoKD~{jOy~ZpIcD~j&f0m4W?4dueEM8Hi^i{Cir))3Ai`O znzY4)p)T|h`&l3Vqro1aR&TAI^to=e&t_X!9ONw6t8436WVgun{6Xy{I@4EX%FX$4 zf}{ApZoU&i|7grpzKczYM)1|}t-jR8}TZ&Ad zFAbf+m006;3N(TZD&d+Ax0~m_5>_sMvT0G;RQ+#83j~Xs=M{W3oVTJT)@eq$+l3Go zxjmb8t8KK_OL0iltol*SlAtH}xj68BLMwPQs8K?fw;}98^9_^#;G$&7`PAc;KbvHp zzulWD5b{D|-v8OErq5L${l2hjZIM|^k<*HF_`En)?W2gSf zAEntN^>eag-n`5Be2(ifBTvryHG5NbhM)QrbtNc1wdH^vTasB$l~$&IoWBy=X6SCOWfW1^IJ#n1z-L9L2*&_)h0ZKml98BUgiDxvR1aB&@zP6MAi0E@A?KE}BgBQ$=T29*`tSbQAh_qz&p=Cg4jJi_%iqYSY1tWd zor?@s5WnouuiN#C@h)SMwnD?%&EXQYDKF9sP0rgld;GrgZf;-1<1gN)yxlH}boolJ z+FH|fgwdwNxUDxUlOf?HhqCa2i&|nY%-$|tUlSX3vEbi&(;tVTZmoSF^I*>QKY2YR zOGP^_US>GpA>K4s$zV^8t%)bhS=^tF6nwS6$#0c$^Z4A%BVW6^7`<(x*Ii%hRMW)1 zj6LVp6t=1y!KxEHdkt25y_V9Pr7PebKykBlCy2iWh#w*E?NoMEcwz8#c)(r9y6s=yCBjCnp`_HBF z_EJx5a>9Gnv~86Moaw!8kJ)PW2JW`5CXBKDZyS&u&Lh@>f^&xQ* z4^OvP9jTc4>66&+hn!c^&Gzg5n)Bn%`aLNv&rb+#?rbY;-tco`w`KfVr_}a1u`<;o)`vcRIA&U~{mAS4Hv@jwYlC{c zIf9F~GB!N0TffPP^;3se`oUj`)jk^VsxNRJhfft~w(j1k`D zX2|UH^-B7?7!QQ;EO5;Dd`Ib<&yMiQoyu9d^NYL$_Re^G{pd-MU(RG~S2?10qo(?+ z+{ef;CyjUst~4L-&*v7Hcl58T&{zCs*Q~E0-?O6WWY@(@3=Y}%R{u$N72L&eu;`nF z7To18ZK}6?tVy~0YtBnSElsf&vA5dyb)HUK+k1c4-tA!~Pk;G5ZLNJi@9S5e4)dft zi}x(}c)Y{PNV0Zwf!yLfcLcXi^AGI2r#>e{d%xpM>B+GLH#$#0yS+Gr{eX0l*YXCfK! zu_k3|<++rjP5%vI(=V5KR;C=xn=kTwho5bzMSpOdU9!DRW2VCF^z{OdkN${z%9`|* zndxoLKHDVe*hH^_ZAtw*XGEP|zQU!Plil`-(q-OLcV=+7qMH zvA4d=|50y;S?%;G?q;9Z7F}G^b5BcV@0WMi-?VpKk^1Iv@!jHIQ!fdWt@-NVe_e3n z^_}}aFSyxd`z$F-cYc!4rWIZV+btx^wThG7MERc`%eg11_}=|U`qs|&kF7h+*_d9q z-JK%v?4@Lt#hGBM6)FK&1giIZIw%o&(7?@%!Jy|gYsFa;R#q*0+oUaZ?XJw{PpjqH z$VaBmGnU-Aw2NcI*38{YZIr$>Hh%qmbff3P%A?C9mo->DRgIe0yTE34d6>kmbRTP8 zEq#G73&VLzssAJukF^@wM)ywdUcI%Z%TwZP`GQ31=k6Dv~RVv)tzo)zE#J_Q}Tw1s%CdBXOecj^@k$W=58aJ;l zVKsB^yw0$=e7EVPxvX1!1>yq_@H(u%A{Tu5y`TH92dzr0zx1uouupHecVJqAf{DYT z=}Qu7_dVy{WStosKL0C6ac_?_d!{_t-6hNnU1290*q7R*O$7B~zgcI<_ms@Dy;Whi z$;zGi!RBB#)e8M4-`$?al%qefZ8+}!ezTg?*&{*$=eei-G2HMrFaOL9=0gWR*RML$ zE6#CwUC+Jp<1Uv4D#{N{IrnkW#OTQMlvBsUPKxE6+PXyhKzm=-=lN$H1H5j`Ts`f5 zbL0Bm;hNrg9+f@nXM!KPTJW%Z5?$-|NIOf1F=mVHY_WxxI-_oK>_{{}Y#gqcfBAD& z`WKrGcRC*4odR` zhfDK1_P1hQ>{DInI>+bfjae!BDKieTHvCjKd(zDPZI(>5#hbHrv-nT^;;!ss z3T0?ZyD;lhQ{aW<^{(a3*+FvBKb`nDu*GfqEM<8vT(po0h$3vQU-f7tU}b!qQ&<<%2;8&(MZ zU=Lb$U*~wT$$z`%blrD_+j#iSACwj{S39uEsAx|@-CnjZo(0?ALpf1h~k9AmJlJ8rwJ*rCdUacqIdxz{w=bWWdj=u{NAwP+U0xNnEbmX66U zJ%3z`Dr21OnA*rC@qg`KfoZ8H_8z?6K7H}41x9N^lcb~$#(zAj6ezjl!~@=>i$~-v z*85MnA^ylDOF^Ee_wIzp-V;9T-FTAwU+=Vv)9>3)q?~6@PGLQ4kovLd0TDFeg)h0=J8P9*k<2yfO za9#Z*o@B*&gxf6nltJ}Lo6jGlUWn;mxIAs~s|PhH=ee5UkG6ML0ZOUd{ziN+i@8Sf^U#ou_UziQ34CU%z3 z;F9OuO@@XGKe`U|o|OwLpTZ^|ea?IHlz#7ydy^acKRCW>m}ouwxB#nM{)wLlynDJr zeHz8L`PH$!>zmssuF;iwgUM}LZltNc&(`cS%a#W{-TeJ%Lw&SR)RB`{1ewmceReE- zmMXN*V2K@5Pf-Z7v~;balSNeI1#UNqJ8^CgxSf-pXYBdgHzgI4O&{OkWmr*mcdglq ztPjP4QZ+fY`o5sB%3)^s-Ia3UO!0D8n{$V1?5nOQJNh)}EIHF*d z=5_nQb0w1>5`}TApw7FTsJr0NUbWOpXIGo7hc{cjs$c)@{h`g0dHc`ixDDYRX&!f( zgZCu1uQ|xwGO?LGMf$~?Gx^g>uD&>`wPwvyAOEGNj{n@>$-^TrJarv|yjp9VUeil~ z&U;Kz0WTN)h-n8o89zQE%o3|rz^JK%rZV* zf59!z{_J&DV~$+KjfYFs6P-POCiJXHkeHsu@Zg~kYjl%l*y4ldT{AuyKH3wqv?j*s zCfkAa>w*d$Eg+QQ`|}(aLPro=h7Z@w{8k3NKl;CtkRi{k18<@krF1!`yB& z4Sg?KNp;<`+#~YQX5OT@wAQJmZhk*I5*?TGpU?4OU4KdR2G3+C)mEnZh`y;}i=+cD zCdA3t=l^ot%J$=sft*r^;D*MhT_2i%*6um7e1?D4RfhN<7cZW2J0NTGFy^8Aq2Avq z4+D2Ew5nM+r<%WNx{S2*FEv+Y7G7(g^LNxcx2`+|iHi_s1}Bq(BO)R@9)oVYTIXcHqifx5XV0_{&g}=Y^J}~W^TIyzv26%5Mva0f& zO_1Ppc9-xvZot`AbjjH)`pN@ip*=~TWqlYEE=$$$v;0b6?n=C$y!d#Wp`Inrx}z8B zs#+f!@=Pw}pXw-GlaL|c+%h9w3)ZKV1M#i8=HhKq2Pv>)pgDM{@a(ZJU_h5+xzDou4&TA z*@=JBBL04Dew4q)ZRC&+0*fo>BwBsrzu-5Ue;LG(tq^G$qvyxg*Qy1 zjSKc&?elmZ^k6clxn*7*%d{&8?%aE})J9g$z2jp>Hp2tQ1V@p5zTu&l*tIm?@U)$E ziaOxT{W5r?Ortu>*|@G*)58`ve~`<(-JF!d%)ncvv;Ru*_nz~oJ}x!#?QLAr?~&nR zbSGBDILPk&QC-VCv9R97R+Ghb`kf2nUj3U@s^%XFbpDF>>EM5TnZM^x8}o{NdeIHlyBel{_uRx>#ptga z!zth0IeB8_jf#cryAls`SlZ3pX1C?;+;34+K3D|xxCb*U+%gijEuL)`(#-!`?C4sB zL)WKWeD(5eFyHir&ssTc|J^F`IChQgzzQ~@ZL^ko?~|(8qE*ExInUOt*6Y;sr3WS@ zm1jL@io2)ubK)H9lT$xzoM9o^ctdRYjQ*dxmiKbr3)pShertZ+k{CLZJs5OdB3}JvLMTX2!r|IbB;_dcwA-uE|)LmbxE61Pbq6 zigtItDtF$-F-P3gDrRqJ7_+ityw&BK^n zg1s2lU&{SBQMByn>WwRfm)00ny|2s(3ag%LowVZIQ`YZy{|3qPvfN-eCs)tse>cc( zQ__;!^%EYiuF!oqyMM|Co>QM1ro6OKY?9Ws{aqe@YWec(T+iiI;^wtJRqeUT8XJ7{ z@y;U^CZ-G@9(-%sW2ZV*Zi)JF_NJLi0S`rf3M7WFUva#NJ!0eK8ILo?l>hFH7Oj}s zx;9N&ZrbBb(bX|gec~o+i@X?$-yHo}`sLYAhC{VNzh4AuZ+m!2=jr5)U+1~oc(Y84 z_RBo~g!TNV_`?T9-%a{-qqx$``{#+SiQo1*{aNU?e*5Cq$vFq(PMggYt@1YB-ts3R z>eO=ekWW3+W@}2FTBBD!GxTGH9ynD-ex5c{;9BwR^RrWG7AU1U&smYNVc-4}iHU>-YI#N3{F}j>r}VFp$h@z8L_Vvh>YRYw zJh3Oqmm_QIJZoofxVo+;%Jp@Ypi0W+>O%qciz}rRK2-P_%JuR#C>q(l6Xtuiviq@g zsc%vkzj{&Ly1Rb+k}ew*`X1qaV)`S!;C^`XhD}Cs25%1@SY~?kBX3t&q{6N{pD!-< zIq)cbiOoLGQ|ifAZ2K+Kk9}>{d&v5}y3_4;*NoZR6JyPR3? zwY}RX>x$S*~{pVQnTS+y%oomi_xvkaaSfynB}plHN^DH~bE3i~n!XJf9w^T3loRzX?L)Wiq7RS{@a?dqCNPMlV?Gsy>5X!Rc=GPtJr)pLO*-0wD zul!c^IhsB8lDEm!&z^yq`Y$x6poPs@9sLXe^9J8=kDpr(O;T#}J$>X^)Ir;zW#yt3 zHp?ZdnHxNQc2rF{S~xe!TjJw~m`f9jn`O;xXT6&lC#WCSBk+T@@RNk_Ro?_e}h5BehX3Smr?M-VMh%BI45P68%F? zxrg;^c`th0hO4Vtrt*f#gzX{x|Mi$mZ8YbH1qK8K-xK8~uGIhNMyom+B#?WdCxmFJVsGaXyHcSE+wpT0SlVs8jfg#=CO59!o3 zW@QCWSRW)_=AF%Vh&xDU{UoEg+MozaN*382Ea=K;b=Exb!N)mCZ+ID$o-$uw#je%- zXougz_A?QeCKex7y0>xKU9fYwr*MC7l2?4%`qk{_XSLPGxt|xFv&E4qHThBgm z*7jgKTSJ2**_X2(F1x?0{lKEGDcjA6K^Ozojh@0Jk!S4pBJ;o-r_!TPI<ePsM~#W~%lt!o)>Y$@L-_-*g^AHl_bQLX9y?ce`(wTElwHLv8HJq;XI;z~K& zPtUklSLvu9m2)NM)`E9@m0C+`QjU83jw}47%pGy)#etZO$rksebs1JK^;NH&`gz(; zUH>(POa}X(ElqDUXPEcaFf`O=o_EhQF7w)pCLI?af!47H*qzfgtKr*FlIi)n;?tw- zv>Z!;N2Lb$d$ND$S+cEPG<~MrGV{0_95biu|NG`#BqQq(@@>y?rgMyOY%PM8XQaQg zTr=9hTTvLZe1m-Far@cDmsUw1Kg``VSK+Be(W{A44>K&{U;b|G=*?-+-0`s~Zp)1> z?t5o_sJm}4CojI$=-=AYu;*0Q{P13o~`j?*B?Fnj? zxOunE-j^YDB;(^{=U>(u^3TPjcJ*9cSJ}6GcS#a!+scoYO84JSef(9*G>x&!zG7x+X^qjP@7+RDb8qbFXgv4W$fzVu-^U`n=lFd^ z|5j)QKhrMptbYEMM(#uBemmb|e)BkJS+>x}%vN7(iJa5dXP(tJ%=}^z)wB8z!wb-= zVY6Qup(gHDZ}pZxtvIA+_?&5~z_jyG=dW?sRNc`(e(J%qU+3>0;FtO^ar*gHU!>0b z6gytZb$wFzr;A$OuG!dx=iZ9sUr{n^{$!)I+TdLDBx2>_g~uNmFt}xe>GxYto<3`z zr^Wf5&hxmS71s@Z|6SgFj%F`To|zq~@JZNQ_SvT|7j}tO-4%P1e%W;8qvEi)p$p}H z#9jQJ5w`gFMNpx*sMjv)Xw{k@yM13*?#j1(RI}r`U**Sd?-*u0P7nOK_rX5hs$HkN zw}iWYl(jP5#Q5RE%K+|edfPM8H%6#gKXqDKV`RXU<$it5hizedn4g+H(&yj4z^|ju zx~=+eu87pK9&xjZEK7UasGlLAw%qxrT)!W>Rlbheo3JNMwdS7k+doS~+MJEODrCTw z#;q5NUGv5Gnx5oToevf{V=_sp!>{t7HT$Loo17*{UrR|oth7g7j<<6f8*|k}iP|rF z{@l=dI_uejLtPoE%WJl1a6NEgO}(7Gk&mZpn^~^n)6YQ(rl((-&%XLVwf4PP*T*h4 z|K~|h13}F-yXo3*+NGbaU7pM3kmU92NSceO+c{Z3FVM=ydkhMz>M#Rd60eo~vH>cZo0gq@W0c+K}%`{CJ~oX~@3vN~ca z57xcoVNtlXX;YG8h43l&?g;fK%cf+m-4h!8KooSySIsEnBdtGUygZnN4k) zTFLYt{ z?1ZYd7W*7?yl>51S`-v_!#gLfAYVT}%A4!7xa9n4m$<)7E8Qge-8<54^`7lhvi_a^ z(Q#y&AJdu*n_jM6^s@O?amdd-A{jQje(y5D;jpK=i!0|&d3h=8!^vwOj5cT;h~9f) zEnEL{fyGag{Nqh{lWcBDGw^e&cm0`nW|oKP3-ZM+dNOx#oz5&-NDlCHVG-yj+v_#es7m}tE%kgE%0Ec&01)^m1zIw!rvgH zx16?4jpw5l2%9N?Y7$D1iqAT9?-g4Xv`KdB#bK_!Mr_Lt&#HRcZ0p+fO}*lS!rRuy zu50gp-CUfpaWUZ;%?6jx}$-QsiV_SaL@4_&u+oX^-PI%B$AoN@j3JIa1D z-QL{2ep-5W$MNEBP!rnh*M(1JyD!;(Z1n=RtUo-K%5n(?J??)32W zlXouo_%*wTC**J!#~*Qhvn<}6D}U@hCYJvFrl@@>|EsWEeWU3wv$v3jhD_p9PZp+) z`cIFZs{VQB%s#;hpePE83(^0UvhC%9-A{JxypaC%n`__yq|J(3TztjczjLvq3Y@bvG32DoCschgU$Yd^b~QoUvt>I z-kr|pKF7VyM2^!;FZsRkkEI9N#cnT>Ty2(fYu1hLhwt%#9cB1xdv#;Mvd}Q`FN=5(~-syE`c!p)ZU}kSccXfl;h7hed!~ST7#WNCP3#uOzTMS! z+`Q`4so9%m{}g=sRPgu1&iPY~&d0eHZ>u^nO;gG-gYk09=RH=pe}1Tzu6dxEKD|jV zG0aZ#egs|kydG51d%M1o`guY215;wN691pS zesko*p&guWhu!|RPEj}%wmdO#vHs-SyJsfa{?ooLum9(m<1O{*bZ&ta>9$o)4gp6l zr_8l`WwvOpB z1)t%j>pa_Bj!oNHp{9-F5I(ph}`za z6Wo3ZITE+=-%;xas#!Puev8~;gm*aJwDazY_9|$8X|m#h?TtwiJh_!G#4NG{8qOTE zdv7WGxRJfF?k5}5E}O8~)j1Z^qmNwR_cyEh*f8a1uHdsk$tq62eu=x{`~FtDe&=Qv zP2FL$dYPa9-ma>#b znNsO`$k|}l>8sK^7JPltc$LR{-l=4f#R;`JfhP68YM!>|{`)jxN@bqlHmg({&{~|u zzYlG%=l@dlaDiaN-9=lM@%cx$v>82apDNxXV3;#+ZZ`cwGiCt<9@8U5vP97OZzRT$E0;gWNmw0JyY_M=@}wvm7ia? z+6KZ~dD@5eJrrA?^jYbD_|#>}`ODYW2ir+j@Hd`a?HZTt_Sx=tyYP?STKZ27cS%e) zpUWGtW$G()O?#bvOk4Jau5-vCqj%bA`E^T+W&Tj(mW_rxZa|7U~b7PCKR4w`mf zJA_N_#mp@G8Tu&;j%R*;E81=PSm7w*8TV6+TY%O zms7yWVeauypBD5cP5eIN)#kW!syR;2Hkw>xmf!Ku=0lpt>}}z1bSGDct-qu-zvs(e zA&sv472jF+R;*tz|KPOq-nY1`xZuuIaXOl2zT2VojM;^c8!~M~HRk6?U;ger(|`4c zy++Y3NfGCsC7yrE9$Lk#_1u>+S>5&X+xIpnekFPvy8AM_9RAO7Ze!i1q>}g_-`;!8 zxcuF1ecA@@rM794rz~G_{Q15z)0G-|EK~Q|+OKhBGH}WLd|qQ$+s9u?ub;|PP5hB? zzh5=>LG#)vFJvcvIg(_U*X6kyTtq48AIo-ouYOSCp{i6a>*~-<@lPSpK7H%o2^`U1 zquxz6x~s!K`&e+}r;}Cs(bXO=E^fZa$}6Ri$C`I8bxWvWn{D^w$w|S&2~nwufA(Bi zaxo#lbauU)g?x^r$RqV-CCB{3dfsv0|Myp=X8L~7bN$|xXVN77=Znq{U3x|Hz1%!U z|5H_sH}ZFH{rDnR(XRNd!uiu4cU5Kfm}Gi-*SIkDTwkKIKKvrby;YyV-6w9rWi^YR zKfNAvfcsm?M%f_WjmmFWZA?8u&Fyb{57oUm@!IlA)n-U)nC$wRX?LCd>U1yJ@3E{C zET>!O>m&#+cpCNO;);x&r;VPZ{&AQnZ^M&ox%N;8bMi_9S$j#tUHUb@zfZPPn{#zl z|3ssC^YrKF@=jQBytn=5mp1$7e;%xz*TcRo>cM4llMz%GJA34wRCqkF<#M$X zjXi#lv0wC^dewx-+c%xIh;BJMU1Bx+!pG{6+Ci^S=foDdKQTo%8{^DhWWGqsYII%Q zoXVYioLgjXkhJqbvsx9CMT>uQrLJnOxT(eR&uuFEJj2z=9sJ+c9Pn<2Zly-ukN?+xt&eKF&Rr&3k6IjhWUi zxoLqB-~!w4(1icdOZd!ut=q+~a7VoA&E9(KZjhXP=7!~Y@FGxJg!k|HPt#gOC$4sj z)Bl=#Z25JM3k#C8+Ul7SRg_;9W)?8+vMn#to4Bp)V$uoW_1C{l^GJ$RsfxJh-Q%!P zTJ~czTa1Y)g8_T*Meh7&=`-$+90k5UII#MNjQ5`{T^DN~uHJuFc~11BCx%gd>KeDI zBp%E<`u+Idg#M#j8xn)QiFxx@9Wm9vz`UKm&{3KByZJi9*Q}z4kLRnG9<1FIbxW)w zQ*_URd%u=kIoM|R?T2R{zln&%(T~h6#U~<~45#|DZF{%1nzzEqnfE#G^Om)fHtY$U z@|-trp5F1dw=Qb9uL*uzxis= z4*vQUwb-Tk{pn{4w$pmew@jJ;C@Du^@$W;^7q3@WoXlX;(PiUyx?)!A>n&d&au?_( z&)9#>;`cc6# z(;{KXGK;beH>|mrl`qto|D4IT?#qtV+HL2au*w?e2!*b>l5ugSP3q6CYzBu2rx}bd zr$3giS;V^M>%)$PXS0Kzz4i4y^Pp{p-Z8fRu!zH_w|(BapuJa=Z_DEXYwiCZF)lQcQzug7dyyy2Rkn{F_m6{l&E>eJd;DTYl@sip<5!18ZE(7!+7CH_ZK$7QiTaP=<5<3(4fOIoVG% zo?Wl*tFLl&cbnP`ZeE});9ewYoJ}$MX zc2riZy=zi6Q<`DLjKceiFC8j=a<)ik_v-B{wr>8s;#k~=bg^e9g0;PJ23otSpX?}` z#>Zav>&PZ6mj{aJjPUt%ePT^E%;oapa0+Pt$&f{8FuftKZnp0p{6JO zr!TNiRP<=__@LyN$v4?vrP+r=SupUILYL6Y?net;WL_LQ&>_gnX7WOGm#Cy`p^}BT z`^9ClGCNPb5S?7$n!d~WagS7~N#Oc8&r7f8rSHGL_k30O+U4I??fw3H-~D%Wb{`5J z)U5tickl1wRb^LKX(_e{IB|f9LjH}ngE+e!9t2!|yDzvW`}e^BmPZ^C93OT*`E~2C z){hyM(Xu(sRo?5)g}f7Q>U^~EJI8g)r4LeBda@t?+y5!TK5Mz<>ziD?A?&{z`FqwU zJ4(IL>hSnw`}o&)q38VBD!kKc)=o&7RnnXYyJUWN>ooyQ)_tFi=r|Dp4|Q0!5y!E2W9oK;uv91GB^ zJ^abt-=`#1i}i=9mf5kbf7iGg>R-Ga{>?S?gJE;)f?0o8n)v}GtoFML{#9}Rdvlko(s#zriH4zP&MB{s5IiW~fAYu+R=4Vn zdn5nN{LJ|^E%+g`hFtmj3mi#0yeiKNb5E6T3;8YRv_Xsc{j~2KiCaC{!rZ>Tl%8f^ zm9$7ZSo45>;6}rBFWE0=S1n+QxfFjfJe*a2p6%uGh=-3>-qHWCYU}a)tD?;r(ms4O z(`7nb;%dL5By9habuZF|&&gkj{#x+;4U^KDB?*5nCI8Is@ah!opUt_sUxI(F?|jYI zjtuqoF{iic-`%_^Z@EG9O80MptY5#at~hw>^BYm7`xjRv`vmLHb!XYK{-dg1?cpK= zy`8nCWr>5`K_W_aJLQ^xZ6`FK%@P zUnZ9+-ap|l`JCTL^;87ci{`Igh0C}!DqM2hjs;(nv*z-4GLGA1RvVH(L*>_;%iqHs zZ5RDXs#aq#2|IV~n1{``Z!;>d-TM4iFq%nT=k`kL{xut)R-Rp&AHnymvs32q?zOoS z?#oRqJkl(9bVZ0p_1`bw3>*$C#3sJy+UKS9qk~7^BYEq)`hcDB^FCFSx>rjvTrKCi zJ0pXocJ|fqo}4CG{!h*Nt|Du0WWJxOdEH7vJ%69m-1!xKf(MP2m&s20Z2sC_`uPie zCz<|_(Ph*1Dr$b(++?~I)_J`D6>s5!Z2M*XhyJNa7N$Dsi|ybDIC$~i;-(7+Ug$Di zpVHsy9Z-dx*U$2PZ4+j(h4Dhn2JaVcb`n#$Qv2mg-hN+@yi5FBXxKsb zClghVzg%$(6uGCHpJqJ%>9%z_=`|79DyN_2zZ)v&CYc ztn8~@*6-h%**@jR;jT6wlS{Lb-vq4p{`dNgUhIb-o1dQ)lZ&kWvtsLS@yqUt8_XV0 zJ>F%v_d@j11&1S96U(@idGDym#m5$Hwb}Hh>4-M8x|4azcnzFX;+ODk|r(~bm{Gw@JTgV5&gCet^S;*SXD!RJj&U%h-{r%5n z1lKlj#eKV{9I|hJV(qlj+xlS zaIf-+*S74MUT6xn{3gPediMnT;-G~&A2*wwP3ijZ_NCLlw$Kj~FZTGChrIpAKJ~Zg z&EG zCERlIKU@0dZ}|2jE`qPNw>)b0d2a8W=?emdYTY(|Wj?^Z)_4E1xbs<;g-usK{wQ2t zX?!m&_m|+_B$+kq3#Zvt90?F$cDAyElz&ew78OKX?X5A;5h&-`kYA^V+%dau;9c=iX8j ztlyik^O9aS>qg7J3$|OodOm+?&7r9Ew~hTL1?{_jt0vC=hssOI%Tw%%bkiSp|J|w{ zDP`bl_$={I$YZ%L5wiU&IwU)|J72DTWx8jBWvtKqN|(d0`QM&&a(H3t(BtQGf5V@P z!orq)S~9zKP2_i5>LmH~^X=t1nmZa_r1exv#U8k>CR&ryXp!;J{J7Vb7kYcy_bT_C z`g_XbI7{~;(SU=eCw^aXJ@n_q=swo4-~01*#lPhx$j-lKozpFQIbZPhD{-4t zz1$BSUpb78E_tch?-uZ?1w7jk1#K$WREnE#rv4Zyh-|6uy?|nIw@7vZ$R(X5T z|A+nV&OW7O^d zfB6+XyUatjUgyUr{x!b4UF)+fBSb&wa|c5Is2C>oRe%QP{&oQ>Bn?&FOUOYUMMTkbIeYSQ`VpQlE=h6l1W3_($2>Ii0{g(T_%=ymJ|KFFnhrSa}PB|H(4^9)c z{Z-waHl?Zd96yp3K4fnb{@mLYSD$^EZ{_3n$9p!X&RQLw@4ElF?h=2SO*a}VL)Y+8=3Y*34zQ>^ds^|yXKbLgKo#ec?n;l)M!t6Eu>U2~Ps21ozJ?<>Ru z|9;c{{j&F{pRqnXf4%x~SZh*`?MIah*Cm$!?JscYxs)267Lim3V} zG~Z|MJ&q{DTQ6rOP5CCSQBkwIaMxjO$2X7JO0LIfRHQ&EZ>8fCTbZG`X_ow!hR7!d z2YwWJ`~Mbj(x*uL_u z|8-0I_e;^D_}>wm=g!CvfyB4=zqrkF_@if+xk$V}=h1#vs6QuWW^Zu%=?Tg!c&z>0 zo_*KvGF6)9uS{$Ykrn}<;wft3k;j!A#fzRbZmkR}ZCLMY@<91@TFPwZji-{|#ifS44-b2Bc7e3* zua})+4zBtiE2@wFm~Q^z@PQ_)&2wj@BV4#i#)K#7)~Xv4b{@+QtIBPx@MGI`efFPe z=hcrh1!XL8HTWX<+1dZ63*VI5GwJ?&6{qQhvD=pS6fq=TOx9=P6s`7=Z>>JlmRZF+ z$J4bx@>ko_{&_P_6qY=k@XF0j;wqP_Tz*)-k66w-y*n>o8}R;cSSaN9B){0_^m5go zh7QM0{7rgY^Z2REOV$OV7aFE7zOve3-@j9JRU4%)PhSGhIokhj95QcM_h9FuUr%w#o} z)Q65PtHnWO38L=Drbo^G=ff@2j`pzJ8)aFN%@3!qV$TN&Cmt@Nb8gEos!RTXWOzjJa0T-?hHh zOGNAUvYq3Pn7vO)=f+_V?P*7pyTj5QkKI%=itb-DciW{p2~y-0Z8Qe%tV>4JyUOeTnH97Y@=BEGp2{n7q zGtD^qttbBNDb=JK7dt{37<}bDT^vJpKlWX{E7i-!ZiX@Ixo@onhc?U2WMXjcMT`RlJQT`@ZVgH*yGb#j?47;WqR_(6}{QP56MqR#A6eDkyrBh`2 zyEFIqY?9xmz|HkuvlkxOto3_4{$9*X^q;$F>4j5@A7|9=WZqM4X#d)9*8hJOf3SN` zTW)%<*wFOLf+Oj!IT?&L`F~YgHb*qBW3&DCttfG`=mtL*wq*gwwyb2c)q8nb;`xa~ zji9u+Sd{C_<>&Qz5writU1EzDzqp`(O|-sV@T}Uvc{R_j34Z?4Hk-Ece_)ij{H?9!=9@0J#d7V2 z=K@=u!_7Y(lvo>370;4TYS^`ktv1%D+3v=x@XtTgUjOUQUSsU`Ui?b*wr`c*b;SZ} z8_X(hDz906V8$c$buXSjJ{GynytM9H2c+D7(O-MDcir2Gy+@g2PR}S4{Md2I?$fNT zKLuN)51yL)`TpilZ=SEU<-6UUbT-{}udIaR{Oq@dVOtL-n*}GSt-E=kO}br7LHesj zPa&wr*81^-)!OxloaQl))=$@dv@^&azrBbtUi>PXe0|x!=be>nKFs{VemY*@*onWN zsy^x*wfu3a`{mvY)-4xLi`km+asA7z`!w~>f~JHywKFYY&CHN5-`5mgI zYOCsUv*k;k;~lamAnV3E_N!d1v9UT0>1_4=`VZ1XYfXRlU1O8KS;z7DnM34v`(28U zDmJP9d^7`Iv1t8zE&48hvsvx3 zdb@Ow7krCuW^rb#2+NN`dPFpQmrRm~6Jj(s+IG zB;Q81!_gl7RWirc`Da-y*(o}ei#7O|o!I}$8@B0}N3zCObA8@q_T$7e@#HtNSoKqj zS4mF&E&BJ&uLggS6H4>bFLGtHPwy(&yXopJoAk%~#6LA3lKiZ zL#xC-GxwH{{48UZO@+UW1^eI3I+FLY=f>lyp?fmEoT)#)dtzbOHNpAHuiok!e$HQW zHvF1f|Bn^P=j|RHtSVG`zwKK^Y%j7iTUp}_&3gS^MQd_3&$Ms7=w5a4L~8(A}Mh_|v|f2aGbFYrU~%cqA!)OvW84L*bV zKk^&DeXoGUx5@5n|+_B>^-P&Jo{_R0e73QZD#xJ{kEMk zx2WFMc|I_8Bd_sP*NR=Y9t!Tw>6!kh{!U}t)eZLp`w!pv>dD(~vVBp|!k(t}tq-N; zLqC`X?aaC6Ca*tl>4PPw=g(VevAk%%k^SZ>>yo4IE=O)*(yG|0a`x7H^G}~AZN0AZ z3zmBs>u;#Ef1I-Q<=zjD2P#%?eRoDDivOjQ;!B&S{;PYTbCmP{(!Z_`^NllF z*8G$SeD7VQ#>UN`+3r5&d8EH*e~#JFRiI>eUENgX(w#1^)=jPQTb`u#u6VFI{G#l# zUc0K-nbp%;U794O{+{}C=GEIi&sDQ8JA+-sGGkUtDBt4qH_w^=wMp;nkA$~od4DB~ z9@u`Ol;KAQM7O>st^-{-jx^A)tNh;!{}TYpNgIPkv3188J>cl zr*6Iccym~KasJU2#}nWDTGHk$<-eZczLwikr;`rhS>GO2%+lW?C|VQy$jbEAi)L1F z*Bjo4R@_;CrvLh9?^Dw@>wUGp28O6L38FnEtKF`~PSCkUn7b^`k{(epCF8XVr)Ar=5G0W@V>+>QS3Sd{XYeNo7?> zs;Bejrq#^e`qNtqRHj_xI_F%XEZ^hRD)e)Ot)<@W0=AOxwQ<1*udA*LUMy=DSD0xO z;BeGtv&7WjNB^8jI@qD`*+qLn)AYp^7rE}v5C7C$`eb%-dJ(L+%e-*k`LWMs1L2LO zh74f`yI)F|+N5os6t`8q{`|B}XPNRMlWXJBzNbHyf4Ab-8jj2EtB;&*I(6%%pp(N7 zsr5G>2px?Bm%&j7D?9R@GzYE>uvu~C@7cF|A3WH-=4((8%v~EENARyvmH&JuDde5_ z=f_uXC!|E)`{Dbw>wdrWbwHqeV3GK%tHGUVquL{}5 zvrI|uV#JZnEe~gGcDrM6Y^BJY-Jr&bSF2I;v~;sro`Z6>VY|JIp^^KLxj3bI!}?|K zc9#A4_-gNo^v|=`1~ZE830d?`@rcud(^9Q@I}i11{a6_@J2G-9!>Q97;JxWB2j!<( zo~yWcYfHek*Qy69%6(?9dc5&_YTvy@9J33VQ|%0obnZ`$e!MTKbiVD^Xa5&ZocLDP z$as(30k{6Z8M22RW^VfVTAoYJ&}Oo(`yX*JvpZVC{ZAw`Wz1fKM=4kz9JAZ-ZO7(+ z1)euQ#jo+*zp81ycg%7(>)MpN6Ath%PDu)R7k<7!e7*H~c8iN#FC6sSUnJP?UFpxi z*7yDic(u#>Z^Qg*x3Ha?))zD=+?-(-dR%#R$Au05KK+<~t^WDE`bjy7aSOB`KVS21 z^YR?FLp*iPW>X8*q}RR9T6|o?EQKjZI8*y;(n2qruH+>JHz0!( zA{)%!{Y>zj`B^Ueb4loj($3dcGM4jyYg(_{e{#j0_4DM{#|8B3>twQ)ESTf8=z#W49g~rX{}6{50#MmsD^6D*t6?9-k4vHMwN( zSH^!=Zaogl{cz%lMD2bl%l?k%5s&q!^MpPUl-YeY9F&z8Ym4-pHL%Oety7IZY* zYIt?Q@p%%9--jzd-*^NRP7Tm-DlBQebX!7L0W>pfNW z%B6DZ#WRQFKfe5`!{<3p#{Nht1_AD8EG3Of&(r(g zB{M&;)=9soEN5JA^l$3cf$o}}PVSb31#f*t(DKiJ!{`zS9(5?_v4udR&~cfvD5{NrIlPy=lfV%o{yOQ+(-PADJbW~ysC5D zdr0+ZQ=Hk$rn;)eh?jm16FzTt|Gts&!vTxEM-PT+uUsM8pA*}3;`6mTv9_`EXvvisL>5+{?`&xL%65KMmkd#gr;i$l7ZNW)gfe~ml8q&}JmOR(Ny z8vC2huxwd$QL3*}EcU|n33*SN3s3%TeXvqd??vily*JPQ9`n>TgZQ4|lvoDi4G;C? zxTX0X+H&<5vTTxb?`dp%o6pkQH>3Ql`joRKGb+z^J^pd5I-J37Ptq?}*8Wh#Z+qe= zcetPFa#$mjbmCmveV*hwcb{p4Lt1;v)_3dTH=CVb#QjU^Qg4lcUgds;ZH3;aTKDq2 zK2%o~zpYMqmg~9ma(Az@IpjyEJg&^{-?&P&e=kFre`>OknXp2WoBNsF&xC(92Y;LL zJ!1Ae&dbG;ciqAcCWrRezQ4Yt8*bN9?J4(e82wm&?b*zd#CXu4=HpA>U(1>u09nOV zR~5Tsexl-DP%v+0++g-ty-D`{Bg3$*k2Y?#^E{=sO8 z_K%mWYKcd;ZT$bWmF$yq?@bD``L-h_;v)>v|t}_h}`LKg+nnr)+46ViuFQ*+k^8D8E^S9)*ZNH{1YpAq- z?0V#}m|@xec=6*F(4sRtR3l)0wq&sFxnnQ6_N|IO`|hIA`70LgvGV%Iw!RaeW)=I? zEAG^;Me;c_&qSncx3I4+nJ>+dW4g~{bAewdYkzj*YUS8BdqsKDljeCId7iJgGpg?R zq{lq3e)n8{bI8jkPcZrW@6y7a&Cq-w*2?sC-y8-r{>V$>HL=avaq{}dO#kl5(vG?4 z?HT^1S+zm({OwpPP{J=}X86VXx;eAa&_9y3fA28~kKJ!NuSuC?$=dDqp}rh(?g0&mBzd$Dwh_V3D@7QqRt zLCJ0}7zp->kK0-|wrRbMU(9j&EOQ9^J7`Ugx&Y&P~%pV{h``ekJJ>{n*OBy5znzLv~}u z%NdoSto`SumRsx3TXB7H<1sfMd(9tzru%PNvPSo(^{Tj;UH$7cV;@F^9li0lE>`C8 z8?*R}y)_2wVi!8>Z4ypsoOr5q7u)t7b^?!Y)$LlnIQHi0Re2BES-tmti>N;gN)PuK z8tSeW{oA}XvpwX)55LDH;?l~mdRN6!aZ_}8HL0wkN&blW!>R|nY9`N3^Qf6PH|}KcwxL~75zq9EJPSJntYwOkr zy5*Xj*!hFLpJH|~eu#@!e-`;<&s}M=IQ=be zs%O`P97yw6{dtM`ug^)dZyYI^DR;J0_N-Q#w2jfaiwq9_k)^V~@ zaklD(bW8aj_OhzJn?6%G=JlW7uKxVT#n1NTj?C#ljp9FEtC6{}QtAJCyNe7zYG1aW zg2csgtves`H~%|aev*wLYNBY;KG$=%YaiD0#OaG<*_TiMt`WDzFVXUaez2J@$5daR z>74P4C#KbY<&^_n%C=y)h1sm{O?j6q=5>j4eE)m@u>yOemvnsDM;lRnfqj10r(Iv$ zr2lnq?8yud8Pryvaa&yY@wGZy*SNY80XVbcR@pp@Uwuykf!2bY--W4kg zBR@Q==U!GSfADs1%wk1widnw=#?k%9RHYf#NA%9fpZK{f{zqTq>sE2m?40xk1&&W! z^)3I4wyle-IasrE?yjv8@}`$KBt9IUvi86a|_JfIqC)p01zy0(_ zT=x0vpP3RR9tJOe)80Jyt=k@lADi~FNKEg49`(8T`?9EKy_0@5TFtxg_}1&R`^;LO zQu#Sg{7lICyJ#KX{FFb7uI@RzpH*93jx$MWR<_BJ*k?Olp1$}i^>IizXL9VH?3h)I z4Ggoj4Bsm~*Nxlky()Y|ZPdrAhYSDZd#_V}(7nJqOtj_L!OtgVG#H+qaW8)J+NzsT z-&h#@xVtNIKb+VR!u;TH7`vg=j_WUe$yENIw{?~{$5i{URjV2JHFg;8vwHMAN3P~L z_m{Qa>q-l11#Vb9db;LAn^oNffpulM%rVk;)=yO2xlE8-(0{6LQ(A#8GF`ToOYr$k?%%Oum&E?8 z)!bcvcijbR+Zn>v1#4#Hb-N!^m14;54c{}JV@ZOQ%?wC+gjzhWkvK9=5KPJMJU|K*QJ z>G$qS8zYwJB*{$G*wuY;@iw>5)k6M1YWJ4@W!zUH_;)^Qo05GKYc)5^**zI+`{j1% z&nSFQ&3Cc5>E2$h`+09aFxRoK-hW^>_dPqw|A+lV?OttrGtaT&`v<;trW^vhi_3c3 zW*El@?9Ds2^!3S~l5ZcyKNjL`h~H|M1o40Mie+9K9=u$t9;x=>QE$yxiPZbg0VNcU)u>j=pFvTxcsCY6(=KQpJ> zF;9&?bNEZ^vHAH8>85>ms~$$aZj3u|Rg$gz(Db79iOn}zysI8Px14Urd^jW2eg@+N z)4KEvlMBn%90}HEWBh6OMEv~e7Pbpfhf`i@7TQ&PSthc6>dpA{+I zeyiBvVcnl!n$oFR8M#c7`WhizJl{TSUVm9`Z~LzoYz$Jmi;pDhSlx(rjZ|g3@L<-Z zH5I4!$<5Ypmpyb?hWkVK`+sJcNAJ(Nu&94QtK8+(UmGo(7fVLBnN_67++{cr&dwZc zIQtLlLZ`R2qI$eLp6vR<6~19)Ahyr&Tv@Vta8WJhFP7=|YD0tfy>q%QJ=T z*KD-|pA1^z1-h=spsCDn%N8PQ3 zDa;J|#!)*Z_v)>!nB&S`wOUJhJNu8KS(i#|Qk~^Z8Zz21yz18pF0=Zl<(^xiv}>(A$+x@%Gl8G9EU zZ5F>H-&~l&{NV4jX09dAt>w}$Hr~^!n*DT+ZrH_-BF&cdH~zlOIJ&+`Rbj*9^-3k4 z?gq+=%T7 zTbWrK_JK_iz{;>Y-m!-z#@=B+~ocTCTBppA& zbwD$t>&I%X)eQ4DnZ=4+asd@4lRj5?FB0dt`*r4xS_wg^7RidRWhMu`_3bpcze$I# zPycYeX?on#vKH}g(R;zPIdU6Z;DBIlJCSShs9dw{+V3;=z{FH-3H;m*Ri1e}kEkG1x28=PKIg z{_~R;$nM|zLLpyt<6o`-&H1ALj$C%1mon31_rAFWXS}}X?sAJftaonq++C8L`@1e= zPZ7MFdMa*Tf8^{uum`4lzqWkrli4JG>{i9Qj=G~o|BNaUodx$Nyfxh36}sT&w=ITz zX>6%UHt)g`%NQQdJl-|$LPC0u?EmkF+{2cg3fb+YdTzy&rB8o5-qSiH!1P#lbHCym z+mmr$o1UCw`oCw6{FWo}8*YWq6o1^o#?U5XRC8US)?tn9y+zD>LXUDCO6&h{B)I0o zwOtB=EnBzQJE>$#9BbISQrq%lcxc#zg%A2)8l`%-{%hF!Qq8h|yGqs0En)W=pD(@s zw?DJw_WFG;Bj_I#MPAZ`x( zvNcDO<<*`y2d+$!oVw@BO}i7(u0I6QYVHx`wGcPw(c&|QoNsj>NZXU~cH^c4bI-(w&e$#M zm{RXEUGulo66w_z2*N@Pn8w_)wo!tK=zE`u90b5sNuA#JdSMt4Wa&$>81&980>p&c^SSH)m*x@s{i8NQ3Ln)-1+<$ zPs<-=DNjh8{o*aid2X+_R_^}X+;Ed;4abc?eRE%xzW(q0{3N&Y{Op7%%__eA2eJ%f zVlCx+PE{X*Cf8L-clI!T;C!6&cz3$h0siD{W`?^hZ@SfIpZAm($iA$1>qV|hgqQi_ z(Co{8T=VK)cuwnldfJGC*M{XwWmWLb4KI40OMW+BZ!z^X`y{9vH}3ur@$r+G_0h1H zSua|BcOL#MW*g^n&U+in*R4Obp11QZ{0IhFHN zFKnW_KgW;Hb96;tebJkL!mqb6Zax1szYjKV|2=Ef=Q!_<(b;#hoyScL%f#i5c*k6MdC5h;=kEUP ztD2@~KRT^>ta#d=KIS7IdWDuP&#ATjZ))K3XYDmfEsdDjr-~0FVsH&(!`%*vy9eyo zGygGglV->e++27eE_#<=MzIU!QMK%I#E-qZA02w|wCT=%-iG|rdDmLn4*b@w4_~G5etSgqo11Ruv!Cuv z4Eb~^{WmBdTBZk)cRI)2Xb9R-|P z&1!q+UaR{4@*XniZH;q`c9yX_{`aMCf3g zV8pbomVS<_-#2_dl;E7xzWTva=V>PI^i=G5CxkA#F30ra{@mq)3lDa<9=NejIlJc6 zkFb-_ym0=T-B=&T{v)x+guyQVWa+s*ck+GSyMj}k)CaRw7PWb8 zdU3|5D}NoB^qM8QNkscmWmR}vCn+T^IrZ!5$2uGhrF==djB`JnxDi{y zqQAGiZ0*F|%dVCms7!Qhm{BFsUoB{h*t?o1OJ+py9kt?Ih+ve=L> zW?I)KFs|nAwD2!{-;nc-S)_9H*33Ogt#^7o3tp<3#Te_TOF1sqTXy=(o7k%J-q)5{ zUuNs?x!c{p}2RW^oQ*Ny7V-r6$r z1#?pFEmzn4#a-Di&QEz3VeMb|zd48JN27m%yN__|$2$IfJV{de=cg$)C!-f9mHZ{%Jj1+g>g#s@rhQ z?UCi2pP|NO%QElY^>mrVfBu`rY}N_*dV8(!bk2=B zf@_lHFVD?vc0cy+(BaBXTPx!X1GArI8DC`!;`ZsP6{MWM`LHO?K<8M;?Y?-QbLV&W z2J7~(`em=no;v?~iDxQz$623VIau)ruAD=AR+};iY&ayr_2=`ghv{OAFEJeWvW-XV z%d}2#-VXU-wrb5L=7`gF%Y2{OxX#*gP49|v?v2fh+Pz;1N360~HdD0x!|%hq*LAqv zsxoqSt&nK2nEbV64$P5>o1^14sN82;yN~&tcm52nR3DpfVGC`#elr}|%kXbw;fI>z z$#E{{{Bsnl4YqDSx!XC1%iQkMp5@-FuUmh$JjcHH(>w#M%9opKCv0CO5uVBLAobL~ zjx??0rz_2VeN0+o%8<0=_nD~eB_g#BYkYTS9KF4wiqZD{2D4hN=bD$6Y%Oz~e=q3Z zX1TMS?|)n`|36=|FZ%zZ2d3R0*^H;^YqHq7dEGtKET;$#&%`~~C)G%A`Nh}r`p^m+ z=TO^x%k`G}ZzLICuy!Bc({X5PqT{6f`JVTBt8Q7ue~!3!S^oA)McLMST@`n9f9*c@ zGYCCkO&F?TQx7-R$4URKFs$Njun>D2sT9x+DZfkiI&xg1Br4(;fiv%c%w${Ft6@A;MV0vtd zTFi8DXQ}g98?plRqt7jGn$CZmrEjs;&UH=lK8usTTvL_|oU`}w!7t(OJ8OR|mF~(F zc|3a#zuQw=P@xHFJm}qHc+sod{P2MNdfvK45_{Pgxc|MZooIBC{m88?-B$P9S~q^W zu9I$>arSg&m#CZB9^vLES`$yLD6rW(Z}E*-p%o$(J_0<<&*HH23s#q41`z|?VI8Pw%# z7#oiI8C_T_dN~YI;49tRrtv$9HA7_0+Qb62=bVhS^Ioo2&=BKUk&-lPMZeBU{{%-u_bAj8!TCzlrk~S=}LEv)c01Yn%SzCdrUmKdY$0c zzQ5mv_4n#0o#{9^?|)$JQd`F3F4w<**?xpSO~KG$y`9`{tuO12FvOn^E}MR>vTJvF z)rOvYo8v3y8TRcDNA<-P#t$#vsYuRd{=bLqZ%R@MF9S>AMnkile#!n=L2$y>__!_f zO{?6yd!_R}q`rvc(q6u%sW)@arj2%HcZ8PDP7<7bG5ec(d8IAKudbJi%-9*6V9r12 zm%|fM=X0izTuWuI&xu>aiyRk$+LL=RTq1qPfuebj`Z_e$B>&3!+oB`e(w3AH z!tl&6?(+L1|M_E##qNYk#-C$ixOk)YWe{)0oTE2lD=hT0TP>C)he?0l(e}pn)C?)V zC%ZO0-Jb5Y&tRjz>l@Qg55D`Km;TuFa-r>n`V5b=e)6JwMJ1Mhwd|0$`D|Ojzsj~I zf^Aml7KVE3><4WAH=LKSPb*w!XTSNv;$wDk2fp9l3C#z`Hf1~nwI1}YF`U@Li*Sx9*%M443Vhh&a zG`4B}`tTy}T+@3GxNQY<7WD|@g74+1^=3}{11OA?kLUu8U5$o%1dv4 z{9NHZzp=V)rp!$PzEfM;ugUw($h@}Q!)9YBSNes`U0%}ijB9LMnR&i{db-<&hsV+H zcmZ0o(u0{n_qyC}rvH1o>Z+1{urb7K$%zq9xchsD zUurT(KIT6&@mn)nXd@#B&*43(+sqF7T$?JHF0P|(WL5ut;){)1$KD-M{&=W~QTfL$ zSJ(F5DOGzLMCGF1rHW~leamxuzh@eeaQ)8o9X6)sG#@;xUUSHd6l*|+Moeyo%u<9+T!XJ%;iM}m?_)tau2 zyBGTIN#N~?oAQSvqCUw^Em=xWC2mi^x<6MYW!rbKSDd=_Ib@GR#+9BgMj;;47wY}0 zlw-HxFu5aWEOy)Ga->B}5KgYf}=%kTW<^2%e28mi(OO+4jvyb|__LvtjE1Wwx z|KN-SpXjg;BKt*VPyg(foxFpA_xMlmI^6{s&vdjTi~hY+zL*?pcrWqUU(gbfF7Y*$ zjr!|!6L#LPXkL7M!*6NrxIG3nTg~e3F*}P#Fe}NGUu+OJi4Z)v|Etwmg?U}9d-{G< zu3meb^@~IEvJg#Cn}s$N7r!-cTKID5%%?R5TKn4rPySw(#qed>1sSm)&$m8Wu5?U# zD`P`b%f~reU!LZgw?U!%`J3!JoOO?OG0yXkvhM$N{Cv=s2i^wrj{nY1(wei0wSnQ; zwBuU>Z))X89PW8(xwJIDJh*tfnJAdt9xN_6|2%GrIKlnV5&2Qpg4C`p`%amf++iQ3rv9Uf*);`qm-U{A^xTle`R-Y5G zIVyTsAmPfBox$5C0dJ&%N+8h^fa-qX9TW4i1##uj%s&Dzl{xSSJfo-pgVj1JT3%dPY zqMJo8O#Z_6_~MTc>%9&gyXt4pUB}Oqw$Qz-YFQ+6XV!B07-^r_+GiX09rOzpHRNWz zt~NV0_>!A{M#6(RH#AH3nN{>{;1Ej7UQphCR=`75&t=Djy&9Dps%<89{*V+@_G8{N zC4>9otu5VVRnD%be|TnJ=GXlAW!Buy=@5YsRqGeS2PV#AgY*`=A&r~|A=6cX0ec@f! z%0HMoUmB(I#9m+IzW!d+3x>Pz4E;TwI3_;g;xe0G{p`a5`}If8uaL;!!p0z#sH64M zK4NxVMCWpyUDmUA*NdwDGddc_9q@8mt$}*kg9yXeaM%7oiP!Hwu9{mFzp2gQAAjtn z3V*>H?F_aiFU0koI11ae9pVhnxibCVBV@lgY0pI|2Av(e2|NbNpMJ}JGjAKIVo;ZQsxsiClDe*^mt zj|cO^KWujGF^^(qxIEh^K=R0z*)BGz#oq%z?zuWCIVR}H{P1lyhR41p@A4_U#(W~o z;mxWC6FFa-%ba}V%At5}!=8liF*+a4pZzMRP#)ghKwx$zFzkFX+I2|cIcCWZT z@l$<>Z2jh>GfXqYS4ReKo5xsJ?gMg||NE_vf1EkmBj8k#Q+ri6M^WmzmAu~b^wkpi zOV|$7Zt84~^(Gdso`{!J6LN!%#Twr|1y>wcmV z&(@!3%>E&sd?(_uwBXx9-H-R87}K?WoL+Gz@&6swufiLIl6LtJBJxlxk&Dx(+&$6xE zF=X5SqwQ;bx33cIk8WuV7I@MrXQ}hWNdH1}MZ5lnn&0<8sU_(<3)ssoYzLwX1emUg z_rEM(`GV7Fsd4$k_e*6m_dn9Voy@RW@L0*rO?ySne9J|gI;ziy9J$LpXa3rxJ%UmU z@<$vw{3B=AP4Akw$H3;}>TSFw`N?8lHfmS9_UCYivs^t|GIP`4%NuhteYR^Uw(R)s z<9f&T04Tm*?`3Q--6y#}IrrCHt%@Wkb5pTJKOL=gy;{!N{aNHH;eWi%_{F>9H@+=1 zeHZ>GaedPXd(dF^?sGR6S+5pyg6snaZ3geCc6hT(Mr4=!{a+F9w**huTeRY$Xy00m z{kQyHPt(76^4%PsC%w;aD7DG+9r(q};}Uv2S#IIIkbU)U_9VvGKi<8@HqAf2`c#>l z&*o)4_Y30`eICBvxN`M;jXZyunbXwE{^gx~Vi~hq_m;_?E0-DPE$ErCY)!LG?#1Tg zzbwn^Seo;?{@z&Kdz~RW=7HqQuf3kL*4?R`Z5p+1>AC#$HMw8q`h~Av_-Md=x$}PV z@>2f|Jmv=so-cnRvbuFw!Q~APJ39P%N|iW%#g?tgp7n+ISmIQ}IvK5L{kERFPGy(n z%{q9_Qp>G}{lJWpWBQTRXOzDfPiuZ)TwlAOCw}|48MY70T?~BnmL;DuV`RE7yvNC2 z?ovhF{aeZTg2!Iobn{^Neb_&9?*3?@y*X2*7^YuzE`B=0`acg-$bP2!UDM2Bn2+2| zsoAzQbLFJ_b){DO=N6u2V^~+b?NA2WtKIUO9xqFO{MY?3le~F^cj`9F|FYkuc7Jsf zX#P6$*(LKMGJR`|j!n0%yQA^{?m1uMO~rYF%4Q5z-s_S&pH$qjbvoP`yF0&Lr@nSU z__@hGJ$hb~*Q&H%%l0|`<%6F8k?&y@iJv8peW?1?S=HG?vLOe_lmlU486y#L!{R+R>c{AFqOI-R9g4@>We>R$F@o_yf~Orz2S>P zW&hdRk3PD!RB3#g^|_OcadkL@-3+^0zxx8g)-|a;cdtG=m8-m9cgmMJ)kivb8FW6L zaxnL@c_)|1D1Izt155sYy(KeOn)z%n%{f-T$xK&xb;UIHQ|B%}c718vQ(eaY$L#Tf zuhnI-bHG6ZL+$)O3RdC+iT6o&iUy(K8GKu6TY$2a9;j&w)Kos zS8VU)=FSUg-v3vrq|W-W6feUYr7D*Cyyg5aUnKh4x1DMH|D-0YO6^|}DD+&`Fc(>D~E{m{m(gd_R4BsU!`QqaI95Ut^PJ! z!E<>=UfY}rr6(%hKQKM8`wH`68AJb7z5D+LfYRklHi!4hT=K>*0uSA=X`6B(d3TuP z;&KsrkBjWW(X*dKhu4)Kp0iI;tz?bv$1Z)xHUGXH?*BTsOzQYCCW9OPvVmLbqOWEq zPda3of03c#-y55oOiY$Zw{(;4Uav@)R?=xMq9+)0zv7S2%^&}LO!*X&xwd%kaZ~)z z-#A@uS^BS=yKN-pH=6xj(zM_9(AU==r#`zR|G<^EVOPQ1|Fd%s%&ph9PhWm-)r*yD z8nY+#@}|#mZ8qke^F8TRMMBNy(~|}Jg|7|}~ExSyUJE9qff`djQ=|L3nf!nh&X$628By2crWgvV72UUYuRhRVy+%Ab#UI} zl<9W)K&8IgqsFsE4DnAtr5s6iHQn(zQLBQ@uHtc_(O>yuHYU3X8-Hh+d8-0;7;r1*O62*Ol4;=dehaUJLhv4Z^J!jvrDOyIT@XK#CDjyj+}l?_`Kjz zrDj`0f05a@?avna?nygjCD0ST#`pbJvAehaUefv{E!rd#xtMLiY0GDphq|R0c3$vs z?Y|**!MXKbQ|dW0OM9tJFCN9ezIWC^|K;Xwx)S-mGv%)_I5DK0+5dHqxbVjVyAB={ z-(e&9F(Q!nUB!#DeF`@$Eu3o`#GlSd=&qg_W3Gv zPb@pX_9)|ng*^&t3BRfzO?~+@_}Y~8wZ8VN#cMW8u^T>EuN5K^>2ri9iRn_)KL&kJ zK~OmP*Lw}+Wt!R_kF>-|T(iB!+-rVhxlQU@L+$78@2|7x>HI$s^Q&!!GUHUwKY_DDES~F1Gej6PZe0GnSyb%olpRN;U(Y;nz0#&XJjYQfKyTAG zgY?*d3>Dvc|2^NnoDBXpC39hyLQSEeK)ldpbEz-kf@#?X1^tdF_2(o!sMvMaxy-db_5yQ|`d(do|36OA5iTqJepbY0kRY{P50e+RBr zuQteOe3(+PqU*%XFD8YUvV&9Uz9ouyrKHiU- z^xv<-L%Bl2Aot{F`L$l|rV%aXZ-si=+^w{S!;liT#ALneXJXg{%-P@d@Dmbywe&4}QZQ*V{%nP@3vuY(vRdLL_u(;`Y zl88x;`O&v6pYsLl)UVkj?yc*YzhQUW^d|N*J5J3wopr6@A8&U5))fW!y1OfWD4#xP z&~bjUV87^92b%>pHd*$DEpO3gCus++Vft#7JS^wDo zk_5jz8@a1)^?@84UWz`85Y>B@3GqnWhSCCAgSvP7R~Y}Pys+S%F2AjD>3M7aH4o1? z_^Y<{Y8iX4IAnTLd5ZFkgGN%zKSZ^`b=ZaTr&G~$dm!ZS=e#Ei; z-KVr-4C>#SDcy_6VXC%BVrDz2^W%R`6n8gs#WD3=k3)+yJFa)x$u);|aN-Bf!fWU$eBYli-_2VM#d6NS(86I$}GwW^%YOGh^FEB}+yK458kJB1YoS&g5{r`B0*dN_!ii6pqSC)Jg7qH@WP`>{h*CHm&VN2`fGq+bD|KM@({uHj3)6Y!CAJ*g`iL>UP15ij<9qLG#je$ScPk&SnjRkf<84Mm z`csxFhiRV4O!gZv9z(>m`3< zmaV}#sm}qH`rq50{CZ%u__p9?`%R}F3)NilW1M!mNu%|3%ewo|-|YB!!&vqDlZ@JZ zbFY=1XUb8xlDpAL31L|CO;G>Zh~^FJ^@%zv=< znw)xe<@{dxvOQs2!%jA>-((i=Y5iwo-nE(OYtCMt`nf1yBSv0lON)N)7a>_zscYvY zJC{rD@v9IIl;ru&XDGVvq}akoZr7{L)~;FjxG}UtC*XIZ_hV0Ko_Bl9mH&K;@2@xC zUU*AqCas%&*n%Wdmja`wuTSJdWZ_~OjA3a;I?CG)8=KUZ3oEN^I z^gMIZgQLq{Z+Kklax+QVcMMX zzc+j0pRyw}&%Cxv@Yu>ZPt1Df?j4fup~t${RrnqAU~V}3H;%h6<@Ei13Awk7OZP0i zIPu}-l*j+mAAg&suOqTtBrW*R1>KrwD<5yw`m-!9wL9<9#i=jFazC^ee_f`aw8;9I z=E=&P<~cj;;^KRf1q%F^Up?+7YGb>lhV`^uebVy89^dn4&t)?`EDyhy*8kx2x8k1a zgv*nYKg5ejDM}W{Tu>^%H`}q!x+QLViDmM}$-A@X7lx$ndC~PerN@L}lYZg`A4kQc z)g_5>#Xi-`MDuToGid$&`99*W=BF)Nk9WqiYHav>fAdkTd0PwSh^`ksTG7(;?#|PB zW5`Hn7OVZ^vR!WCxNHMX8QyL z)+tK8n7VMQzuwMsod@d!btD9y+j+k}S8XgN{HW^4><1RBjN(d;78|)qF$9<`ms6Ob zzN~eVM|Fe#%o*D*cA3BN6qxzU|GsAQDznvF_C41*$|=v@(CVXx!b)Guv4=FY-f=Hq>2ncc@3 z%Fp``-KbuF@x+0!qcfIG=VMosle_b@TcKgo$@v_;8+mk6@5~9`bCQF*{k6!_2E#<_a?#1)3#g`e)ih^!_)4mOvbCKxqP&$oQ`&WUt=C~ z_<)I(&IRHANL%|yJR1z7YOZT7U_5UpQzaLXyfb9kjg=o~HO93*+w;iRtwO^Fl+?JM zyG#h2oZl?5n3ti0RmF7bqNjIn`7+2}?%JO7aB5}Dua!U8t!s6UCOc?2DBJNVG~Ji) z-}r3m%I%xv_FdWZz_X#@%)-k1uJ!zD_?d1b%}W&B`=vx)iXkUa`^cUL?>W4Bd3fjM zd}xuBmfv0&(76Bnp;| zk7~+xf_>g*3=!PBwx2xPArURb@aDCep6_hdhm0jVk0&pS`Z$eO|4eUp&eQexex%>Y z5@!|tCx3Uz#GG9ZXI=NnsQAJa$l_?*^Z(@mkxdmotD5X%{H^3~`<)g#ZZkXN!=`1? z<_r=3Gx&Q_G7aWx%jsWTpsug8;qvY51?*CKH|!thw|(CKc4OUFoz%HI!XE?zczo|>k?6^+a$-lMo+?Xx}ILe*Q#R{ALK5l-#uyU>yWfev|i$P>I1uF z!F}7Zj(WRp&tX`waQ4SNTQhH#gkE?(ahA!0Oy7(x$$Njq@%uMh|JRUMF5gw2P$ah@ zBFN43hd!5cljIBAm~-0qQjRP?2wMNnJ)osSo8 zB?pS`Np)+?e8qla)VMPfN~= zEmRNF^W*O7G}ru?35vM}RU66b@Grk66mBzPSg=N5rnHX!QTrP!8#<1^SoJ{n_A>Ks zp}1bNZDp?8`6F&m{=k|2*;lKg^}^mC>Bl^;tM0!XzN=(n_*&n3wMY89>*REH2z`Dd z_R(ljv%)1!yMeHQk?R!7|>-_kc?~{SZ3F)4*2#kLQP9`{BMR0^vt3rsps?+^qRLQ`b?CfBBr+Zzc=@E01vrZOBay z-@S5i*S$^iIL^P1nyoG@o&4p(yJJn)?}@84IcWEalo&=<)=-G8=FJC;gx%Krf6Rx{I(=Y5e_DkA1IN-ZS z$Lgdx-sTK)lI(Yny->~Qo5&%`S$2EXhUJV554ATQYF~SH_o_wPzq~%$m@W9(U~S|2 zr8CkmbzNr<&f{YePnKJD@4AL;IN!euw+|e!V$ZSpEwDCbb=SJP3;IOsm{5s7wUC+%Q9n_gnuJ%$-PjOvWq267FhN{3_8h7p|SE{yn$!}zuVR!BI zwca$2BD$h^Py%#FF_v<5d2TiYkemQ1r8P~YKzp8LeofRr)%#qSld95Wgh3AXP z(J%MppH+N6uyNzx&quH93=56j&B*Z3?pK8rPjTAs^}l)A`fmPjW(*2C9+#O{&p&FTD!TWp(i6v}*|lPeW=IxJKVV$C z=Nr@7fBD&$!&_ha^Q@6An0D{yZp*pZp;vXK7&bpkIHUXY;GPX4_wMU^{i{<`JTi4> zwWIO2Js)Cs7FTUNtF_JG{KT`%Ha99uF=(8qO3 zlNoh_AG=nz@R;2B<}>rq_4I8sPM*2h!}R?9vkBRzi=xdK7Nq_E!MKod;@3x}LQXM~ zk7HIdG9=#n75Cw)ef{6M{2#Z^2=X$lxX!(C=OpP5$I2A7*H3wVL`pb9)L>ccl#1Bd zN$pFMWu;8TPvuq448{ftqJ#`?$RO#H` zjW>>|x|=e5D4o;zuzkk1HH`TkwI^-_om>=cxA^u1&baH>uCCYd2?@;?-JcmKeB$SB z%ZE|96G6s3p4l~FPPRFNLe0U&(mTyv3)p^7TD<7I(vJOymVTDG%HU95;#S%fvWhX^ zKlAOF|n{ER;meWWCvE3Ury+>8&N-k;uw~~JdD?I0Ox?kLhLou%Z#!93AVviH5LU-w`w?1}t$5Bh;Y4Z9l0#1A69AnFtzvnNV zqaJ(ktnA~gJCEi_!gMXrD?T*uLr+?|MS0Ka!tUZU9s4*A#g+=&rd>KOl+#V`3UA^Xmh0W7qCfl~a%=r>y)$35Bn ucq2*xAf^{aS)i~Q4V*!r4hmEM)jK}DxYsnJ@)ZLE1B0ilpUXO@geCywtd#}; 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 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4mJh`hA$OYelajKFnGE+hE&A8nalqoFEaFd zyo-{Olai9sGa*6!g&rP)hDzejN)w%goZV!WXmrI1E%tCrV`EiTRz2vXzQdf8cd32A z!ZzN;GeV|x)(6OZjXUde`qZ2GQ@7oFUOn%Af{(ZYy zJ@@aN&HvZ!J@@~oy)^`st0`^r&P>?*=Y!+=WB<)6{~dJnFTZ5I@}l3d|0d5SU%f8( zWy{b1)~PXeFS6&n-nI19i-&ckfB$~#=XATGe0bUFtOi}l+Ru*mRTrGu|6XoouvR`7^ZT(LD*=72nZhplDZN94Oj`OE4{%agR?eX54 z2Me^>|6Xv6*G-i>8}x1Wiv8Q|X8%|AtBm%zX%Un^sVqlsa@ei!OPcF`F6fv2vcBlC zK_0<>T<-Sa2x2;RHG_4J*kz0N$b$-nf@WSR|!kIrC z*JN-=zfI|Rv17|S%NLJ}SY!`;dB2S3&gV1kd{y7(E7H z7Kop0@GE)x_g?9R{L>#R-!gu0d|r2C_nSrk?)}Pn$K1eOdh0tgd#&Rqn}wU_^z3u3 zG5B%e`|%s>43cZtOwXV4ctyZxk*Ux7XYWtE)^c;x+2yO;tR2LUfQ1UKG{Cq>4(oT{ zk8YCvuW|h5EZxaoH{Zj3YkKRuXXf+bOL2bPdqT}~JbmlKpBjB*ba0#dwdrQ}^`Fh` zA@gPkRwumw@_Nac>8mdK9h=qn`v0S<6rqN{hS}QR!j%6#UXVX&)1H7;J#ya|4+PyW zydbX1#>~ufqt3CNmsi1ht`^gp?vyY|1h+!yPjJI=ih`yzt2n+2NQ(a)K+ z{rNVwe}da;!cWzSfBn9j|JRBS+m03O=4W`|-l}`_&0nV3$Ct4E-XMGB@a-jQ=ZI~r zW6r%g#ck?oP^6Sz;kL@VCYW^c{7g_Zu@ZdE`3=!>=Px^2GUSG}U+Gz}xx{g2aJpDlP# z(j@L;!h`Qi_nZs;RA(shv**cyRWf19503nv)Li#%LHv}(z7ki`euiqk)mZYTT=9GR zRG~k78~O`(^D}HI*nhwH!gSLQ2l%>!*Hf z?#{NAn_#q88|*l3vvWyz-Z3+jOt^lO{ddoOy{D#&zs`Qt>~?cmtVZGf_tPcTWSOkB z&`fkaZ}l(gL2<%*Yflsh-AZC;Si6ct=J%HCB3@H2Yi&EUxnS10DX)`+@4n1hFjWl8*;pHTzZvJ*|tkdwO7} z|EcV#)^v&0qJsX@KJD^6RX6?A-;1x8e1ZnqThZ)&=&$BVVZ})w#73jCxCVtM# zik)H0V#H^^@C)A*x8eJtJ&U>f|0(!yd)^}HdgB9IrP)fu`6sl$&kS02ep;h#7@JhV z?0plongz|;IHeEonS9;Ze?oGA%(p)-4{p5py!1@*LbhmyGOfqqOMI&NY(4+%I=F47 zueIimc#BnTC9=1C|t5lf?FQS#2o)?1#9SNvF|<<);q( z+u$4TG4)_XcJhq!PgQ5k+#5odB`vVj&g4EK^Y6~{A3eKt?B4yWDcFC;o;_x&(O(YN zI>!8|Mtis2SKYBcvggB0`IRlb%3F^zF!Y_~;klJt72a7C^`-hzv8T^1pPhPU7w4{X z*=yrnvmqq;a@waOSxn5v60_|lr$_PrJn(o%Bm325`MQs55A9S*tqKDr_Q_^OM$8Q- zt}kRiGiiVDsiIBaKZT?`DsVlu#oYPcC0nP3w@TjB3C^0@xcQgaUyXy)8+fNUZWLVr z%9hKIa?JlZ(d&jqP&5yNl0Dz-X!ojZ(Y#n zz|-YgDH^S?W=BQui$j}#h)pop*52lD;_oYqe_}G~n_s%Wl6zR=qpggT@GC-zAYun;$LRyrJ z`GlXQl={}(DK~+pmKD=iOg>h$%6I zP=Q9_EttM5^D=xKYedv@RjqQZ1i^Rf^u)p zY2W=b=lnh@WdC^5l++x>ZAX~|96wG8yZQdno%y25HP76&5+8fmUSnr?u2`n;OrCwFh@)7^Yy%@*@R^3z@@uP^7WJ6_ULv$X#~(&5ajXYc5T1xs+;&H8*V z=sV|g5gCu0?~~r07gnx$_Eh3Jb7Rkiw!eQI|NHlS^nFk>M{>rzDH|>d&$Is1_u-@C z&lxWk)T{||eQJM=onhX5NwoyCyPh+PVsjQ*$1aI0h@0{7-N}rJHnxgxc8`BAo%j5& zRxR85$4N;c@e_=vALRJ#aqGl~SAz3B(rO|fvGr+kg;r%L$V8{bhQCY~n0#Q_X^y#> z`ZGG?CmH#xy??^nciykw{JXU5-`BD)zHqHJ>IuG)pSr($r@cSV`zZn{YpH18{@!?hFeKp5_sA_&br_ptUV_8OHpRT}X_XQtW zm}2Lyi#g96^z3ZP!LB_1PT&al(taMJ$CM1x1ek`@5#-RjcLy#w$kaCa0_DTr`YRDMSjJ#}f`H_0>69 zyvVt+x8h*thX2`@UAwleU`W${(enJDjno$Jq8^NrY*BvOoT^0mH#c8h-YZaj;Nd~pr=4t4f?qc+t_3Bp7Fq3*XGRi|eZiyI1TtHvF(A^>&lkJHce0B7zQgu8 z+jc3Xo+{ngc~0ZV(QgOBpXS|CIBnLjmhvWk@_MLcfiJUT4Z_hZc{&)jL=0H{RO7$e8x=+GY9w($PW@p^4|$ zrEN_0&PiGBXHt=N(JirI`FhvAw%-@@-}jhx=vKnp+f3`{gvAQYx1V5?yAWJq&iwLQ zh_GeHnX_?^8RVpzjHOY`#jR-*gbX>0hQ=hl|6ILozF%|BYsP2X$;szuWU`WB=d zI9&YAv0>$Od%<%i!sk~kUbk0Zg)zsfOS9AuzuqxJB5#S)TE^6n>8nE}dy4rP7JdGh zJ$>=UE4_cqv~AXZi<^;Nb9(pDMInB6?4F5fnT`u&X6sGbKIx-O>3)m2lN&1M2KT&t zv24?QkV6j}FWIwBZMSQ-Wo^%?jfS5>8M`z3IBPFE{F4XRBxQ=tdZB3SrikG@L)>L&l_w^6@c?cj~3?^0-%aZS~c^Zr<0; z))#l(G+o=E@jLd){h3*RHcYvmBH9{ytaFOy+~njZ)faaCHQ_!lz529d^;wnUcG_+? z@*-4EeQb$fm(!nWw01Q(w%ZP!+1h^ov?M#@s<=gd@AZ%DoVIfFy2ATC*K8!?jz4`F zofi|-ppk3#*Xg8@0?!YLh4CF)&4T+*fU{GT?D}QTOs?xDcZ-LKeEiz|x+CK8l(MG1 z6Smjvk8tyQ@#*6x*8I@#r#b3>8+Y(7X!y4`=F0t&_yZjSTVe+Nn|D;QFIiM0V+EhWz_qCjFLLS(Pa7%NP!E<=*1+ z#_{)Ofa2R`?}KBitot=nf3;3Zb+|aqyCTIG5vU*cxNR2AnY-o8g3CtRD)^T2cNNQC zoF5mHoXPw0HSfE+=u;&VALJ+eoUkjNZ5vxo-194EWf*#9*wlJi@XIWD&v@v%wd7U( zYPBnSu5Mn$cew1ZR$!l~Rb~H4M-xaWvL)T$mCxE3Y9`OJqAD@&^nxE2{!7m7s`Ie8 ze;eXH+p5Qtnl>c2fn4`O?xRvp)p{PcA4=-EDI2O%s-GL*__X3${OTUvd*L#{jBE6c zY)g%*;ZRtAF|W7d#oSXr-Anfig5sN(W&1rQj`=^?6lDyOZ!wveZAxdmqni3}Ef;&p zymO)LokeTp1m=fuJc|Se-N7XH_Ns%H&wIqPw}m5lOhrc7}Lb(;+R*e3Hd=qta_%FSmUDb<9R`&Er|EPglOM zxE-pwUNc1Vu-2;bSNw}kCA8=lU2<~^>EHUIQtA!svi|8tn$dgsgj2pJ#M{ItWRyNi zQWu<+X8EYxB|hx?Ytv=M?~04rD$UOOoI3*Q?U$>${WxR(iuq9GrcHvYy?N#^Z2tM} z#YQ8Bgny#pOMRpvPV@MFa$C&wUN**6aWj(HJgn0l=9bFJverZ=&+A$7*y*uM>*k{x zrdL<6rB*oI-J}}au=yT`!6Ag>0=CS0%*dZL=jO4C>a)W`PliRy;ZFRb#6C1&IcJk@J{*s_F{4>!vUpm zv;4^`iX@ktWi?vNTpRLs_tV7=Eh}=Ir%hWWZXstBBzu3&xm|9L&;M{pp2u^yFfsCn z_|qQ&RreQf)3NiHS$Q!`<5uyUgEJE*r~rv z_q+MX@@(THDW#m9m0Q*YRV?gqJkPM);cotxDi-OSxaR>+1ntjuO#Q3(>b+0Thne{z z*1Syr9By{J)p65W;p^1j%E%Bc>12^Gdz)6M-eVnR1{YT=^~)SpOG5U#Y`(v&(LFu( zaRu+=_fnrzUi7`L`+dA&+1}f3(dmjovD1EsO7?tk^}BWA1P{XkZz1{jYYJV3R{Nh4 z)Bjf&!S8ma{$N|~+RnezUOJv_-Q)II`&FI%hWd~{bEf>9>uj$#=cHOHxP7(w>GpM< zYacc24>J2V_wV9GR|}`S+-Sg%puT!TRL147rKeJJ>=-)azUXG!_RDx(<<9=&P#C-9 z+^)3W%N}s|RvJaIGek&c%>8@TRpkFX0hz9K-g9rKG8l04)^9l*__$Pv$7!nW^!%72 zC21Cou4c#6YDxQ8%wm1-S$|yrs&1p{kppFIXWz>l(h^Osnmz>{P$5qi{N_we7U{eB z?B?p@hZr8@A6Z@+VBEz?_B)hYGj(Jqg^aNEaIx}d@u%xSx31}g>i0ZjD{zf9{&I17kHlJLh7Wq0pZTw5 ztiGo2-FLf?VFBy;Oq>0-f0}0enW8&S|ETy zZ&LdHsO^l%nuG6nx?~tWuIC~t5BOdf z2R$`1lVsSickgW1;Ep)|RdEu&{484*t&{wJcZT}nbOr`HwN%NF-fic)loX@#zB3-s z(fR+7d&>NYW&&xyr}S@n`to#&@XgIP&Q$WsuJMhYT;8%K{q;M>n2d^vD}QUe$}o($ z6*1BIizde;BzG*FUw_OaI`2DU!}qx@Z>nE}u~>4NV!w*Nms<+)U*wana>nu&i=s)wEUO-xQj`2~GCx&X?kP z%5G~PS55YN^FcN!E_g*yTc~b9-|s~$ptV5Y>f@$|Hkq<*m0TvwbZVOSv-if^c>M1@ zU~Kv7@x=5-cW8IZjo%C2my0pIF}Seq#-8m0c2iF`FyGKzyXpSXo%Y&p=kt#~u}nBu z6LnLv!y& z-g0g8?Snim&q=AL->O; zH?QC8eHsp^5qW*y@y|VWWs?P!FBe|jmla;R#%qe<>q!NNY>i-} z14dshoTjV%(>)L_8Es>9pDCeN^hcTIW#f%YA73l}c~2$b*OE=a-!+V1scblv@>zam zI>+*YMIqmITYOIk4J^62eqW+IxoUpxyvF2}XScgw%+J2;XB93Rb}I3BqB!#*xzc1~ zu8`RIEbb_MR0Xdt>b5-c6e6SXsHPL-sJ!`^+fknRz59uFY361lKYmWid>CFEQuiw3PaQQ#H1Vw-&J<61ly_=gdsuefDO$ zm-im`7E^d|=1Y0d_leB+rtM>pFmSi?IWJz^yXk)Go%v$QHP56)Yj3rsJk-cC1vcJf47OkhM(&eVl`nd^n)d0sT{s=w6h9kYum znzboba2@laO_0$}eeapu+|Nu;c~*GpSXPwg=lrX0Ef{{}e)O~qx_+hi>?$iHsr9Ed?E}rsJcgd^Px7QigxQJXgQhj}k>$sBS`8;qUxM8QaLXO*e z8l&s^S-z*C&6u`z?pN5ity;6pe?Onlp1+d+6W+DTKHpqv4Sx z^h|m1LfG_#q!PPf&58C#UG~{^*Y=uEDlH5>W#b?8mFYe!TkT9+)mOKK*|d48zwK0W zD`OCNrkHvQ-lKNDp8RBa(&uR9w!TwrhUxp>Pc-uP=Z{(Po^|7giT&4SF0MVOQ}gbE zVf7xJf0h4dCIuzFyB@qHm8sy2(DJ-DrIHKRcJ;|Nm3vp-1r5s|`Rn?i@=VTk1J`pW z5~LC&q~nt=Wf(L>Z{})Pf57d=m&svQ5^v;1Of0(Lb*uK#lZ%VAQ_~NIB?uv`kPu({;WUq_z*`0yjD}%24ych2K`ej|`Mt+7DyNOP=y_^Z+4dv@!uuD<1rTgt;#%9A*M zE_~drW@y}P{D0n*zj~d&R~N?qvWksqo^Z71vUO@sPZAr$k@AhHsb2*2)+MZ;xdq&r z>6+5~)~NrUWBS6p9JeByERoW4&l!GCyS6U!$GYzv0{u6i<-TtHxWeMP5o@XJ$=knK zE*ma7W+VBVVZk0dAyD|9=wRGSsk`y!Ma^y7I6wT`K9}z{*NQ^l zrmHT`>ueVWui|A&+YrUrZpt$EkQn1D{zC2ze=A~_u5O&l9?jkgcJk?lzCBA1H~x41 zqQiXU^ptxW1<&OQ>Z*G#@OjSvrgXB*?==#MyAz*IIs9k2!9neH??cVilr5fa-S;B; z15;t%Cnx3o_5m(ajlUN8F1@I(9Q^q78+lO+rPR-FE^@biK7MZR z2DcYGgPxsjNnROm6DQWB`n-C{o;oq9e>w#Qt_&)(k2{_FYxHpMuL*P5&-}{yxensY zrZR5kSy2vhYpz|HvsV1v{=VLbw@t!HN0zs&d8Ki1qH$E~h8wSE)`s58WlGvrXD(?}Hs8%9}%8Je?U>@`>sB?C8%aZeg|!#%DC!W)kAYUL)X%k6M=jr&t?;bpie@|=^}`JP2= zSyobzZ@3&%u=L(f2U8`Bg>#^h0&dN@_jQt-_yR` z+~B&L|9?f$x7{l5`Ww$}`ChFhcwV@UKWyvThA;!o-mFBrcwME=-*&=3 z;Ub$$?KN-q-l}S7UU6Ksa9!-;yeB*6M?}A~6u!l$7puxEN&f&3J9? zI-hxxf2aPKW3};^jpVk+T}L>q!(N<8H8^B5$;>FncHbp6gOo?H6g-rsT8jY5pS3#VIX6q4Wi#NX^w*0pEI$+*`bk{oB`5J`8Kp zN|nDZmWysa$EP1$80^JS(=_wcw3_uRx{mzcCopZd#4pyxMW1?F+~yQo?Y;|c0Z)u* zZ>!&)Deg5*aP77OKhG>nPc=Aa$K&W(RaJp)o;o@@5mos=6w9-uD6WUh7UNyJB7@g zKD>4~(<&w)aV~G+hI3OFy^-$RYah$cQSqcP^v$DPTVFj>Vh+*xw(1=3qGbyoYUg=y zo>SZEAe~*gYDU+b|ND>dOS}EMcF)Og-oyxwN3D-NAj6uU=0v9D+-jYansDvR0fwt! zhubnVtkavE%eH6b=I>2aX{WE5_P>b^n)=&z`Gv5Q@Q{s6CY6u1f5)2bu;@#M(jJJNld+CGXb~kCmh0Z^3%=(eOGtf>=I3wM_<6tIhj{oiK-Amp`dxrFF zzsr`TZm{c0?F&$4%E}1TC+Wo+_ zx6&x$_|s0?3kxEyUGbXMSZl~8#WH)|)Pp-DnHgSm`R!mV@QPxQ;L4eKG;F5936}|a zv;R#CdsKIR?TZ=9*yg@1*ZdvpXTD>zv6M}`SBA$Tk2K|j- z;@1+IvYI_VeG!>!6|rWD$rDb)MSZ8a{Q7xhwr+gs?=`LQ^SvIWxaO%^&4ruU7zAv- z-xNC~wazOt>|2~t`>mUmZ?*+|synZn!+(v5KT-AB?5ThCmP_BcDakueUzvw#gU)7E z)8%?*kL5b06I51AKYvzS>Yh^St(E_--q|C<(a`0qWQSW)>osq@1e zK$fytTQ5HN`6);5)oH)qS%@W_p40mD$=XPZhZ(G2i+(OU&9CUnB`u#)?>@(vf1 z{&+jv6&y)aP}}8OAGE3GMJ4xZ@t{z0>{oyPoO2_h&EM-E`;HZoMr1h@0<=yf%P?*(GB|l|56CGS|1uHODqj z+~BzS%IW)lmQz~JHB7Zh-?*ar!>p~Dn=73r7T;Yi^{G+Gp20%w=9kGrsV>nvM_;w8 zhq65mlVr$9T+JQr@n#9yZcuM6ZSKHl7F=ENWHLGFR=u>((kif2Z?Nc!F!v-Z`V`_XRJ*5zrYeJQ~It zqRA|4!>p}m23qlvEYI+yg|R~I-2slQeGZy$-@GYFidR$Wd136Rmls+WbE>*%>I(5Q z6*qG3_GSj06uw^3q8O!JdG1h7kE}L}zt6M9qNO)JWa*uI$CjEf?ZhjgCFhr}Hj?Qqx_Rs;4G!TE6-tKVeghRwx^Ds^Xj#%kx98CyO5bS1a~- zqZz}6Ll>`XKQ-gv0ejgmS)6_bUoz6umka+k3T1hJS5TgBSA5@Q9{&#$U?!S<%&5;|BPqad@NmMeyZ2M`q8fy#jL@1S+-qmd0->hG4ow(xZ9i7 zY^#OLcDz}qxgqQ9Z0DT~lRM4Kpu6Y)cbPA*i%R_d?vV*!m7`)g=fw`azsJnGeY*eZbzWyg4&R>=_*wnnMw4j2&{aKha+8@G=FXivSKxMq3TuDj zx1+mwqWKw?`Tzd5Vq=`a;jaf~z71CQ`;>A&Zin}&BCeS>*_&&5FU|846Sy7K(%_Yo z%@MD#IrYHCi_FZ-zqW3i6(VWNBhB#U&6_uO%sMX5P;0HV$!D9Lc$)FRDh6fQuT81t z{g;21s{7Aj{kDLyYmLvm$P0n`-{NI&SAE?a#K&bAuq8F2Kek!*@s`PIleHGN*D`F_ zyt(+fqF?f@1>Tp;Ic{Vz7@W1ZzG8D+oqLVIt?SQzc6b^$Z&+g%uQgS@FEg!SK7$N% zq}}3{V5axe9&4TWlJ6(U(fn%zPmksLQ!f(bK~B3<*>QP>8JF>{X{s3;jxsFxb#O*c z*cI;X$Nzt8`I~*NpF8DeYf$B*74k^}3R~X1S~dsar$GEf}Mof~?iFLi0enm=oIXf5SkZL%V6gHn^~ zkC!{J z+Xyz!iTk%bIU6+1(gnH-lw}*U%!=iw%oO&l(9NC!c3IgwjvRK22>QOT0^Pb_ME-_Jo3o7r-HzuTX!Q?Ac(MYz*uaa~l6 zokHrZ&6(*S@VxIpSws4@^QR{9uik%G^ZfMd2p8o~npb_{ z@tk`5-3U)sEYAiRlnyqjd>7RvAO<2xIpvNMtB*dfPhI?B&#E_KwPzjg7hmX?`%=ul z_r-#0zN$-({pFX!m%hn(T7TqZezxkX-iNoUci;J+VI5kR{gt=q&z7(6WxjlD{`Yyo z`V)eC?EjZuc+OvS-En`(h2W(R=SpQf510MY(_Hs1fG`yF<~eM~O6z|B|HB{M&G_CD`Z+e}4%N|jy?D1UML!|gx2WNy!>pZK_% z;ljR$I}h)FY#qCXcUD?GY*u6a$36_&d;e`2z80Q1xjIhvmvJ}8N4SVt9F zPg^^6g?U@{`?p2^7rzbp`IUqDB0Jx&@|Vx|vftHBy~n+Fi}3q%-2A_mta)r)^hV=Q zTk6ua<+5LH7bQDfI<@@ijm{h%%Ki+sr%HYH2cFbdyytKCd(LEk^Kr%A$JrX6Okb%5mfzPo-s#F;uXDWo z)HBP>gGcS-X0JD~(eh)Elph~B!y(KT?YCmd zTm8xZ7`d2*g-d$g9dNyQ_0;u~y)_pE|J?m_J~K|KFLv{!uWkH$zixRPG4W7O*}3Qc z_5Rl^`qsC8;fd=jW_#V;8@D|6;OwPmeutRfYIwe~TgP8OeX^-hYW1($NPf3hC%V@a zfWlUO7i!q1eq}C7zM0Ey;Qik%GG9GR^g==2+2&36u8E&6OAspXJY^Icn)5%*GS51P zla1-dxrDN7-H8cT zQw=lX3qJP$XuSDCta|#j3!z4-(TVT#MLr1s$ot)Ie5!A|{1^Uj?-@a%r}dT1Fs3@R zE`P$~EdOnji$CA?I_3AKd7)#`_7a&G>(DyMy{_ku+hn}mD7^on<-u)tGA~?8(7rWm z|Gc6b^IU6WZ~uso~l;_zoKV(y&uJr=YOHJmbl!bMIxpH2gizo%Q~Q zyl38P$Iq6Qap6z0TTJHf-J!-D_Vj`9Cw9NPiC5iz@9trF|LW!UXoLTYKSY{KMOQyw z{bY*m>#auf>^J{6aDH>3>)_MoxK)>>?zHD+3s!HHShrWj-s0tfjY55nJm&k?y1Y5U z+5K zZyY}GOq~B$rKR52eSUqsQc^3#9^9C*RdlZOw+iD0cYg+3T4^4(v*vs$w(axktK+Ru`yi8ckn7y`6PNVv9Bx;=@3`jc?+*v| z6)n5BAkh1Ul$m8_mm}i;ld}I6iMmNS@Mj4eaTP17{GJaTWKIxibS%x7f zIC{60m()dPZkgNpN1>wZdV{ipRlwfoQk?V7l{l=b__1Kk+!trE7hG<4p7^ylvFW+t zw|L{q=J#GVx*EOKer}%|adN}^?dmr;)Ke`OuIU>TmAc*M256Z-M;qEoq{pp z%wIoP9M|Lc-uw4Mb;vKC`l1V$m)Mv#I(}Kt@jk)y2A6fL_nYcXUgr-eurn{sT(FqM z-F}YU!2|l*-KGg$oGZ)9&elofI0(UMsk`DH6v-U;6Rvdw3${FGk~3~$e`%*j2V zW1(94;Niy1-L9wB=`P*lIcJZ|UEPy{<{O>6AKH~fYbWMU7W!#;uIm5dw?0qWpW8BR zZk<2n)2=lJH`bN5>HFO1yv$w?|}C?zhx7C+Wr3b=x<`J#eq{7R=&B( zu=cmu$>}9?w|;*zMb=W0NrzeNC(EH8p5u{DGS+J(-hm3MZ)Tbk=$PPwyBn`>;p$Dv)n+Mlc~iRi z<>&wXHJTR!`qx}uaah)n;m3?E+i!1OoL+Em&LyjFw)GP?v7BF{t1pzZ{_nE2?|)o= z&MkXq&!jH4lFMc@r)|<-Wo(mQc68aY-OIEs?^kI3ImDn_c1FLo0Bfet7{no&i5By`z@cFJ|X_?)17KRRqwBvaXH(Bp<#MCOB&<5!o5?IP9M3* z&>(w%R^*#rK7lYHi4FTBQUkVKpVlGn{eW2{*L006gMvnA_H%pfPa7@v2KUu!Ol}la zi1u@N#_j@yz23d7v8o;!t}S?#ZF!c3-Rk}B2O!;!qb`8%xKq3vop(` z?sIeEd&Y>zGYb1zSV|4LAO2f`k|cOObxYZ~JdnEk(>dp;q7;M6>6#tSb*<`O=pR*= zVsLTXv(9T~U5nCw29}4BOTxF^Uo9+ErLHt_!=xh-)(jDwgBbTWbgZ0g^6hHP4guc*OTKGumARMoBX7GMWx_T%fF6I z$$jk74102Bu3<3Ft3Io7knwsmFT;lLb3fcQKg+A0`Sh|{|M<-BntM0dMop@pHsxuL zK=jfWV}^qHzugq?7$v__>8-W+c)(%rtv}I63*K)HD!=F@U&lXt-?A0L?vZADxAp)1 zw_u0f$_&-ln?a8JaAcZCfy~imxzjXfPi4K3jcQ zESvO4N!{p(_x6|WoYggQ_ZS$wcLg55_DcJ*mJI*b1m=bl+Bfg^%dg1z%K!Y3f9BM; zfzIhO5p3C%)zj^LOq}VwwzX@{N4>2$}rb(O+`{w$#;{3c_>TT8QJ9i~(=Nm_| zpL(%E>FWdk8{5Nr{@aSi86K#<$T){3_m?`Q9&boSn|*&cGm!T-4B)VC$qRDSX;41MEre60b)56*{w zW`Vpf77emz9rh@!joVWSHjlaDPF+%=E?&twt`ohxetI-&K8+8>D3K_V!5La~}$Lf+>G~3bR zRcu_1Rcp4y`tW?;`CdSJPQ!QY-{&s&8Q-b)y&@}OxhLJP_RlNL(v!wZeZX#9I6txG zLrBF@hMo$^4{Y0BUJgoizj$Gnwb$ZeK86RHe*~xYHR{_ve3)6J6ZCD{qg%TV{*7Ln zdMbU(T($ifKkt8iU^~5MZpWcxJ64uC9lLhoU{;90(lrG=GYa1-+e42RSG}MEsfYseQ#;9`k&U6D~E^j{3UL`-gZ^bkSAa#caQ;THbdW)O`%8IMmRd zAG4j|p=$1}Eg?5gv1rece$twCjO&ySwp>MQ7VQ|j$nrKqUFw~`{f^S%UG|Cz1y!BA=T_q3ogL&H1I z_XYRpYr#Y3?wX9$x67CM*lTx9WjO9PyKW)d@2FM@1wnrs(Rl_8veL|qH95bggxuUx zEcSet-$f&#(gPRxW$glYi|H&3Z9bz>9sRP_QzEaf@0!t5<6C}Hx_5RZOO`BvFy*jug!Q?PvJU{igWU znP=CBLxbqm2|~^D`*m)A~itlUga#{B#xhLm1xZLsMj^4y0c5p(gt)HxP z-WL7Ux6DhIndLJ6Tev9vT)6axn%KEptAbLE@5pZrj^<@pccWg$!D4s7w?me^@msr` z4rSf>6;t`&Y_0l@PD!n%xi0BzOg+A@klF8V4-PZ=eGkvQ=;X8uIdv{$f#sLryL-K^ zY5Ez7T%YH7k+bW5@V4_!!f9+S`>UI#JmsC3JYDo~?K|tKnf8*(ZxeQ;s=V^!p8qAy zMkBSV#%xBT$6T}8g>Ju%oOIe&^xE?Jh@JT_!NG5#TpY4!g4S%wn9@ZV0nZrIvKXt6 zz4##T)r-I9fe)wujd=}IF7ce(xO?$iP+Hsb z`MdEt-RDy_39okM`L$R>W@p`L*lez3fHrwkV~$ui?A#g8)_#dAl=2neXU|zZ5_7KSD4q zMeh0%pXjMtvnvCe9_{j*o6JzKf6Dc$rpuD6PfPNMyeXX=cH{Q%Nd|ZRbbKh-G(*Vv z{M1joCcE8U`KdcBpv~-=ZBUt;Uyk`*KaI%czRTm-x?Bxd;sds2C%Uyd)@uJq5IVPS z{XYfajo#f4?Y_@V`qRGYz06yS!+Wp0+>J~WIKIPsTIS8-kA<3_ORpxzFZu4lvu}H` z*M5ipiUQ|07|%-gTJ`3|owqBFC7otukZW0WxP9@9)x{<(pSY^4BhG(vM=w^E9pZ1{^d#T59gRy>Tti+cRmcGZm%2#udm2Ew6cyk@k6=)`n~` zP@IaK?fD;fv%Ift4mhmV6tv84RK8uj>&5G>FGWA{{hqNQSEe!fTz9tE4SS(Pmb23; z<5W|B1+24fuUD4E7XT5kv?D7jMg5r!i>?86VsofDs}`Z-%|uhjL-`Jo~wI1&uvay)f5 z)bkv%i@I177d5r_X&#%BpxE`YjMa<>?glN(58U#kMPQEE-gV|`ZtuMh6?``CI@Hm$ zfASM6(6!k*)VCTfsF@2=RR1Rs6ba zqp;TvukG8;EirxDAQtvKt9qT}1|`8?e5!AfdaCs7H0Pvi{a#d7^L2Z&9@Hrh4xDwE zpz+Nz=R!%=ip%>Vw>E?_?C;#0cZuou@va%mAMRe_S>WD^XlTazH{%LEG8b97M;E(*mnge$IY0#{m7+VndxK{Nz>;aMwEMo6{JO1AL*FGu=&up1Km$73f?nTpFgUV zbx7}62ZK#`>*fxP_W!yyMI7%Y8~qP6%z9t3+F-@!1FKZ4w|)GmJ==eU-|q|i+;YVV zw)bxpyI~(Da`dO4yNYqDvXvmS=)SZs61s_Z-!L*9DOl{;yRE#SZsy~<8^K$yhnn8- zn;Tz}l6&OUWi3tX#gWf6to2WFP1?3howHd~mido(>TiyPYfcy4@lX4GwCCspi^Oo2 zrrS5ZL?*f_B~|tJ*6N#Onn+AolX*PSz$&2ncyws@()Wh7dpEky{q}d~k%Xqa^_p7_ z{4-Q6Tf^-hmlGAf>`jIm&pll~InJv#7xJGnHq8E?QO*AR_`Gh(BgxyZFEIf*=*v^L z^2l#G>AhX&Y9v*?H!zU&+Zl z<3)u0j6dZ)wOh;n>MNTjHGB5&-M5PG_oDStP4_2$k_zX3BQIRF zt3c0l=DrER-}3#!i#PladLsN{;p0@pKG`sy?+afG96npWWv}+9Y*kr>!z+4Ed%5-8dy@&}Z=4Uz?6~&+xXtg1NRPL&?*o_JnYk{QPxf14>)gh-onO2jF#SHh zB;ci7{sg03#pO8?=ND<8Ran62^h)ffrefKF<sfU; z*GK(|O-z5guTXFH#Cw-+d7U~poiqJP;Z5&NOsTJ#zvXvb5qiqzBx_jbce9Q!apwHF z=JO90Th2~hfBjTb-%hcaAKI7fOKsG>d3pDn8|{oPy8J(CuCl3iO#XDD#2tKF2& zPH!$G?ck4Xm&p6K_-*XOc<$KW|Dv?tthrnMW#{W<#e1VYZvHfR_~54gy0-#SMm$wY z7u^DH{tEeRe)R0B;46ZUHBWsjURf5DR+T4Qwza8WCUNUmJ4?P}7y6RBO*gr2?kI78 zvzm4J%!9U%Dl68?+%A;qJaT-h!Nu%_-}=6DZIsGN@V^%~J*d4laKqAN3o5za$WLy% z%=~y}(6V?9jo)8``5qrQ?BagAQFgP$JLl_)ZJxW<)8uq(IZ-;{ZfwPniO zK##B*^#wc1lo#5?PTE!_A7&F&-HI1X;v zU+b1wIrHNK$IUN2y3=oDopwwRaeRMynV+rUv*&W!G4GA*vsvYhH$C<9y77H-FyG$< z)^fQ!Mn>-|)+^nYW?|j*^75+<`d#ajF1JM`{J*i_Mb2C1pk?-&k!ERJ6Wuqm+hojF zJzcY@cbYmPv=07^y~N4UeLg77^oHN;_>w(r`mu$7*>)+ZewVghY_PN=J>l~mr<>)R z4~rxtW^MbKw=%;t=~c{>w{ky(R-G-LvPgG@PlHlh?YoB$jw{_U6gVW_cqL~hXaoC& zzU1EvSbtRBR4Cx7Ua!0D>KCSmJLGdtuR3hUb3{&QQ>L@-hWZ>4>vr}!8aWc>VDa;-M`VTa1&5|hxI?-|Nj)}bM-&;>-1~tSp|86Z<}!b-g2SuzVdNT*>9UmLu^ZaTz@K2JHhri z&-Xa#++-)0F#ao3eQz&u{8pOT_&0g+BJm!%o6;d=VLj_rP@H+c^}*R_X8CEGt_SvR zyL^&=wOdcUhLQ2$0?U-8?Abr<=hBFcO}FelU~ zCbA!{ePMN^;In-0`xk$vWJQ}xd|W=4$^EwHr|z`t_F8^+%|{MpS2HtcSf$tYPx;v& za+-l5y2WJgmGe#OL%$r|QZF<$?B=`1U)w7CgSPbkQeE!1R^nZ;eH7E9iy79Q zbr-*}Zkrgk|BH)`m#j3ygJk(0-M1|n?S47EG9R{mVO_zx#L?t(a8;`HV(lB=PWQFv z@K3b5nzZBm-wf3^(wc46>$P*cYmD#cZ{3!)w08BD1OGNUUtF%Ru{{5Bzs_#8H|d6P zQ#Uj}v@4NK+VQg=G+OLd(Rt(51s|=tuNl_Oe>U7X{bS3Nm+l?9uQxL?h}4#x{5$E1 z!d6fkNqwrQdAVPpPszBzDPv;X&2tI=T(iG!D2Nlh@=~Ym_nrQ*L;EHs?fAay(zX-7 z>pOy9c>N9o#ryJzKN&LVn=;*}%XV58HoW(}aj-ys7bM?XTiLsFNVtBt(C646%iFun z_ym9R%@g;&dsLo{+UTbs#c-fgk4f&%w^@R(BpGZnCvISIuX}5n-BR(Hr@YnW=DCLW zZr${?H%^^D6YDtl97-_cbZ=XC!EDFsTWg~d?`MIM!4-Zk_QJX1FWIM`WpUr!aU30p>Mq+hxc9=-DayI^XKfc z_m{YShh;bZeayA-*g^}p&5k$cExOQI@ZkhXfb4ge=x%YK?(LV(K!5NEck)s}i)USp zy?gbv=gjMz=l@|br}g5SdI$bKKArY$S6Jdr7X1zZWqs_La^8J5ONW zQT{0!(!6|0JR+;vJr^2w&iLeeW8*mYdCy&@1SU(ZuzKXg(Z0%X@5NMYC6@YP-A`|ae=6St0nMjI}A z-7pX0`5tGTbJ$qi^|tk2J>@Hj_jTyLX%M#QSSr2+tAI*{OU#bhM3b`-dJg4ux*^N%4 zO$&sN8J-P4^|8C4V@F%Sq4m>}|He=M#S_}hXYt!nHplbIz4MYRs}4S3EA7y25D+;&0AC#l;7? z>g;_M88NTS6a4MA>UW&>HbvVx`HFj%*DD!Th#5Ek*`oEWa@SLK27#t!6$*#0XvQvm z^Wu%2(*MM=|I$T0k9(8dDlTq_nlN)^g5>8f&TM50E*fmQJT@g3$L|;|j+*;Fqq_Nz zfSma+8{lF>X8QI=BI^4NhV7}p zxZKx$(kI=59Cwa8*DmbLKA6ycv-KNa>9owpHV=XgF10643eHzHc@n3+ZR6W4Hkrs= zxz!;X&tJ|pWH^x1RQRe!E7kt{F;(Y-{rheN1(e;R@xHsPq|)x3#Z-A3wvZ&2X`KjU#h|zhWy8}ofPkKy;D*@TkKZL~y;6e`cvI1%zVv!X`4+P^XcPXVhWef6-e6`SMhM`f$0x6O?mqD zRu|*X_)``JC;ZR;+N!lJ`SnNMO}rnzE?Av$YF(g6>YBi%_o_@=4s6R`{6wK=3Ga)L{{dPD3&L(ydG@InSKY5>adWciTvvN*$=eKP z18J9>$)*P`@@R91_7|mI*XT03rV*LHR>~@AYm<7IBpnM-Pr`uQ-doylGF?^kX>?nQ<@PCA}?|heyPKxaB0 z6(cvwXUA{LE;=KBJok>|{;AmfcJ3!=VauJ13vBN==uF_U%FaFnS=53-R%}q2eb zycX*TOt*h~hqd$u)4_+le(5{ve`K7IyZ7Plfyh#e#Rm=7{o0+On&Vi5USR`HwxtF2(=6lO?w*9&?_aC(O{hD2y5U{;t9lL++kNs=Px-P%kvVEDN#%;I6 zw`a0AWJ`O?7=y(zoqCYf#U(HHVGEoH>G1(?p^E%O8@*-YpJXLgnbGYFIpZjc72#+c3v#H zK!xkR=zGS#o~r_LdABVnc3{3Bb(gERRl4EGs)J%Y9T$FXyVcb9D|M2D3EQ>@HTmB< z#QKgY?me4a%=)eRP~gUS#xFP=rT3Lbn%#)d==L=;Y9vb28QEpGtBF?UwpZ9%r>!I>|9zO*Tu-5 zHsNgvU!uFy=d*siao4I*Xq&?iiBq>4^k+CGi6wr{+sb{m^djEv5@pP0|Of2(fu%_hdfQbC&+1{S^f-Xc0T z(8oQ$dg_+#q0Rx@dJo;2c~wBGv{!fQwW(XBBoZ#^8YZ3hkBzD3+a|-H(B{k%w)d^+ z>Lu?|IXeIBVq>^*{63dWLHE<|2l^sssIKSac=9n%^K@s9qxweGquvrXnEu|Yd6;0Z zdqe8goSUx>>*Y_~q?64(?ZjR;LB>jfiqwBA1j6#(iPpPvpHDH>x~&oVN@!R3vCrB; z(uI~hc}}hGykC}j;M@22EB{EGWoD?!)Mx${wf@A%)4BHQ z$BX~>d*rqC-29$d-+E)=)W{-t<-T*=@oY!LnVz37JY;x^`{t{<2aPw1uD@I*ap$`A zlN&b{`5APd{>6FV-_d!x8(B}C;Y;MUx)6S%P?P)jiJN^=-zTgT=eR9ldg0|Eu|IMv z#Mi{wDD7;QzwWZ_!2H8a9dT!F3NbF))AlP-^K#nNE!X9)sD?yOW2rc9z)-VNX1{O4 zThsi6Bpa`Fx@t zvy};tOI>W=&&PY~7t1WRyLMoA1W!|^?LF)5qS3qz8)nUBe7N%M*1JmIn@*k#R*8#r z{9mwMV!!HC`Nf$G4E+B&BtB}bbiaA{dHBOZ*+##+_gd6TWCR}`51Dd7F+t?-#>c7q zgyl9Km3GMZ@W|C{LEwS#H(3b*p_`X(yVHB0!|>wU(#-+<`ujY@Hx}@zY`c=Cy29M% z#y2b7JN~&U`#Mgp%}Jb_`tWvQORh$Lf&G+>x=h0j8|#Z*Uloa2edgF@s&w2j|A_dK zJg?OlBIz=Gq;&fYjy@L76IhEd98zoK{SbBo@_x=Ke@KNA$%d#x{a`YziGTQ7@x z7u-6q>}(fr(KacDl44aZ8@ekSzBQE$e4l@OTD2;h;el-CMUmy6cQhI%2hU38KGnI--90LsBW+>gLGIHBn(M9n zp516_bTMgmcTYQ=a)|rey~n9~5>Mt{(vCKmurXzN;a)a|pEpY;ZrHov^@Ck1Va#)O z&1t=F&a~R%o7C!CVWMo$zs(A0+#B5yn9VR#BCoDlbnj}jw<^!;3RG|5KN54maXHA24*8ZMHgJV7@LJ!`AO-gnZx4nzBQ@?f0Hle&vnPusR9b7B$aD=y>c=s1;< zgdfY89&h>c^v0)MuBTLQeA|$k`o%NG$D;JyDY5hVEXJI(*Q=kOEZHnDTkq2C{^`07 z%-NT9pImvWGiO&~LP7dv$RCix$K_i858AwZZ{W->i)G? zO0{%O$w~A+YP|JnuA0t8_XLxi+=C1{kKC8&s4H8fxcRJRI3{MIlvB`Fe|}v@gzM^n z<$M7*A01tGg6-hPSHjgx&18kAh1jlYHVB>`)iLv6g~O>YKKE<(97(-+AYJ6f?pBGd zY!f;kJ~NNVW@nfm@KSfb!nv=WSMUA2Z2abm{ylxRrmCBpqFScfofcze*!GFDUeoQN z{Kdsb_8Q!I9{TjS2QR~gN`nti_bz(i_HgsOp68m@X&av^G<0RmEnS|P#k`cAo1S zcRAkp=Tg1+>)Jlv>=I9l88>3K|F1ZBddZp9k?_cNu`kvl7@TRy9$9@nW@ zXLQXjWnqu*J)!qJhhBEnHwprgxdlyZwnq?>zy{Krq52-Uu(3@B{5aMv$(doVBZwbq`G%V z{k!Ii3o{x%hMp4pW7iW{o%&zL?(+NfIUhGvx_;V_xn@TGsY8`olQ!Mwx-G*q+brmd z^uyU4X0VTWrOF#7VfV^TzVyg; zJ5{Hh&tIbU`ZmKs^VW}bnwQ(T-rRNSiDEu+1XKW;GB;$Hu6P>Ral|6m(Bf-%g?>+* z`^SJj-o1y!n%nONC2G&<&`kcE6Z9xf;@#&}pOTlbG5n~y@9g5baBfnK=S@b}bM>)} zK2a$-olUDnu9tNfp1m|h_hR9xK=+VE%G343w<(+uQhwbSf7$j>W#6Wwvpa9^QHsp6 zW9V>yU}%|}w)D;Rt~vXn`l?;ewRC(<#Bd#hSupF-(z&aqJ-njdche~6pYxL&7qqW! zygk`aKjPm{mRs%D*6wIUif4;W!nEO)u4|%fmPgZKTdGfh)eKz^eqTb~H(VO;6VE3uI)vFIx?w#_| z*40iVfvEKbUEXj zNqC9Yy6;=AE&=87U!gw@^Ew$Fwy=JXOm+XFJonu3e9d5W3&w*HyYD=eb)J2&vLe@e zi~asG1taZWy>?4;DzE$tG(0tJMV?&Xwu<{}c0`~0V{@?Zk`Sv=g}Vmx1R6gwf*J#G4Xt( zcrr@@L(1i4Q+J5BWiLO~U+B%lut@T5^c2~N?JsXHIa9vFXRZrl=q6d-%c&22;`4*I zRe0YuHoI?{^dfYYhQXw>>%%W5JV`z!CL_GbM_ohclheCRo3G|1+}iqd;hXOwrBWA9 zPJTb7;`FNBjq7e$N5NCui|t!7)m7i6PuUl1v7CRY)fT38pRR4 zx@X>mhm&FP`FNTH!-9uNexp^^Zu-zQd`nRBnmKSBtRo_4Z`O$KO1(;hc8% z(H~u#SWh?0DNk2ZJZ7&Q9kHk5);ycf4Qbc^F7+!p_;N+a>M4KcEy{OW@XmJmofC%K z3DIk-kFH93pJABAs~~*%Z2pG4r;G=h4_Ve;KXgObQg6TODWkRwhq(vJdm0-j`0~4I ztK6*(gk^#|Ld*;-t3J+{qcuhL-kO}Y+6QdLg>U-ne5UBu><%{d~@}NBFL>*b0!7?>yH|lm@wZ(fy0d=1$o$ zKkyS%lYx_WG;it$0_eguc)!lF99K$I^)fo75{bXmp)SA{Wq^lkNpa#bx`E( zL!WPnHC$zxZMGDYMnUz^j+e{~Vn(75HgCAq_o8^~j}s^7s7ra;6igIfeO&oadgbIf zR-HB<8NYv%-y+XDtC#q!7p;xpj?Ocy4TY7T zg~99$D{AIses@@IX1PVfdeP?f<);n(T~2ga9pU-zo%vd2xnIPC=`$~1|2?Vd;YTC! zKPQ&YT)pY&=d$^U#&_i|vU%S;R3LvrG--}p#?Fb~*nMY(U08VQlcN5gqU-9VTV;OR zDz3OBbx-lHb_loXTUph%-!-Z^$vb}Xiq+0v;$y8@Xs;|{X>K1M5|w7vG9|tF&bOrdHX#j0GFEJpIxI5Anm&3=UiGcQoGIc9uJ-j)|is z<+54ljr>+!wrfAG>7v@G!o^=nXEtKqeC*cdi%u>XF+asE{4)m|I;WmWv*6&B6k>-PG${<@0^46}+<)@mm17dzCScHr-= z?DiEG?}4VC!Vj5&Du;{TJx^_!EcAV|n~sp&kB%uX*V^{4`Xboj+BnnLj=yK|RslcX zg*`kRX2)_e0!uDwJ#DB^G|SkfmwGDzo|aGZFg)OXpf_L3deZ`<3zFieUEN-ADD7{W zd3lXypt^;b(A?JihqGAxI!{f@w~T#ge28D+*85#|*%-20sy>8e8*psS+Lyd9rIN8-tF(xt`Z7*>5_Vd>DBQ3|{Mf3OuAB<<6F!@a*l) z3rg1tx7XR(7O|AS?$s>R=lNal^rJbqe3qNUm_g;n0zO(fuSa7exH!UMw4HXW#LVCa!ET)eR| z%R1|UcHz;lp04teOp}dcH~H0hC7u7nW04p9W@n~kbewVgivp{_iz}o4{IE`CIPfa* zMY^B%x8#D^u~}CW0&dFARtP$J?_$!9^UxT)%f-xaCg7%z#2)`s*?vLaTrOHx`yboG zAkKc^Q#iFn|s@Ej~*#c*KLcYo_&m6mdXmAh4}ge~9I&X>|kbw4so^4*$S znpWr)sgoO^I@@_vqU`ag2s#i$258wBrh>C?TK$W%*kr+U@p{fZQ^RHZg{!v9H}dFTUAXuXsnFnXlh$?`l|K zDPhHMg3B*^Ye&EfjpbiX@wHWdp4;{9(*D_3PS&1IVVzNXeNOxXhRWJ~4I6%3S(40c z#kozywE6F=s2qkHQMZL67vv=N{OMIVCdSOba^pj(jPVEbdw5Y&aMO3Pd;-r=3Z;d+@C$$Uby>xU+Uu%_tqk1pqb1K8f&aLw#%pUZcz{V zViW&vgW{}~b&?mVbC39F-G@1PhbD7FWZ)HRgSlJ*Wd?Uk3}h?yHQWuMybXXWgJVcJl_3b|0%I!C+>43^T$SgzTFd_yM^(+G&3{v?y9>=NB9yKtlzv%#On_C;o7*+x}Moq3=NC; zzBjXHT1&h;Tdu|R<^yA}`V3iDK9h3qne}epA0#n_X)L?AWTUJ0X4#8gUuv1|zBF?C z=O&Q+g`q)q-eQ38{%l??JJcGl< z?hmDqm_PKU^W*nrev3ZOw+PSvoF_P~UoxD3mhZ;vXS@lRt27xZV<#=^r-ZgMr=k1m`K|Em!Q#p7|>UJRAgWL5DWX4=CGk)9l>s z+cwubKB}gAm%L%MKNgjrZL{X$U%R-Qhe|w)8vB$sm^SQLGlRRB|K?Nc zDY9!GvY4GWUBp;3Mfc+2FWy1VZYt})ICEz2scEw=Oica0H2p!v;^c>i87F?!OXbc^ zIcMte+sS#a%OQp{*$cj=FHet+Ut-b3Z*sF_^Mwm_&)hBvpGY^8e<`qU)<;9lgs zf1EMZT;G_WUM-%rPb0b$JVsT~mwW4C(t9q@H3;vH3$s-OPO+O4`rh*Qy=?#f7bz1sK*9&DM z-6mm`$Nf`vBNN>^|EA43aG)dJ+@V0qd3jU5YK5`4%+r)TQ8$H-Hf^Z}8k(a$eiU1Yr}xd`2ZGxxSB3+8->{C1V|K!V?y(^UQIPhPB@9 z{hAv}azDQbyt1>#s?g`KR+!^y0~v{yRYy{hnw$j8YO?u4AkX{`H~YaIC=i2@AFHYn62`& z-B&LX<@w&d=#<&A`4(~SHibTCaP6DfEh@J5$X|y*+eUMbO5f>IZ08+*bLQ4sksrE- z>(pL7`Kz^;_4HS>zZ|X`xi=ijxa{wBhdcQ)qr(>QnRDN|9Qvp7c=9Q}7n{u*58Ymz zdv!kd^Y$sa6T@}w&Ln?+E7*OAZC}Q#RPTqAr}4NPnC0tuD`Tcu`m5Y~wn?&QO^o9L z_2Z8Jkkh=JS$V@`BG;6^9Li@RcF%ou)ciz1$&*v}Ufh-3=qhg89`SD5%dNRS7u|Cl z3imUIXiwfI-}$?7alT2z;p5ldn3_JEd(5s)$lJfnF8N!o&k?!9|IR*2_UKC0oWh&0jTS|7 zos!efxW=<>)1CY3ifbYamPJTA&ULuxr8_TtcHNRi4sm;B%@&)3a^R8Qk_l(KW25;# zE8TN7R_0O5k(6|u_?O$WQCm z^O|-I=8yaJMRb{&TkN+!y&|G}daja%Wb~pFEk7Kq*xfQdEaEHqGEb;$+Gg~BN| zT3UOPxu@-r=nzdfeVRkL?$}G7-O1i6IMm>vCy%1n>@oeR{!f3+a7%HJ$G-P_or`)vQ~U- zRvp1{eYz9(@QJw}&aqO{zqW7p{^uv8RrX%H(a+!El5k$!yVvaftPgh^{k`RQtnNkui5(T&;Wcd?}MC`ALi4em`y4#$t7b|CI~l^`jfzPj6hSmJ^(E zaQBZiODp$Bb1Ob9Xn$g{W9{0g3;~@A5AIJ~yP|!6@cxpm2O1=8 zWOM@4XQ(c|mzDeRjP#N9lkKNGHV&NakZGr%EEBTTV1moO$b|P_oBsve<-V1_M_Tv% z#Am_mf0v!=zi7g6peWg7_3VS)y|$VqkA4)spPcN>eg5Z}j32XBaFxE9@pY?E^@{Te z8yef0`7-n;KAsdQ!kEKme`Zl&Y0gUB>)9Uy9xQZk?pqq(^Q+$d(T|@}B@)Y@`W+Wr z`+E86yau+{{!f1FPBauMT9xnN>caHnjk@_5hkuq%NxhoWd8ImRu4PM7Li^sQm8J}v>!uuhB;ixXex$C%M|bn(xDR@+`*a!j zR;l0a*eg(@m{s|;;b8FA&+SaNv%2;jv0&no46n+!g=C* zu2SO{aegYFvgH^YUX(0WR^R)8UGG~fvvpdcszQ-k4r}iNRnTIFgK_rRH?BABI#%X- zO>B)JS}!e>$=niBh#m$8|@$aT;mQ4zOYeAz;{be*THuu z-!DkYzs9J?d@8r-$m$zm!uJ-xIpxhA@y?GoVa=JpcS^Lrb~C>1U{t6p|6z6GPww0` z3s01{LPM>Tmm%${%*Ra$E5-Xv&Hj6GTBjve7U@R(TU@=8Gur0(I@5%e_ofAIR(*VN z4(sptq4G@!#B_hut%=n<`1MO<Ojw}c`*kX%zmvc z(8_pqMChB-ja`SXMNd&@ULAkKY0b}25wBZe!eO?8@0T3^Ch5axa3XsjyKb?)`P`Jh zhaPTD^{-*uZ++|XhI3PRvo$`&F3e-Qd%Dx5ZZ+uObZ?#SUvjwrESH&kt}AkX*rmS6 z8GBRaywtg&yw-9_YOdqlJx;&2s|D!K2yAP#*4lQ!`06V zW=*(x|GI)LTSekr2i|QtiWRZx;Y|XK>pon{+aT}$t}1-rhkb`;u8N2Z`XnP`Dq*qZ z2KU3*`bjeL-|wIB_?1{%=jY#!TaUqNSNDG#WNNcM^t~?U z`Py|~lrJXyNyWGNw}EFTgQ7s{wfJf`3(K!39OqAds!+CW`}b29^PfI>8h`oRu^U|c zEs-p7ITj3y5;xf!G$}pUV{7&Ig~kad*)oTWj1BjHsaaZyzB(tdZNbE27maFUd84Ji zRfWrLif0N|9|xV@%@cU{w62&U3bmez*@@NZeSsE$dY%n`CZ1x z^@|VJyYzi6jJkJr)j=`clDZi;7b+iI;9k!3Lgd5y8H^%xQzDD+yC};Pao%=weCB&XXcqRvwqJhUj1&}*6W+Uxxckq{_e}KgUr*b zzuo)##;SBx+1viDhV9Jz&)?$3jd5d?c90Zc600bH`c4K2%L+Vzt!iP`xMiKwq@B3 z=6CkrvGkGVKK?E(^>6#x+zF0r*7$2zi2d1L6tlO@GXDIew(N&T;<9JfE@eCaHNk0u zybs61-^t6ZHpllo-S2ox@W0XAo8`LEHWAwsxaZ$24_m1?XQy5E%s2fS*-q7Ot|{bK zaGp42H}Tzc(KT!KOSUf#c8-XgAE?lAQz@-`>D-E|XZ8g(S63J)&YHQqtjxX6@ZtJh zrF)d`%i%Hc#m+OQ>?R6)+B%!@>+;Ew(xxqpUnM!27F?WueQxF48MRBxwSCATUTyP^agA}uwc8T%XiM!g~vmAR@jHV&WN{_I@Iv1ht1hVf5M)9 zldsxVvTj~~MPb@^#aZIVtiLfZc~_{H_}6WGv(9Y7(Rnr5m)NzFz9z4H-k}k{*t0fi z?oX+HiO0?fE8bYI7Wq^q_|@ycZ}rK)7VMd|S6?FHUsrG07JvVU>AkA6)-Jf$zT<6Y zJMWOIqx? zYgwEoZMzouncnyH|Lgv(LE+OQ=0opY7c8}lzO#5kIEzyBwRbU`0?&_rI`w16ljE_< zCRc3k>36Qbq9Dv{o<4V5Soz71Po_mJst}I4BXaJ6c>Xv2$;m9Mg??J=LoA=cE*G$S|M3?3(fZih^+R+mkIH3S2FG z`AtNg*`$B_?WEbh{V3}s-xQn8`DzwvRKK#X143{)gdqbWS0onYrhcvaKPZWV(z15yPv%f{p44F0iqw#dFy+*T~Hwizq z{%FP@eP;DZ5Brd$9-Emj%)ZYkUv;}Pp2P5s?Yqe;fBR~jJJj9sQ zFYjkDn*h7EHzoU8&%M~cLiC=*jLi#9ORSBbR#qa(o*Pha^QS~|?=H{%qHC=W9sH-T zGW=Ll-C?f8CiUcZzvfSwShwPcM6Hpb^OTR$MN{+K>n=X}l(r>l#evyRmL8ROE1i^I zQ(e6wDZZk6@6Ri{G|f$Q3_q(SHm=e;W&CsfC9eE8cM{5-V~=InYFgg7b7r>r($f|h zu77R!$f`w`1lMU#(>wC~zuz9Cl9{$uyMEY&hs@)dt00*C<>Z%pF3QQCVKzP^$aa((Sm?v0f@ z=X|^M=||I(r(a%7`FitF)Zxm6KS_^7_qs*hxEnnEx^m9ivknKW=GNJ@8(TRl9eXjk zGtceVLTlDr6YqQO*OS?DjejqL_2*i}<4={M{O`~7H`%b^=*DaO@6Ig0=DTBM;?Iff zY71W4grt9nUpw{QyoLWOk1n1x-PkK%T>6Z?)z__G(|^t9j3`=uI_v7n*NZQ&n`$2N zxshvq_EX);W2;Px_j$GXP3SQEmGa2(u(LeKl9LYfwzB`ed}`e z;|tql;`_yF<9{{Y4{5snM+D~UrB7Lt_s{>-$IZd<(G`Zt&WaTWOk&mB{9@(jmaWxm zDc*hPwUpwtj^m$I&KC!O?AUnpWXc&vi?V0sLa&3^t%9V4lf5B!{9JbIUWR(X=6t#P zwhRBK@qTEFeV}8Iv+{J5_8QaeGx|#VvQK9l>J`LAmg*t9xtA> zeE!sh!YWBUb~EpF-uW!R5E8ZNbFB5(xtcY67vu7?DrSG)!)Vh`vTu>!ll|Vzdp3r8 zbp$Uw9i_d;Amf6vLG8QR`dw?9*{;ZcVtVkW;YsSXS215DHILbcRlJ=l{*nLd5o6Ei zGvPMOKI#5M>u%qlEL+7n>2Gd)cY0Xd@vSQO_p4AjwrR1iY(lLj{@3IE^litloI`h+ zmjp%#r8QbI2G1&vjlTEyXCKsf)$$j+4rTXWc<|{5=gH4HQ=sn8$&}jRzwXl86YIrZ ztFf%Vwefu`C<;Co}==&Y0aWT*{kJ5_+Ev7wLW)&Q^?S5p3j?rBMz%8{B{fPpSEGO z{NvBo-}VOlkXdAV_ln-LDKcl&uKk>~W#Rv;QZIV%F5fVDq2A`u|6VyI{D*h0*FB0YLPAP5+iwdRf0s)A~r8xSek0E!&vr9p%QGrf+Nd7BD~V@1%kcwbv@vUfnOs zmlTw8-pgOo%2A0;iz9fe(-9S)Db8o=U%O9f>OL_icK4z8FZAyVJ>I|m!`-TR-d^e= zsZkF^qxlfCa9&~X#p5roas8iNFWHG;?^72zh2~DukAEeu@_mBj_5c1yBtAZ!ex!rV!5U)S%GZ~TgLkih+Qu8ZPE6v_ zl}zuXy{0*-jo&|9%;dkd;NQwUrZ3mBS4dBNP-k2Z_G{e0!?!4}0kS4w`a z-g4;wv;C)g_e3Ud`n)WacaxdW?`cn7s=a;N_%heSxV@mfrJjT9R|}I!z#8und^T+x~wt` zRMrPp7hZW0ek46EX|wQtpB~fG4E_(RuLU1iZkk=UWzD;XoNtSBYj0HUSNC-}@ZRtF z>q~6^+B2Q=U#!}`g0u59Q@dhWv)}AjQ~1vW{SB_1eAP~+@|WlGHRm%nd}7w$^y`Oi zobFwrK2hmKk)WLAcKT@G;^&;{d?g!{_fL~B-R)%P_YjnK7X{zDvgeo0q$SA-O_R@E zU(@@C|6A5Dw4o>{+y zYkkzc;)2ra|CfNn?(3r!7o%I6&wqPnE}DI@IsYA`^!KX_*1pQVySb8QrpS(k|F7P< zAboqE_-(z{TcT#PFLXRV@#fSy8UEL&UAuccNA3O;8{3jozqWk&r+7*}Y{}m>GS9OT z){EE9S){dR*&*X6`?;Uno%~z^F0wQBT4Wt@N@Chlw=X-rF1U7o!M+X2_qkioD_TAE zT^l0*XPJTY?c}dZ4)Fe6f9=>kJL~0fpY~2#KKIDCVtbccdfXR@xy64m+!UwDp`FnNS zRCnFKvd1v0Vf$sB1)iSjU+%9Fu1LQ4;h)o|E%T$6>{#|~&ikLYyq=!zbn>{XdDnNP z|8%WARkp=1ey!iDHw#?1=TsU={@)S2f5x3d7F!;aK}_9y|H{6C6DKq0iBu&0_qCd6 z?$Wf`qx`6|gG~2vRsH_V_t(Q#xwn4KvRt;)^J`{UYq*#<)1O68UbbsML$%75;r_ja z@h7^T+ZO&1g1U9@{VN{=v|JzL%z_%bJ%0bG#g#i_mRGj^%3(@=w##PO-uUR12HW21 zW^4QUU41HBa_pDM>IYwa4>>BwE|*^hO6`);q7e9I=-Ii?asp6OA zs&C%k^2_Ap$124+XLtQm2Nj_b(R0s;b3ea!?oVC+k1cPw&)zs>p0de#{<5-npO@Xa za-(I-lvQVQ&pnCf7W`o}_ky(a{rXuS_A(2&$4!6ya#>9DN-t+Wp4fAa)+_wC%B=V2 zVh>a*{T283$kMn)rqabcen~l{M)t~c!rz?Gk9vFK_sqtnj-P+r@2hdvX^J#qIQByA z?h?b_cNS*_J&^qCzo_@4aMkaV$=i5*_N44)w{!kfWOH{(cD7KH($@v2Ei>4^t`z(D z%j#iGU{6uliC3k&%-ndECFK+v?UkJPAj!SXUfr%c*P#4~bkf8VewCYJmd^dXC)508 zf46^O{CDHu?R6d;>fh4VeoS}P{qOtfkLukm+1Z!gdsy%4X)X(zuO%60_~lt+TTa+H zu9?^G<(x5CaTL_WX?w=%Y@s82voZe19%Z`>8J$mW50q<_yq@*ujQZ7cwfDBnF7e%w zP#*MP=3i$Mq34}@|Nc`Z`oixKLixZ`InYT z-fZyWy3<(S^L_T58@E}F!kQMOTh)8sD_?S1uH@0L>Lh0sp1J=Q$X;Ep@pS51CdTbY zPwWqp*kwGkSF?GVgtDJn9Yg+Pqk84=N>E?>&x|P_`GeScnTuziF#hqac>P+>oX;xUbH6W`d-ZteqpRHo z+*MM|sv8+XLwOl`LMsY?MsEw7^W~KHUu(-U^=a1>{S%Dt{t2*cjIa3f*1Oi_)LtRY z4pxV92J6Q6Vzp5mN7R#y6U09M&5UBLS=7EH`}UbI!TADp9vlZxHTdW3nfUb*kH9IH zo6mOK7X2{o?~S>?7Kr|T9A2qgpwsgHX!(-Q-#Yer#y7gXyX>kPTl^<^ALrY`Nrl(H zPj7M9lUwPV?B6Q%-E-b~VVeWp<+%&>R$P7*W#RnScB)*9rqz4LC3XVygzudayTZO- zb+6*ZG_O6EUVomnf7+iaxrN_363wPb9AUB1@zm^PohMY2SfRbq@5Q`7b*@~yP2XNA zcY7pkJ?*diN9M|kn#jJ#3vcd}Sa{^Qb5Tw}m9~*Y#+F*AzWn~1hFYzAG!ApK9c@Uj zsMlD@FSVdv<#^osFoX7I_fLPElQDnZhXYkUeP1hrUO9?vj^1?V&8yxbo#(f^r=+WV zEL)YmOGHZM%9_9KJry#W*{=we)_&pns{fKVsQp}dK#=^OZ+Wq=W?zq7<(Pk`m0%B=OteNf7=dE=1>JLP@;yY|g~wIg|R`nT>%?iY`8;#OCt-M^}0=RR?6x8SXb z=Ru>7t8X6oZ(#Oe*?IZNriuK{u2!;_1 zcww|v8^sC^#E|WBpcAF)Mvvb_bh7-?z{p z%@Ey1A70w1VzYBT7ry|@-9!IaBQ`~IU0L#3-=)vcspCZYS}~78dAZK*cMTrYY`p)g z^-1Q|4JRkD3F?TyleKzkWj7fZI#z|p#P4^$ueykF!s6PgEs|H{ zcDyK^Hz~~ULkz>)wMztjLM1}wF4)a%oABfRP4B(Vea^jWGxE7rk4vnzpJMdxu=2-k z(rk0|OG2YxluQ$N=zn>Wvv%@%UIv%Xb58wS@#~CIHiLqj{k7CmF=mzzEAzMYGB0~`cfsc=EbZxtm@;$g{kJ=XhWyhab{n=o?CAsnGk@@?t>=yc; zec_&%SD#_fi-ju=)>iPW`X9hHXFb0sU)_JJxvH;!ot+kMB_~;($B=MR^V43>dWREI z3@b0xwy}TkUv4S#@(!{U74-_b$QT!brU}cHT{hIt?IA(Rlh&%ycpzvWNO+H zUItzLKHDVK1b3&o=~FJScHa2hW4-8;#n*({>JMcWd7tO~6e4iWk+ka?86}!TLWr z12bj&dxFgvqP}PUFt*I&3hTbjAQ7gh^yXHJ`8=T=2Vd-ASKTLhUPyVqvYWSUA#-vC zFGK$K4YwyPm)CuL+^lf_iZ6b)AKXqJ;16D&ADFr5ip#U@f%n|>UaX5A?z75g4Jp8PEHhn=GhNR_QbHiTmEPkbWv-DL(%A8G%3=iYJ^mzZBBqRMe zUijR7@7iXeIa?VGIA{Nzva5e%Ou%#RT4TK5J|mHZ9WxbDO!_UEi=V)%k@~J&*Cx+B?-=t#V%2)NK(*1=`OUuMTLx zbS~F4KXvP&7F+(Dr>j0M`FDN7)Y+>F`l3RA?K632yJFG59m_V?Xn$!a;PZYPHc#Ko zQNj5&ZS=XTn_eHLC(z zKNNh9`g=nx^torPanyYa$+cfooMqye84jt4vR%u6^<{C&*%@;W{Mh6-e_pljmQ9lV zp7Zv({*cQ$Al-59wo?A0L^I2)mg18OYBwvkcFIq&iSK^L!T$Vk@AkkI%=eCYeO7ie z2`H3#Z+)y{|GDCTgY{bLYvykIE@7%=%4Pb_R^#Kbi0ay_hRxI8^D*j0 z$ut9g*XGY_FPB!XIdQmP|6xz$CHTee^KKMGx@Jx1o z(4*dmW(?9T)mq;?Hb&T_gwNB@_7mE*qtwd#T6xb7eyiB&$GdEJzFW`Pvgi*d&-1X_ z``lmJJ0(Izvn~i9I{4$6QthMJ3s38JUX1cTleJC5l;OkUPsgI8t4*?=Y<;%5bn|4c zuLoTG-Le8&y}vS2X-7jJn6Y6=j@Ei2fHs+8iigv_V3YY?)bjoi1gy`<;%Y&ZalbkACyd))Z!} z{m!~~FK@ZB@j;%akrv`B_iSI@nbWaQAwNc4BtVw9>}#A9 zDt%0^9F*2OBwcfUc3LLWo5ho!N3J;d-%^@EXOrxYV9RBRJH9VqyyxZQJc;-1-)T=? zUav^nFZlWU@-MqAznYf#1@s?RIiFGT(}(9)^64$GVEr^L{^8loiTe(fua-FcD)Qyb zzZV*Qt6kAp|J&aB{z|P^2iuIQ%-(#wcKLDfHPc=Rv0ZB>?U9q1`)xs2@xs)zL4K7E zpKPMU!!sp)1qG#F1hQ&&1@Cg zoX!Sf@zZVII%?Z&blc&V&iRqg#MJxb>Zwm8d-djbK8#bICia3~*l-qqgd$tsr|vDH z`=XCZgq`;~AEzMVbL`T~>egx2k4+~npC1~?ziso(oifW6TyAWhvFqo?Ge?!A%~lDN zt=S?`B|I^CM>gBJK+O-6x8K`d>6Dl(yFshH@xGV0J_GkHqmN!EKc{4Us9(p%Q0L#X zW#OrU_m0|or#>+~us{6M7xuLuzSNyuxp&#x>FRgbiJ$>~i}Rk76PY{Y>4f!VtK(WS(cf=cUL54d3U5S0mW-Z?GzO z%{h1Y$Chsm)Z!9XhfysRG*$gi)G zdU}b)PYV#+4 zy(U=6?-meq+-Bv$|CUk=IcM#ws!IN_v{my*JzT3|786>dY;NFZrMH?mGEOC=9{gAe%S=@V``|HKUujV{r zRts(ZA8gJb(A=0fSGld4|J9eUOR&5=)`tVqjQ^{JvgJfeP-m| zq*maQlAi!gX>8|nY_(s+KALu_dV9YTvEZ(UEeGLD@VEjZsxcPsu z87LChY&=%*-cj~m))&K#jyBwL%_c+Qa22FpiJm?`tMQ+K;NqVReH)H6C-$+zqRzJZ z?wWTJwT|M444dy}gkF0b_)sLhEWWdG-AR+V+uiowSg=#*@pn~!>(!rPDh?Id^6$z0 zyW-#SM{y@luMJC=-SbfL!CKF>gPR!ERnNT_+Fbw5fz2xSy`kE|<)@kUhUQ50KfKp$ zwP)&)X^$FLwJpQtD3=O}ir3wnc_S8Z-Cvv3vnqXcdQYQ~+!UMIwR_g(vHZU9(m3=Q z^X+^igL~^PXID->Thha{e)p#hmY$B{!AZ>J!i!H82Tr;kap;qwnILzVnN&T6>it)i&Z^s2XbL)f-?`1cwS}hoTGO^(t>gwjE!j1!-4Yd%8SofxXX>47&x2XE@x&wI3Ir{45jlaCc~r z;8&X!7dM;ty({w;KeFrp>gg{+uN|Hpnt$wBSkmUZkq5Z9nY^=|wESvPcK^XxPzh71 zm-S`Ime27=XU-2;En0eD=f@rHpKNl1WE@sR&fT|y?f>#Dh7G3w%T@emcRjq41wYhA%N_;1;W}?w1KkL=UlU?Q7e#`pZ+Vv*{ToIpT_i54lU*5r= zm$zm04p0%C{c2}GdwTw&DU$a#E#OX3+2j4`%QC4~2c@L9tNO1#nz!gp@AvY{E}YK< zCJCL|azMJ%Z+F1qof@3I55qnzTX{P3+9|6&p%zwEs!op88`c$u^ZxU{$WXDiV3Y6J zDH88&R~(G)ji|`=k-3(+(#mGzT$lOEl~KB8H!tOGD5+R;65OnTC%-N03NNicT`2oC zx#&&QA?X-S9?m(h&SncO{&~cxwBlHb#xdtf%k??)@AW)7bI+6a>xW$(rME)v%~tLH z@Vs~Fp_>hgO*M$bx@wW_7Fp+$D{pW^Ou^q>ib-M zQ2&~(Vf&Vkp_7(hPulT)fmgoiE&)GHi>pqu^CG64oMp+LKU+xsf%)3Yx>p`|d*|(u&Umov@!>?OD?~u7{6J0-{Vcq>l z`SVsDwEwNkICtA~iJV7z$2OKmdGmai3wgQL?Z)H-Oy5)HUJGqk&(50^{oZB!`b*1w zerpGmf&y~Is?*R^SfceoxpLZ*)JhRCt2FK!A@j;rdYur&SG$=?p;r~ zS6tdwsXx_nrMIpnS4=MJzJ75kD4GvN_g>hLX7c{g&4(vvRYdGA@3{Iw|Jx4qIKH^j zL8jr=*ZrVkFT-x5&)j)?*}p4KTAp5YIaIED*-w_ zb3i4F$^3|AqVsOH?8x_;H?6bue|_YqE$gISA7nbV;q$Atj(WMAU7jgC*LHtdY7%&8 zxr+bnl^nVdDPbdPA1DLVB&U!-Eje4EMq%~fxPg^i^S zXZpmdo^4FY`mo=X`N7NMZ`78utrk1H_nU0~)=y#SC2LJGzZ|LBn-$g?ak_PN)l@0l zYtN8_rsHCbgXOswM`lap9lN%pm(5-;_Lw6m>Rzl|zEX5vB)8<0T}KtSx$ucZpNi`J zn((^oAh(MD|E;cF+5aLwZJ8!F*GOZNtm@JEUwaC(=h_87-udAPZ@j^(Lz||VY~T}k zy>?T=UL#+y>l#=M59j+W*X?|GuURI4Ym%)*?A`wXi8db+%R#>rP}ozSqo?ulwncSh=~Lv~A>lZZ+N4B~d!>oUW!69anxle~!)3J(HH7-BS8-bO8fV93YA2W|l*wh!5AIi8@OiOiY4U=^ zv__8Qk^j65?^I8CxYjja%+FUqE|_=TjdRN@Brjxcy~e`-$;N0A_ zlm!>4iC<+N%v{^Q{n&Q(M5i8C-=3Oa*{=s8^qH;i`o6qTw9#Ef|MKP2eK(ChY_nPV z@!IX(!U4ghbGk#sA2n;t3XaMzWjRpf7F#zp>{{c=>Kp4`ZM=~6;eF+$%)QAQPWDM; zMH?GV2GyFc@;lG|TQ3>4#LMQb;M>6N!gFj4aSVH_8RA~(shoEQ6$~ALp$pk66O#>n zHh9E?vLnc8lKg7N_&w&yUHm2yI{A)H>6OE;_s+Psr8;(%_@wDCJ4<<+GSjx1hcBqw zvG8%sl22-9A}>DP6Tx@j?w^*-RM{E3d z)%vG@pWV}|{FS9T?_0;W6-O;nnIEVY{oxacy|g<|cixo8pRe4mEV8>+;-Kah+A7tO z8!lO|sRiu{6i-Mm`@FF!O<~_IS3|#T%gbQNIm0CSZpF+ki}>d4nLW*Kw-@if-L|U7e=L5j^LxRg_lOe=kXAULy0xBpLiZ!QPrpNv|q{k*b-1h z+_Q?y^n~@tn`x$?d@m7p-f`aEyuT%BTg~IHi2Mp)y;<&i7w^5Ok3X+}We;DF?lC=d zak}#6gI5Ba4j*QU?tZmth1EIMHs^qJH(!32%ts4ue_ehxuwAOuJ}3I>gZ;OIi+b)} z45-&N`^R5$OYQB?8qarzQ_bbyYODBbxm_(*(X1D_X{ZM7WQ2sS2<&+i0dmHPzPqa% z=S6-m+m-%(@vF_9m)fjcORqiFdsy-M|5*zG(N9uSG$#96Y*{26R63_S`cd$1UE^#6 z-wlc_KC-1N0>2;L77B@j8?wA#{h$7Q_ReG8KF`kOIp>@fm3Q-oJ$$hxD(AY-+;fw@ z&ES$YH@xTnX-QIut^1@;?h|E`FERa1R01WBqbK^n_5Yh2d!=5?+{?Huzdxr_T5bA^ zPqVfxlG9NTw28bkaZis}q;*eY^;i3K8GSX^D)b`meCnLG<8_Yk6wVm#P~%I|uLD-U zYtLOR^{2ert8b&V)T{ZgERWw&1C4)qPMvpyEy!xFYW7Qurll`SLXIr{E?vuca&i}I zD3ew0vds&7Rg!qB4b+lSj<5P~SIe#JO&sr+p6wyWVjrDv%AcpQZ0jPqdL^^}3;q|f zF3Nvt6m=xe!2d;kOyC{onAHzs7Gz)CZMooJ*w+JoPu^~3_!scWW~S7u3yWax$+9>m zWq7ALV7d9W=WGXz+~@A|oI1}=%kss|OD}aI4`-MjY)%vG?|6LRhq#Kr7EieGhA$Dz z<6q=tD0fP{bG@nQR<;II>8BijcQxUBM_78{{0{=#=TF!(CvEKt_v}5tw_beYm49;9 z&gu z>Z>myz5KuD$;&~4{clpA+1H);O<*l`JbCN@O1?sOO%0 zZ^KlJ#aff*E$8yr5tzQ!afaCK{6l-sZRP2|w{B&-!6P?0-6Lxf6FAM^HCp*Ud-((w zEbF^VCT;`u3=Y4tys=q4=D1RC({?ZI*AHutYrdauliz>CMcMcI&y^P9OXE0V;`a*6 zsy}@8q%iuE&1S7v7Zzj24*%S3k)rc%$h6E7_3EfvS$KY;>S5*G;S1gz{xD~v`|0Rf z)5eAU;in2`mmkS;?D%9oX-a)jL4lInXU_E##3geJC$IScuB7rFNQ(REzMpDSKRxW) z;afjXCwb%_UHWdKRaUut^omC@f9C$^S@k^qNc*w66)T+kQ3C|j``u&d5_x8-Txi9n zdsDK%t5?>XnlOR2TidhJrJv=s=kL^wpADwgeeS6@lYel~`Zl`pe3!fOkazb}!&&NwqE>xKS$n}zIc)jn6x$Vr@fTmlX!dt_WNpk4 z2))96NTy4!uEy@b%87e~BHVjyZ%F_9W)Pu0yl;vhUk| zHF>R-$u8~YWf!HJU@H@6A8HIO-DN z9jPg;MPU9C@FwA$teZekfcHQ$2z`F_}aoPFqHbwm0cYq_a=?r5q06O?Bz zcK`5uTiT564SzJ)pPn^OGwi#s7^A(S`1JRwO~tn7r@jA>*1OPuLE(bS=Bpocp6zx# z-{QCW>m$prlb#)0@~NY3qg<2Q;?t*tgpSwPq`PIQ`27YVab zw&GetmBJPE#gOV=Dw6s}Od{Bre_Io*OOPs1}znfIL!d{MO8x#ehlP?4V7 zgsJfhuk!q`b6l_4&Dp46PH{*uX0B2_{WXw>OZm6ccg!C zzH~u$-=*WNGTWyfd1l1%v*Vqvj^;tD$65RryyPRy|JYd0W1ho3N4RHtBG10;REKtX zpI1kUB+j%PtvvAaK&4|&Yx>DT&k5&}S3SP5waGo_*yjeB@*9UM4{iOUd}-qz56sZemb=WzBvA4?nsILFkhm{Vo^YxDJdTf)x&m6P;gLTr3CyFbN^VT_E={h&RD%LqH zZ&4mw#pSpeH&pIUTe>QiXNB^GxGlRRpRAw$^208>dGqUM8CQD5C1~A|C=fpE!n;O( z9joz$cy(U$elPpU263hHZv-xtn8DC&x+TJH`RUhR86Pq>$!^>8aVuxVkKC0V)ps8< z^D4ZV8vB^}qjcWMjgAXW8-~7Q=NF98zIUej>|1YJ9lvG;%QEdS7MqyK9PYp7PcG>> z?|s|Fk%hl<{*-2uL`i`pIg{Sq>n!+hyiw8i@7w=&R)Xh_ypIJMdD*kHK1%a8i1K_` zw$~uhZ6E8lT~{+_?ve;_U$8f$)c8ca{^f_IwKn#vr5HH+8cMsA53q3@ZF4y2zp8y{ zO|x#}?EO2xmp1dyS-jrM{#|9}hsN0!zaL&}C=RSSA^Jx_YWh~6<)U-WyS_-*uq@Ml zv2xa_gNMZq7FRXs8wcH)dLiK3wFu)20lUIKeezjr!GC^QYGj7QRg(stRn2v(b&DA{ zG%?=WBJQ2Br%FfZvE{2I2ieANi>{tksNd_;e*NpmKP9FN7fRo}&A!7ecSC>Doj*E{ z)J2yWGxN{&UpT>0K}zNAx3WJ9R*y0^FtOa< zZdazQKQ&qMMd8P7j%yjOe`tO3wf+3GdxqRDn=U_1iz<}6bWE>G?po74!K$@BUVq$} z6Ihw|Wa>{7Fun44yUO|GmEDY`EVsJD%$xswHIj|He*E+0I}AT~Ll1~O;PnZApm5lK zmHgCQ>)ysa6WGEkFCaH%@yRM>F1rnB+to{oJL?6E+o|KG z?;ehZ?1R1P(yvYb*t&)KYUx`+|Jxj{9?iov~aZ)Z`S&a3qKpI?=U%jfP1pveWms4 zX@U>?Dw3-k6CyN!{}pR_zohM2z}*=?dZu%A&$3FovG&WMr{`WO9Wp-gSa=bWvg$tx zj^e)i5v$ic@X(xLbLL$C^XKxr+7c~Pd9nCIn&x!Fk3Nr>4?RyTj5ZcG`VmxriU{J{*T*osPp5D7pM8FFN?O>%Ml_$rtZAo<7W7`K;VIe5%0y$r9Pp43ll% zTj_p&wYMf^|3tUQ>*rn0Zg}!}(if@PdOc3(0MiFm7c0G!Y~m-CEa?+i=f3_p^Lc5j z>8B=qpUZS-`Q`hE9XsvW-j)A~P<>ea%^|N5?z**~L~CU0|IVYtpviVhg*JWc!vQ?kl;) zpRZ2asaR*Q^ydBUrS_fjF(q-#Opgzn$|;!i?U7!1oxvlNxz5M*{>9j$H`l&ykkiYs zd9i6r!8@<(Uu-8mx6Zgw$&hj4^&X$4Cwo7?D~V#=-(zdappgHr!`$$Su-t{7HG&>{ z9!90fw4IX**>z9dD4%sBM_oc;ze4GpmhFZ49t`>St@>4EE=MnHNuHs-MW~*~}s zz6VEVTW`tenDF_rYt!E=b51(!2IU=L6_1_vc zhMFq=J+}<_)v&SzPf!96+2`BQBEIx?SQ&MUCpKf7Pqm~lVvXKVc*sc+UYHsnRI?fO{y zOKLC6OwJd&F-yw#Bzk9ZznQcAve82JF!NkWa+^!UZwIiFuxU{F&L`z(ISABn9_{QS(~FBM}S z9sk^z=NVhqt9K;Lg_%KkVaYG4yUk}r6~tQ4MF*`kOh30FzEZBi`uG9Cdto+b3U4&U zAG;^T5NEEvYgy?psfXWBov1o5lb_KLvEbvX%SIcG3}P}G#jRu6gkRr1t@g&U)Tgx} zO73RLE0-PXt9JD6JR;A`|5!&}uxWE;fj0498YWN}M;%zan}_TKILS!_Hqd2|PCszuC4bB$Mev z;v@e@$7hEuKb<$R)XvR>L2PoRR4apq>ZYGMQx8Q8Fq_BEt$SN}U=Q!yeXjQc9KK!S z68&TJ?HWVFp6Lxcr>_Zm@i6(%$|o;v^Y7IzEUP#)tEJ4nZ+_%{PoB;fMl&R4lyCBQ zeI@YpZ@mMpPb&93=iH%MIj>cyzUS6&2C0W~97~pc-thTwo3OYxd(4D@3g!i=MXjnO z^NlZ98!Qj4D|np#XU&u2nSx5+4NI6G7|vB-Tx$_q)@@LB>WAIa$Ff$APX5>Tiyb>I z*=~662tV`P)4T47J@k#;yp3_gx5S{T>;pN8bC-wBFgR7w`sC&HIqh5T^FA?sRv3Kk zrFWfV_WsSD%F+yR;n}aB=zobbD#*X|()(V7?cVJVJu_4yd=AfAGkccbAD<&_(Py(a z2Dt0`c%&y7mEP)Zva0&bzCq`*%ptF-Kb#-m37U31ATvYI+t644*w2qu%kJrU%89G& z5mH;0k?Ll3B%)=y&!39P(G7b0L*st#VtjD+mf8LlY?W&-{g$rT(sRG`MY?E*99EZPi_fc@zP4SqBY`9@3ePi z9KzRhGAvp1*Q6h-nH-W7`|5IpMOB%O+@w7^x@IqbO?#5*U3%qE_tNiwlU^ixWq&R3 zd2lc6YVf5O{hSML+4AjOJFVyO{*rT3i!0PZ8e62hH~v_tUbja&`TdODVz29br5WxC z@zzY-btbHGukw5Engmuh=Fci6JNJHKYP8Y)`1tJW11#>bd225$I27Qv%30aO(o8t{ z?co;R3A0x0suW1fXqtHMK-%1oHV*e}81AH${+bkjjAQ+T?+q6+z2g+WoHEE#53Ab$ z(_O{CKH%VOfBDVn36*pFRv&%#-^Y1&P6uQD*xaV(?_>U6acS%KJnfYC`SXD3^MPAo zXEZN9E!PHx@sYzV8&hwl-MDHzpS>e-TKtF82B%+Lc)`YSWSaA}s(-(xI5uuiom&>x zY96-k`=UeB%wNs;|K?w_qIK*um2U?w_jX5$mZto2&SXEVwo4{l`_DtyeqMRTnlKr^ zu;b|;nLk{b^|a8_I7fvoo1tRkruO}@S*mt>I@O{-zdgb(ZOpPXtfG6*yLY!c95`KHGAY{we~z#e*b0fpS=?2o3u)nxmc{^j}zU; zx`y$=LR-G@rTuF?r5WBFXE*wini@~OciKoE#^_G=4YPU!%-E>J>diDMZHo8tLqBoeAnDHE# z<`k{p_U`A(HH+1^y)js-8gbr9_Mg|s(0@LkY(k#=$Y|Ib?O`)Zt3=K6-h{mgmWwlT z8G3dZ?pXDqkn0i~L-W>m*E{aIJQHrVpK5R~{_?MH4l+!;=3KP=rgE$!yRGokrt>v& z3T=^f2lWr@tagqzW!U3jw?_N^Z1Z55W2?U$d#0YSa=u@7sUovkOnXCtSa*b8c+$bk z86DXxHucThbMmopa7}ZX>951aBHCLdUfAmI`?2H}PpLwQ>e{_CuM6=q`1vv?bboft zpVcX^rjz_pZTFQ1!_zrtB}X?(DxVWX504jHbt-R73fKJ-7x_SO&1ENk^~|7s8kM(v zrrvueJp23!BiU%vhJ|jiXreK4Qay-_ukx?^;X}nH?Yob-mG9kO@$=)EXjOeMVb);+D&_{G}5Al|NoywEJmA^N%Gzo?m_>xKnHSqZ>Vf{KCghp0-2D ze>WK#BK%kO@tr)@v^qXRYR|^J=Ovq$onLic?F{c+A=~d(FPrpF8_g|^+fd)3m%p&h zYT2RB&pk8QpRas&({%MEc`31bweR#*&j&9(yTj_h<44WiqPoY=M6^n*VSJE#;L?|+ zUo}2%yt>Rh)T?HLys1NZ+_de%YqAAuls|5ElSr;<{_))G={@hI&zm~YLvG>yi0ZAS z%n!_ux|Q$Tyz%8>Icd|Z58u)@R`$x+rypE?CUtq+jl;RKBF`_17JGdw;CP6iTJWO8;gap3{0`S>gSyQhRKJb3R<_KOeb4pXcVwO**w1rxwhJg3md(PzDJS>U~ALit{Qdak|i=)Wg%QV7#&+)FNh%XrPkaAeoz<@Z(PK8dBR zNMGibFLqh_-}{&R>lVEJJoSbFL(;qIr{dLl(yt?`HtdgGce$$CqG8q2M%MZYy~+@C zlNn~t6%w8e9KkbZx*0|=KMk2McufKF_9*2)uN{zaunJ{_?>fe)+JU_{%cJ(Z^^J06d zlK7SXEW2?!^kc6@=wkD2uVtj=4!jSa{*d!FPjteK!&x#Rd;gkUxtMjtCxK~^sGi*O z-_f2{yr-^IiWU8-+`TyIQqAif|8rBWoMCfY%)jKqv#6)G5fRJ`Ne|5q-g4ud&B1)> z>vw^uZ9F|QUvFyn{F!iDy!o)p&Z$Ys@XF2&N6>?|`)L4= z&xXq{qo3$rX11z5xmtZ&_cPmH^5F@+JM<2RE;bh{>B!yGw$ox4kN&8Z1LHy!=% z9p|>}lvUN?!v}6J;r)Dk4cjS}Q`aif1nc|bo~D|#+lm`;)|nYdJ=A?Fm_MCE_v8mD zhBdEwmOQq${j}?fe7K#!t4V=Zw_eqL+T8k%B|>$=)BGvdt&VLBwhv=wSd%+rqx|## zLC+6iq)h!?R+6T_&l*2mdU$p`L&>~J;r?$LPp7q*+lyH3snie9yy`7Db+PS?qVCcRyebMJD63lwVGK(({Y|HgNUqwP4;WvL$Rw zd6k)glqFkc;upII7ba;vG-XhTnbV{1S$9UN(pKn!-r@G``b<*n;*;c;yH@;I*j>MI z_Kb&*6+iBru;3UMx{pv$lmGxW)tGV0V&O1Nj`XIF8^Q@v*9q&cG zFG^mkV^OXBIpOlLnDy)b+}-CluQ9Iw-VdEM=9!n-8qCGo*KK)|V&QZ&JGy--8$;cn zd$-NYR$My%{m8TxJ`1kq{5&AN;ri0HX$j}6YNm7-&);|Re6}mE{DkXUJGswGGhcQJ zU-F%8TgSbrKN5Kvrd&1V3CPm1n>?pGgy-|YD;kIjNVINm=Qf7@yDvF&eV5VsdMQ68 zjzKrpt@rw*FCTSx$??eE+p{xVIJ>=KZ}n%FubZ2%2`;Upr zIvdYT=bB%A_g_NZS3&s*?Iz2 zX;Si|c2fqm3&}yk)=Zf#8zmpUc)8d-*wQTTOWhbRIZq0wa>55`}vyRi`n#T>-CP$TQch^+lRda(yl0Qz z$8xb}!X{Evbe~S>Vh}F=Frj*@%?bYlg3-klv{ z(Fb=P|6ySBqPR-=l-od7Husup{)t6T%qxbRDg?zFJef8y= z*T;iJadVfuo_OErHtU@8)GLL&kvqNa@_f2}wx-FgdDGJ$&$`$jdVEkga(M5vh2gVa z)>?a73zh8G-}Ro`uHf<=DH{WCJFjBNYYlEw?BZT6-^RFMnz&cpo4}N>(RF*cV&84p zYli(B(5)?bZ09j&7Odx4icN8Z6>^3^|P$b>U*`E}rj z>g}8*tS)9H%+iu|6Yg;bTw&gozqwN6pqHs`Qyj;N zi^pq=cqZTBdX{gaP+2)s#{Nat5gGO^zp8&~Te{_6PJNvWT3Peyap;Sp_NldtJ{zq0 zb0ojtz@(jX>5tll6Rm9{KJMPHeB7<({442T#}89n{tE~#H2qy;X~yOb%5R@SZZ zvX9Vhijz8X<5-2xzUD8z9M2y{htFSlvEf9_(Nr5p0TvV_zH9eZ$9occ{GyjK&Z}z4 zd+~h!%Nv`+WbQjHFT7frv*zvbf@3N?jW0|>P8weJh)@qK%5i&E{_~^h(oK`j&ooeH zo%lvzapJ5$ZHdZ0XI$QN#82uyV!gj#>$*;EA=a}5C81^h)Kkxd5MWUtKFJ#7)%19sV(s?(@X6?!+Gt`$q zx-s+q)8p5tobs}}erV2=`~H{34*U^hnSJr;7Y@Z0hRxE&W_-VwYtO%7b#>NRyGBcy zU1ypn6-i_=CzvXqjyn8AnIlhRN11i0ur$MtmygbdR{Kjc2v=|)cG-IL)1I|k7QGX* z>RsKJTpycbkR83;bgInfCm}M{pLhTHF)JwY9eHvc!; zjrSRtc{5_ah4B1Kb75BC_{OyEuHVb{j4p66&iF8`d1>>Twlq_Qpby`!?&&Y@4!d#} z6rdG3`PqIk!d83HeKx=4-Je-6X2@mO#CW{h=;l9tPQJ9AqP~Shop)rve-NLt6ocai z@q)KyuhdK!Hgs~D@oENUG592O-uL&an{zcPu$+7DvCzjG@9Wz|Sgrb;;?-AKq}4a~ zXKY=K@5(h#K6~yhRMs&FTVFfvi&;uY&AktnCvU&{X5Zm8f6CzwHV1(>tTDUi-YRBh z$i9>BX}guTf%A;)0gG3&*4=1@I=vug_3wyg_BrRKYg#@#xe|1g7&H|q=9hc_nYW=+ zo$$5Ec2P}M<-T|Y&s$4m+)Nmn67(P3dV57qnt@|O^WHhp znYj!;57N3gIwHd}R>SOhGu>NuhRd?g-{gvSOiDK1WOKTIz2?-ZHGCWIYF=EOWU40k zRp_&pR|2c!S+BFVQtScHqa3Z(&OzNzGJ7I`B`{fj4Dyw|}2;c&g0oiBBUTAyyo-Sghj5 z>*UPDn|J4GGBfrDnp}u*esuY|r?`#cb+b}C-7jVlH9qSuu{ahqoWd&`;mf$OF1 zy&8M=J5JiJR+`h3oSZyMF3{?wvf(R+2u!pjj&$DRLP-8fUA?B=0Y zLC5~DVZXTMRC3;;CpUU@&VPEG&6T=z^6PsG?WZL7H&oA?lC`bz*iLtij4v_?7K{&9 zu6>`ubpLfqu>AQ;?#p_*o_huP39y(m{K>E`UB||dH$$R6{q9P+8r1Rq{ z1UBcI+6u7$PYtSU4nO|?u`{RphqkshKJDX9%V)N%tK^CFxi8x^<3+M@B0pm{`&avQ zJK{eXrG?5B3#%UAcHq}{o%2dZO<#Vp?B-XQpZuPO_gl(^i!>H04 zZ4T~xk2aj_i|fmnSl8FaEO+eMx1$om*N$&vc`p_}`Eij+_x|a7=0u7fzvFXqqwa<$ z#-}%X6rMfKox0cJ=bbDUvFBa_52OC|t$Ez1lFH`zK}2OQr^%}jUIw2H)ggcKe7Ie{ z9KOABt81ZJQI7YA<~9$1{v-1_BKOagnP0kfdBSt&mVVb7X6`A?e=pdtus8Veu4~t3 z&IAd!gU{bYIT%RBPFwf>aTa*6#ptmfOdCV$xa>!bJIom-lh7PT8*_B{FV zrpszEgFgYM{ll-^t*U0dEW`SxAY#FD*Xc`NR~M-s-D8%p5Zr&CPr^z3Q8~?^DeK-#?+3zCB>= zc+oat)#n>4Wx|E$OzIbT)px*MEPldklMD`nGjcNy8JdR{-KH^CvW+!BC)3K)?=mzv;M4+S#SJ#cSDmx1=pf{7T#N%7z4O>9oiUuYu37u+BaK& z31-%J{O2#NDS5BNB%Kx${h9GvlX=w{^&RfhdilNIycOE5vFzmqg$+MUPoAE`H}mg* z5wE_rjtcX1{!TS%-{KdUA#iR!yQ9DxzB@~-O4qR+m|Vp3{&#ZHl#8nwpF4BsM@LML z-RiTkpoYcbkeb0-TOQ5&>Xp|!4J9vGn%&dVT{H8OPKu7r>n$%0H?Ixd9#)idcE8}^ z*ASy;G#l2-u{dt9{u29oPDn-7Y8i8lgVLM!Ij#>XdZXLI{<5}c4ok+Rf*PLUgr5<& z&Yu2pWNDmEuC_qX!TM#5gf&9dmt(hUdZ z@RgmJ?A+H|`fHcn%AjXkmgu-Fji~`U^GfgQH7fI>ru|8bysBI@r(=2kt*c&rwoWe= z9$2zE-$S+c`2R~&)`MoGj3*{9etzIl{t=DGUN7b*EV!5?`}xTkcK#-X3a!QYea}@s zwy?iA{OG*#h8?QM|DOBWymG~p8`)o)FKsvbUCevD{_LF9JZhd|>K>Dllg~AyU9hpBgDt$WhDSYO!D-3WiaL8{zGas@?&jPUXtmtFcy0+}m|Vnh8_UlJn-w_b zoq93*v8AU?2u>(GqrlPhz{kh@318*i&sMopwQTPy zm&N=RDp!f$eXlCVrsX1QpT+mJ36=_P?bciTYY*&TJfz~Ec|l{N!{X+169bMP`@sFU z{pgAO7aKL3llbIBSR4iB%zW8)XJN4^gIZ$lpXWOp#}b*xToK2QHGgG%IQgMhjmNs(&$^BJ>^XTwuYK6Sbo~ME zyjpV(pCy-HCvQ%0PIIw$Uo)L;BkQwdHb()LbSCr1ejD3)E@ZCWa!L5n#r@&GDpHHy za9i$k-Q2#8{n)<$b48fmM-;vKm%8z1&y!5`y#}7kPd;;2Iqr5qu1#F!_@(o*n^Pai z9NqWPQGn&S+b8MrwcoBW%xGzUKYQt0#(x1tZ{$8)>$%Fc%X(kbl6Mlt=^J8St8I*6 zs#BdYgu~Spy{6fgT#}UGZQNp zJ_}CTbCTUrfW_bO%i#r%)%grQhoUXb8Xb=~xMedxm?m@m)a&HZ)r;QUcqg&N=h*MN zt8Ex2|NiovofWRfd@19ESFV%x`!>&FV`vk(r=+#ytw!jzzsH|uP2cIY@Z)D!%@!8E zCIyapO)uIVoAZ)D&U(9F?Ro_-gNx!H(MZF6@r#A8*#--+IDQauvFDL0nJmpPx$a5O z|Cd4Q{rYShKkPam{?Z&IFEB^suR_*^|D3!Ho}V{uIlcdX%q!)4yhu`Myba(Jyv^L~ zTckxGoEJ-f`vaws4OTcRG8zH{9s&og b{+Y)rMHUrrF{@%=U|{fc^>bP0l+XkKI+<;j literal 0 HcmV?d00001 diff --git a/webpack.config.js b/webpack.config.js index 054152b2..b2f383af 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -94,7 +94,7 @@ module.exports = { ] }, { - test: /\.(ico|eot|ttf|woff|woff2)$/, + test: /\.(ico|eot|ttf|woff|woff2|fnt)$/, loader: "url-loader", options: { limit: 10000 From 2cd3e9cacde8e553b3a0e9244839a4eff90809e3 Mon Sep 17 00:00:00 2001 From: j433866 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
+ +
+ +