Browse Source

Merge branch 'more-image-ops' of https://github.com/j433866/CyberChef into j433866-more-image-ops

n1474335 6 năm trước cách đây
mục cha
commit
db72cad610
33 tập tin đã thay đổi với 2814 bổ sung260 xóa
  1. 3 0
      src/core/config/Categories.json
  2. 251 0
      src/core/lib/ImageManipulation.mjs
  3. 11 1
      src/core/lib/Magic.mjs
  4. 93 0
      src/core/lib/QRCode.mjs
  5. 266 0
      src/core/operations/AddTextToImage.mjs
  6. 22 13
      src/core/operations/BlurImage.mjs
  7. 30 12
      src/core/operations/ContainImage.mjs
  8. 143 0
      src/core/operations/ConvertImageFormat.mjs
  9. 17 11
      src/core/operations/CoverImage.mjs
  10. 17 11
      src/core/operations/CropImage.mjs
  11. 18 11
      src/core/operations/DitherImage.mjs
  12. 17 11
      src/core/operations/FlipImage.mjs
  13. 17 42
      src/core/operations/GenerateQRCode.mjs
  14. 17 11
      src/core/operations/ImageBrightnessContrast.mjs
  15. 17 11
      src/core/operations/ImageFilter.mjs
  16. 18 11
      src/core/operations/ImageHueSaturationLightness.mjs
  17. 17 11
      src/core/operations/ImageOpacity.mjs
  18. 18 11
      src/core/operations/InvertImage.mjs
  19. 27 12
      src/core/operations/NormaliseImage.mjs
  20. 15 57
      src/core/operations/ParseQRCode.mjs
  21. 17 11
      src/core/operations/ResizeImage.mjs
  22. 18 11
      src/core/operations/RotateImage.mjs
  23. 168 0
      src/core/operations/SharpenImage.mjs
  24. 485 0
      src/web/static/fonts/bmfonts/Roboto72White.fnt
  25. BIN
      src/web/static/fonts/bmfonts/Roboto72White.png
  26. 488 0
      src/web/static/fonts/bmfonts/RobotoBlack72White.fnt
  27. BIN
      src/web/static/fonts/bmfonts/RobotoBlack72White.png
  28. 103 0
      src/web/static/fonts/bmfonts/RobotoMono72White.fnt
  29. BIN
      src/web/static/fonts/bmfonts/RobotoMono72White.png
  30. 492 0
      src/web/static/fonts/bmfonts/RobotoSlab72White.fnt
  31. BIN
      src/web/static/fonts/bmfonts/RobotoSlab72White.png
  32. 0 1
      tests/operations/tests/Magic.mjs
  33. 9 1
      webpack.config.js

+ 3 - 0
src/core/config/Categories.json

@@ -384,6 +384,9 @@
             "Contain Image",
             "Cover Image",
             "Image Hue/Saturation/Lightness",
+            "Sharpen Image",
+            "Convert Image Format",
+            "Add Text To Image",
             "Hex Density chart",
             "Scatter chart",
             "Series chart",

+ 251 - 0
src/core/lib/ImageManipulation.mjs

@@ -0,0 +1,251 @@
+/**
+ * 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 {number} 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) {
+        throw new OperationError(`Error blurring image. (${err})`);
+    }
+
+    return input;
+}
+
+/**
+ *
+ * @param {number} radius
+ * @param {number} 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 {number} width
+ * @param {number} height
+ * @param {number} 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;
+}

+ 11 - 1
src/core/lib/Magic.mjs

@@ -312,6 +312,11 @@ class Magic {
                 return;
             }
 
+            // If the recipe returned an empty buffer, do not continue
+            if (_buffersEqual(output, new ArrayBuffer())) {
+                return;
+            }
+
             const magic = new Magic(output, this.opPatterns),
                 speculativeResults = await magic.speculativeExecution(
                     depth-1, extLang, intensive, [...recipeConfig, opConfig], op.useful, crib);
@@ -395,7 +400,12 @@ class Magic {
         const recipe = new Recipe(recipeConfig);
         try {
             await recipe.execute(dish);
-            return dish.get(Dish.ARRAY_BUFFER);
+            // Return an empty buffer if the recipe did not run to completion
+            if (recipe.lastRunOp === recipe.opList[recipe.opList.length - 1]) {
+                return dish.get(Dish.ARRAY_BUFFER);
+            } else {
+                return new ArrayBuffer();
+            }
         } catch (err) {
             // If there are errors, return an empty buffer
             return new ArrayBuffer();

+ 93 - 0
src/core/lib/QRCode.mjs

@@ -0,0 +1,93 @@
+/**
+ * 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";
+import Utils from "../Utils";
+
+/**
+ * Parses a QR code image from an image
+ *
+ * @param {ArrayBuffer} input
+ * @param {boolean} normalise
+ * @returns {string}
+ */
+export async function parseQrCode(input, normalise) {
+    let image;
+    try {
+        image = await jimp.read(input);
+    } catch (err) {
+        throw new OperationError(`Error opening image. (${err})`);
+    }
+
+    try {
+        if (normalise) {
+            image.rgba(false);
+            image.background(0xFFFFFFFF);
+            image.normalize();
+            image.greyscale();
+            image = await image.getBufferAsync(jimp.MIME_JPEG);
+            image = await jimp.read(image);
+        }
+    } 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 {ArrayBuffer}
+ */
+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 Utils.strToArrayBuffer(qrImage);
+        case "PNG":
+            return qrImage.buffer;
+        default:
+            throw new OperationError("Unsupported QR code format.");
+    }
+}

+ 266 - 0
src/core/operations/AddTextToImage.mjs

@@ -0,0 +1,266 @@
+/**
+ * @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.<br><br>Text can be horizontally or vertically aligned, or the position can be manually specified.<br>Variants of the Roboto font face are available in any size or colour.";
+        this.infoURL = "";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
+        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 {ArrayBuffer} 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(new Uint8Array(input))) {
+            throw new OperationError("Invalid file type.");
+        }
+
+        let image;
+        try {
+            image = await jimp.read(input);
+        } catch (err) {
+            throw new OperationError(`Error loading image. (${err})`);
+        }
+        try {
+            if (ENVIRONMENT_IS_WORKER())
+                self.sendStatusMessage("Adding text to image...");
+
+            const fontsMap = {};
+            const fonts = [
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"),
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"),
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt")
+            ];
+
+            await Promise.all(fonts)
+                .then(fonts => {
+                    fontsMap.Roboto = fonts[0];
+                    fontsMap["Roboto Black"] = fonts[1];
+                    fontsMap["Roboto Mono"] = fonts[2];
+                    fontsMap["Roboto Slab"] = fonts[3];
+                });
+
+
+            // Make Webpack load the png font images
+            await Promise.all([
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"),
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png"),
+                import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png"),
+                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;
+                        }
+                    }
+                }
+            });
+
+            // Create a temporary image to hold the rendered text
+            const textImage = new jimp(jimp.measureText(jimpFont, text), jimp.measureTextHeight(jimpFont, text));
+            textImage.print(jimpFont, 0, 0, text);
+
+            // Scale the rendered text image to the correct size
+            const scaleFactor = size / 72;
+            if (size !== 1) {
+                // Use bicubic for decreasing size
+                if (size > 1) {
+                    textImage.scale(scaleFactor, jimp.RESIZE_BICUBIC);
+                } else {
+                    textImage.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) - (textImage.getWidth() / 2);
+                    break;
+                case "Right":
+                    xPos = image.getWidth() - textImage.getWidth();
+                    break;
+            }
+
+            switch (vAlign) {
+                case "Top":
+                    yPos = 0;
+                    break;
+                case "Middle":
+                    yPos = (image.getHeight() / 2) - (textImage.getHeight() / 2);
+                    break;
+                case "Bottom":
+                    yPos = image.getHeight() - textImage.getHeight();
+                    break;
+            }
+
+            // Blit the rendered text image onto the original source image
+            image.blit(textImage, xPos, yPos);
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
+        } catch (err) {
+            throw new OperationError(`Error adding text to image. (${err})`);
+        }
+    }
+
+    /**
+     * Displays the blurred image using HTML for web apps
+     *
+     * @param {ArrayBuffer} data
+     * @returns {html}
+     */
+    present(data) {
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
+
+        const type = isImage(dataArray);
+        if (!type) {
+            throw new OperationError("Invalid file type.");
+        }
+
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
+    }
+
+}
+
+export default AddTextToImage;

+ 22 - 13
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
@@ -25,8 +26,8 @@ class BlurImage extends Operation {
         this.module = "Image";
         this.description = "Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
         this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -44,37 +45,44 @@ class BlurImage extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [blurAmount, blurType] = args;
 
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
         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;
             }
 
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error blurring image. (${err})`);
         }
@@ -83,18 +91,19 @@ class BlurImage extends Operation {
     /**
      * Displays the blurred image using HTML for web apps
      *
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 30 - 12
src/core/operations/ContainImage.mjs

@@ -25,8 +25,8 @@ class ContainImage extends Operation {
         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.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -72,17 +72,22 @@ class ContainImage extends Operation {
                     "Bezier"
                 ],
                 defaultIndex: 1
+            },
+            {
+                name: "Opaque background",
+                type: "boolean",
+                value: true
             }
         ];
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @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,
@@ -101,13 +106,13 @@ class ContainImage extends Operation {
             "Bottom": jimp.VERTICAL_ALIGN_BOTTOM
         };
 
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -115,8 +120,20 @@ class ContainImage extends Operation {
             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];
+
+            if (opaqueBg) {
+                const newImage = await jimp.read(width, height, 0x000000FF);
+                newImage.blit(image, 0, 0);
+                image = newImage;
+            }
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error containing image. (${err})`);
         }
@@ -124,18 +141,19 @@ class ContainImage extends Operation {
 
     /**
      * Displays the contained image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 143 - 0
src/core/operations/ConvertImageFormat.mjs

@@ -0,0 +1,143 @@
+/**
+ * @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";
+
+/**
+ * Convert Image Format operation
+ */
+class ConvertImageFormat extends Operation {
+
+    /**
+     * ConvertImageFormat constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Convert Image Format";
+        this.module = "Image";
+        this.description = "Converts an image between different formats. Supported formats:<br><ul><li>Joint Photographic Experts Group (JPEG)</li><li>Portable Network Graphics (PNG)</li><li>Bitmap (BMP)</li><li>Tagged Image File Format (TIFF)</li></ul><br>Note: GIF files are supported for input, but cannot be outputted.";
+        this.infoURL = "https://wikipedia.org/wiki/Image_file_formats";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
+        this.presentType = "html";
+        this.args = [
+            {
+                name: "Output Format",
+                type: "option",
+                value: [
+                    "JPEG",
+                    "PNG",
+                    "BMP",
+                    "TIFF"
+                ]
+            },
+            {
+                name: "JPEG Quality",
+                type: "number",
+                value: 80,
+                min: 1,
+                max: 100
+            },
+            {
+                name: "PNG Filter Type",
+                type: "option",
+                value: [
+                    "Auto",
+                    "None",
+                    "Sub",
+                    "Up",
+                    "Average",
+                    "Paeth"
+                ]
+            },
+            {
+                name: "PNG Deflate Level",
+                type: "number",
+                value: 9,
+                min: 0,
+                max: 9
+            }
+        ];
+    }
+
+    /**
+     * @param {ArrayBuffer} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    async run(input, args) {
+        const [format, jpegQuality, pngFilterType, pngDeflateLevel] = args;
+        const formatMap = {
+            "JPEG": jimp.MIME_JPEG,
+            "PNG": jimp.MIME_PNG,
+            "BMP": jimp.MIME_BMP,
+            "TIFF": jimp.MIME_TIFF
+        };
+
+        const pngFilterMap = {
+            "Auto": jimp.PNG_FILTER_AUTO,
+            "None": jimp.PNG_FILTER_NONE,
+            "Sub": jimp.PNG_FILTER_SUB,
+            "Up": jimp.PNG_FILTER_UP,
+            "Average": jimp.PNG_FILTER_AVERAGE,
+            "Paeth": jimp.PNG_FILTER_PATH // Incorrect spelling in Jimp library
+        };
+
+        const mime = formatMap[format];
+
+        if (!isImage(new Uint8Array(input))) {
+            throw new OperationError("Invalid file format.");
+        }
+        let image;
+        try {
+            image = await jimp.read(input);
+        } catch (err) {
+            throw new OperationError(`Error opening image file. (${err})`);
+        }
+        try {
+            switch (format) {
+                case "JPEG":
+                    image.quality(jpegQuality);
+                    break;
+                case "PNG":
+                    image.filterType(pngFilterMap[pngFilterType]);
+                    image.deflateLevel(pngDeflateLevel);
+                    break;
+            }
+
+            const imageBuffer = await image.getBufferAsync(mime);
+            return imageBuffer.buffer;
+        } catch (err) {
+            throw new OperationError(`Error converting image format. (${err})`);
+        }
+    }
+
+    /**
+     * Displays the converted image using HTML for web apps
+     *
+     * @param {ArrayBuffer} data
+     * @returns {html}
+     */
+    present(data) {
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
+
+        const type = isImage(dataArray);
+        if (!type) {
+            throw new OperationError("Invalid file type.");
+        }
+
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
+    }
+
+}
+
+export default ConvertImageFormat;

+ 17 - 11
src/core/operations/CoverImage.mjs

@@ -25,8 +25,8 @@ class CoverImage extends Operation {
         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.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -77,7 +77,7 @@ class CoverImage extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
@@ -101,13 +101,13 @@ class CoverImage extends Operation {
             "Bottom": jimp.VERTICAL_ALIGN_BOTTOM
         };
 
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -115,8 +115,13 @@ class CoverImage extends Operation {
             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 imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error covering image. (${err})`);
         }
@@ -124,18 +129,19 @@ class CoverImage extends Operation {
 
     /**
      * Displays the covered image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 17 - 11
src/core/operations/CropImage.mjs

@@ -25,8 +25,8 @@ class CropImage extends Operation {
         this.module = "Image";
         this.description = "Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>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.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -86,19 +86,19 @@ class CropImage extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -116,8 +116,13 @@ class CropImage extends Operation {
                 image.crop(xPos, yPos, width, height);
             }
 
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error cropping image. (${err})`);
         }
@@ -125,18 +130,19 @@ class CropImage extends Operation {
 
     /**
      * Displays the cropped image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 18 - 11
src/core/operations/DitherImage.mjs

@@ -25,25 +25,25 @@ class DitherImage extends Operation {
         this.module = "Image";
         this.description = "Apply a dither effect to an image.";
         this.infoURL = "https://wikipedia.org/wiki/Dither";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [];
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -51,8 +51,14 @@ class DitherImage extends Operation {
             if (ENVIRONMENT_IS_WORKER())
                 self.sendStatusMessage("Applying dither to image...");
             image.dither565();
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error applying dither to image. (${err})`);
         }
@@ -60,18 +66,19 @@ class DitherImage extends Operation {
 
     /**
      * Displays the dithered image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 17 - 11
src/core/operations/FlipImage.mjs

@@ -25,8 +25,8 @@ class FlipImage extends Operation {
         this.module = "Image";
         this.description = "Flips an image along its X or Y axis.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -38,19 +38,19 @@ class FlipImage extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [flipAxis] = args;
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid input file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -66,8 +66,13 @@ class FlipImage extends Operation {
                     break;
             }
 
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error flipping image. (${err})`);
         }
@@ -75,18 +80,19 @@ class FlipImage extends Operation {
 
     /**
      * Displays the flipped image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 17 - 42
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 { isImage } from "../lib/FileType";
 import Utils from "../Utils";
@@ -27,7 +27,7 @@ class GenerateQRCode extends Operation {
         this.description = "Generates a Quick Response (QR) code from the input text.<br><br>A QR code is a type of matrix barcode (or two-dimensional barcode) first designed in 1994 for the automotive industry in Japan. A barcode is a machine-readable optical label that contains information about the item to which it is attached.";
         this.infoURL = "https://wikipedia.org/wiki/QR_code";
         this.inputType = "string";
-        this.outputType = "byteArray";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -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",
@@ -57,61 +59,34 @@ class GenerateQRCode extends Operation {
     /**
      * @param {string} input
      * @param {Object[]} args
-     * @returns {byteArray}
+     * @returns {ArrayBuffer}
      */
     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);
     }
 
     /**
      * Displays the QR image using HTML for web apps
      *
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data, args) {
-        if (!data.length) return "";
-
-        const [format] = args;
-
+        if (!data.byteLength && !data.length) return "";
+        const dataArray = new Uint8Array(data),
+            [format] = args;
         if (format === "PNG") {
-            let dataURI = "data:";
-            const mime = isImage(data);
-            if (mime){
-                dataURI += mime + ";";
-            } else {
-                throw new OperationError("Invalid PNG file generated by QR image");
+            const type = isImage(dataArray);
+            if (!type) {
+                throw new OperationError("Invalid file type.");
             }
-            dataURI += "base64," + toBase64(data);
 
-            return `<img src="${dataURI}">`;
+            return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
         }
 
-        return Utils.byteArrayToChars(data);
+        return Utils.arrayBufferToStr(data);
     }
 
 }

+ 17 - 11
src/core/operations/ImageBrightnessContrast.mjs

@@ -25,8 +25,8 @@ class ImageBrightnessContrast extends Operation {
         this.module = "Image";
         this.description = "Adjust the brightness or contrast of an image.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -47,19 +47,19 @@ class ImageBrightnessContrast extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [brightness, contrast] = args;
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -75,8 +75,13 @@ class ImageBrightnessContrast extends Operation {
                 image.contrast(contrast / 100);
             }
 
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error adjusting image brightness or contrast. (${err})`);
         }
@@ -84,18 +89,19 @@ class ImageBrightnessContrast extends Operation {
 
     /**
      * Displays the image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 17 - 11
src/core/operations/ImageFilter.mjs

@@ -25,8 +25,8 @@ class ImageFilter extends Operation {
         this.module = "Image";
         this.description = "Applies a greyscale or sepia filter to an image.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -41,19 +41,19 @@ class ImageFilter extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [filterType] = args;
-        if (!isImage(input)){
+        if (!isImage(new Uint8Array(input))){
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -66,8 +66,13 @@ class ImageFilter extends Operation {
                 image.sepia();
             }
 
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error applying filter to image. (${err})`);
         }
@@ -75,18 +80,19 @@ class ImageFilter extends Operation {
 
     /**
      * Displays the blurred image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 18 - 11
src/core/operations/ImageHueSaturationLightness.mjs

@@ -25,8 +25,8 @@ class ImageHueSaturationLightness extends Operation {
         this.module = "Image";
         this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -54,20 +54,20 @@ class ImageHueSaturationLightness extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [hue, saturation, lightness] = args;
 
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -102,8 +102,14 @@ class ImageHueSaturationLightness extends Operation {
                     }
                 ]);
             }
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`);
         }
@@ -111,18 +117,19 @@ class ImageHueSaturationLightness extends Operation {
 
     /**
      * Displays the image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 }
 

+ 17 - 11
src/core/operations/ImageOpacity.mjs

@@ -25,8 +25,8 @@ class ImageOpacity extends Operation {
         this.module = "Image";
         this.description = "Adjust the opacity of an image.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -40,19 +40,19 @@ class ImageOpacity extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [opacity] = args;
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -61,8 +61,13 @@ class ImageOpacity extends Operation {
                 self.sendStatusMessage("Changing image opacity...");
             image.opacity(opacity / 100);
 
-            const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error changing image opacity. (${err})`);
         }
@@ -70,18 +75,19 @@ class ImageOpacity extends Operation {
 
     /**
      * Displays the image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 18 - 11
src/core/operations/InvertImage.mjs

@@ -25,25 +25,25 @@ class InvertImage extends Operation {
         this.module = "Image";
         this.description = "Invert the colours of an image.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [];
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid input file format.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -51,8 +51,14 @@ class InvertImage extends Operation {
             if (ENVIRONMENT_IS_WORKER())
                 self.sendStatusMessage("Inverting image...");
             image.invert();
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error inverting image. (${err})`);
         }
@@ -60,18 +66,19 @@ class InvertImage extends Operation {
 
     /**
      * Displays the inverted image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 27 - 12
src/core/operations/NormaliseImage.mjs

@@ -25,44 +25,59 @@ class NormaliseImage extends Operation {
         this.module = "Image";
         this.description = "Normalise the image colours.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType=  "html";
         this.args = [];
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
-        const image = await jimp.read(Buffer.from(input));
+        let image;
+        try {
+            image = await jimp.read(input);
+        } catch (err) {
+            throw new OperationError(`Error opening image file. (${err})`);
+        }
 
-        image.normalize();
+        try {
+            image.normalize();
 
-        const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-        return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
+        } catch (err) {
+            throw new OperationError(`Error normalising image. (${err})`);
+        }
     }
 
     /**
      * Displays the normalised image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 15 - 57
src/core/operations/ParseQRCode.mjs

@@ -6,9 +6,8 @@
 
 import Operation from "../Operation";
 import OperationError from "../errors/OperationError";
-import { isImage } from "../lib/FileType";
-import jsqr from "jsqr";
-import jimp from "jimp";
+import { isImage } from "../lib/FileType.mjs";
+import { parseQrCode } from "../lib/QRCode";
 
 /**
  * Parse QR Code operation
@@ -25,7 +24,7 @@ class ParseQRCode extends Operation {
         this.module = "Image";
         this.description = "Reads an image file and attempts to detect and read a Quick Response (QR) code from the image.<br><br><u>Normalise Image</u><br>Attempts to normalise the image before parsing it to improve detection of a QR code.";
         this.infoURL = "https://wikipedia.org/wiki/QR_code";
-        this.inputType = "byteArray";
+        this.inputType = "ArrayBuffer";
         this.outputType = "string";
         this.args = [
             {
@@ -34,69 +33,28 @@ 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
+            }
+        ];
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {string}
      */
     async run(input, args) {
         const [normalise] = args;
 
-        // Make sure that the input is an image
-        if (!isImage(input)) throw new OperationError("Invalid file type.");
-
-        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 (!isImage(new Uint8Array(input))) {
+            throw new OperationError("Invalid file type.");
         }
-
-        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."));
-                });
-        });
-
+        return await parseQrCode(input, normalise);
     }
 
 }

+ 17 - 11
src/core/operations/ResizeImage.mjs

@@ -25,8 +25,8 @@ class ResizeImage extends Operation {
         this.module = "Image";
         this.description = "Resizes an image to the specified width and height values.";
         this.infoURL = "https://wikipedia.org/wiki/Image_scaling";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -67,7 +67,7 @@ class ResizeImage extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
@@ -86,13 +86,13 @@ class ResizeImage extends Operation {
             "Bezier": jimp.RESIZE_BEZIER
         };
 
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -110,8 +110,13 @@ class ResizeImage extends Operation {
                 image.resize(width, height, resizeMap[resizeAlg]);
             }
 
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error resizing image. (${err})`);
         }
@@ -119,18 +124,19 @@ class ResizeImage extends Operation {
 
     /**
      * Displays the resized image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 18 - 11
src/core/operations/RotateImage.mjs

@@ -25,8 +25,8 @@ class RotateImage extends Operation {
         this.module = "Image";
         this.description = "Rotates an image by the specified number of degrees.";
         this.infoURL = "";
-        this.inputType = "byteArray";
-        this.outputType = "byteArray";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
         this.presentType = "html";
         this.args = [
             {
@@ -38,20 +38,20 @@ class RotateImage extends Operation {
     }
 
     /**
-     * @param {byteArray} input
+     * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {byteArray}
      */
     async run(input, args) {
         const [degrees] = args;
 
-        if (!isImage(input)) {
+        if (!isImage(new Uint8Array(input))) {
             throw new OperationError("Invalid file type.");
         }
 
         let image;
         try {
-            image = await jimp.read(Buffer.from(input));
+            image = await jimp.read(input);
         } catch (err) {
             throw new OperationError(`Error loading image. (${err})`);
         }
@@ -59,8 +59,14 @@ class RotateImage extends Operation {
             if (ENVIRONMENT_IS_WORKER())
                 self.sendStatusMessage("Rotating image...");
             image.rotate(degrees);
-            const imageBuffer = await image.getBufferAsync(jimp.AUTO);
-            return [...imageBuffer];
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
         } catch (err) {
             throw new OperationError(`Error rotating image. (${err})`);
         }
@@ -68,18 +74,19 @@ class RotateImage extends Operation {
 
     /**
      * Displays the rotated image using HTML for web apps
-     * @param {byteArray} data
+     * @param {ArrayBuffer} data
      * @returns {html}
      */
     present(data) {
-        if (!data.length) return "";
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
 
-        const type = isImage(data);
+        const type = isImage(dataArray);
         if (!type) {
             throw new OperationError("Invalid file type.");
         }
 
-        return `<img src="data:${type};base64,${toBase64(data)}">`;
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
     }
 
 }

+ 168 - 0
src/core/operations/SharpenImage.mjs

@@ -0,0 +1,168 @@
+/**
+ * @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 { gaussianBlur } from "../lib/ImageManipulation";
+import jimp from "jimp";
+
+/**
+ * Sharpen Image operation
+ */
+class SharpenImage extends Operation {
+
+    /**
+     * SharpenImage constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Sharpen Image";
+        this.module = "Image";
+        this.description = "Sharpens an image (Unsharp mask)";
+        this.infoURL = "https://wikipedia.org/wiki/Unsharp_masking";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "ArrayBuffer";
+        this.presentType = "html";
+        this.args = [
+            {
+                name: "Radius",
+                type: "number",
+                value: 2,
+                min: 1
+            },
+            {
+                name: "Amount",
+                type: "number",
+                value: 1,
+                min: 0,
+                step: 0.1
+            },
+            {
+                name: "Threshold",
+                type: "number",
+                value: 10,
+                min: 0,
+                max: 100
+            }
+        ];
+    }
+
+    /**
+     * @param {ArrayBuffer} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    async run(input, args) {
+        const [radius, amount, threshold] = args;
+
+        if (!isImage(new Uint8Array(input))){
+            throw new OperationError("Invalid file type.");
+        }
+
+        let image;
+        try {
+            image = await jimp.read(input);
+        } catch (err) {
+            throw new OperationError(`Error loading image. (${err})`);
+        }
+
+        try {
+            if (ENVIRONMENT_IS_WORKER())
+                self.sendStatusMessage("Sharpening image... (Cloning image)");
+            const blurMask = image.clone();
+
+            if (ENVIRONMENT_IS_WORKER())
+                self.sendStatusMessage("Sharpening image... (Blurring cloned image)");
+            const blurImage = gaussianBlur(image.clone(), radius, 3);
+
+
+            if (ENVIRONMENT_IS_WORKER())
+                self.sendStatusMessage("Sharpening image... (Creating unsharp mask)");
+            blurMask.scan(0, 0, blurMask.bitmap.width, blurMask.bitmap.height, function(x, y, idx) {
+                const blurRed = blurImage.bitmap.data[idx];
+                const blurGreen = blurImage.bitmap.data[idx + 1];
+                const blurBlue = blurImage.bitmap.data[idx + 2];
+
+                const normalRed = this.bitmap.data[idx];
+                const normalGreen = this.bitmap.data[idx + 1];
+                const normalBlue = this.bitmap.data[idx + 2];
+
+                // Subtract blurred pixel value from normal image
+                this.bitmap.data[idx] = (normalRed > blurRed) ? normalRed - blurRed : 0;
+                this.bitmap.data[idx + 1] = (normalGreen > blurGreen) ? normalGreen - blurGreen : 0;
+                this.bitmap.data[idx + 2] = (normalBlue > blurBlue) ? normalBlue - blurBlue : 0;
+            });
+
+            if (ENVIRONMENT_IS_WORKER())
+                self.sendStatusMessage("Sharpening image... (Merging with unsharp mask)");
+            image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) {
+                let maskRed = blurMask.bitmap.data[idx];
+                let maskGreen = blurMask.bitmap.data[idx + 1];
+                let maskBlue = blurMask.bitmap.data[idx + 2];
+
+                const normalRed = this.bitmap.data[idx];
+                const normalGreen = this.bitmap.data[idx + 1];
+                const normalBlue = this.bitmap.data[idx + 2];
+
+                // Calculate luminance
+                const maskLuminance = (0.2126 * maskRed + 0.7152 * maskGreen + 0.0722 * maskBlue);
+                const normalLuminance = (0.2126 * normalRed + 0.7152 * normalGreen + 0.0722 * normalBlue);
+
+                let luminanceDiff;
+                if (maskLuminance > normalLuminance) {
+                    luminanceDiff = maskLuminance - normalLuminance;
+                } else {
+                    luminanceDiff = normalLuminance - maskLuminance;
+                }
+
+                // Scale mask colours by amount
+                maskRed = maskRed * amount;
+                maskGreen = maskGreen * amount;
+                maskBlue = maskBlue * amount;
+
+                // Only change pixel value if the difference is higher than threshold
+                if ((luminanceDiff / 255) * 100 >= threshold) {
+                    this.bitmap.data[idx] = (normalRed + maskRed) <= 255 ? normalRed + maskRed : 255;
+                    this.bitmap.data[idx + 1] = (normalGreen + maskGreen) <= 255 ? normalGreen + maskGreen : 255;
+                    this.bitmap.data[idx + 2] = (normalBlue + maskBlue) <= 255 ? normalBlue + maskBlue : 255;
+                }
+            });
+
+            let imageBuffer;
+            if (image.getMIME() === "image/gif") {
+                imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+            } else {
+                imageBuffer = await image.getBufferAsync(jimp.AUTO);
+            }
+            return imageBuffer.buffer;
+        } catch (err) {
+            throw new OperationError(`Error sharpening image. (${err})`);
+        }
+    }
+
+    /**
+     * Displays the sharpened image using HTML for web apps
+     * @param {ArrayBuffer} data
+     * @returns {html}
+     */
+    present(data) {
+        if (!data.byteLength) return "";
+        const dataArray = new Uint8Array(data);
+
+        const type = isImage(dataArray);
+        if (!type) {
+            throw new OperationError("Invalid file type.");
+        }
+
+        return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
+    }
+
+}
+
+export default SharpenImage;

+ 485 - 0
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="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

BIN
src/web/static/fonts/bmfonts/Roboto72White.png


+ 488 - 0
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="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

BIN
src/web/static/fonts/bmfonts/RobotoBlack72White.png


+ 103 - 0
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="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

BIN
src/web/static/fonts/bmfonts/RobotoMono72White.png


+ 492 - 0
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="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

BIN
src/web/static/fonts/bmfonts/RobotoSlab72White.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 1
tests/operations/tests/Magic.mjs


+ 9 - 1
webpack.config.js

@@ -119,9 +119,17 @@ module.exports = {
                     encoding: "base64"
                 }
             },
+            { // Store font .fnt and .png files in a separate fonts folder
+                test: /(\.fnt$|bmfonts\/.+\.png$)/,
+                loader: "file-loader",
+                options: {
+                    name: "[name].[ext]",
+                    outputPath: "assets/fonts"
+                }
+            },
             { // First party images are saved as files to be cached
                 test: /\.(png|jpg|gif)$/,
-                exclude: /node_modules/,
+                exclude: /(node_modules|bmfonts)/,
                 loader: "file-loader",
                 options: {
                     name: "images/[name].[ext]"

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác