feature/IO-3052-Skia-Canvas-Handler: Initial commit

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-12-05 11:26:23 -08:00
parent 8d2bdb171b
commit 6336e7568f
5 changed files with 227 additions and 44 deletions

View File

@@ -7,7 +7,6 @@ RUN dnf install -y git \
&& dnf install -y nodejs \ && dnf install -y nodejs \
&& dnf clean all && dnf clean all
# Install dependencies required by node-canvas # Install dependencies required by node-canvas
RUN dnf install -y \ RUN dnf install -y \
gcc \ gcc \
@@ -19,9 +18,22 @@ RUN dnf install -y \
libpng-devel \ libpng-devel \
make \ make \
python3 \ python3 \
fontconfig \
freetype \
python3-pip \ python3-pip \
wget \
unzip \
&& dnf clean all && dnf clean all
# Install Montserrat fonts
RUN cd /tmp \
&& wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip \
&& unzip montserrat.zip -d montserrat \
&& mv montserrat/montserrat/*.ttf /usr/share/fonts \
&& fc-cache -fv \
&& rm -rf /tmp/montserrat /tmp/montserrat.zip \
&& echo "Montserrat fonts installed and cached successfully."
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app

98
package-lock.json generated
View File

@@ -52,6 +52,7 @@
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"redis": "^4.7.0", "redis": "^4.7.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6", "soap": "^1.1.6",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5", "socket.io-adapter": "^2.5.5",
@@ -3734,6 +3735,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cargo-cp-artifact": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.9.tgz",
"integrity": "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==",
"license": "MIT",
"bin": {
"cargo-cp-artifact": "bin/cargo-cp-artifact.js"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -6757,6 +6767,12 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
}, },
"node_modules/parenthesis": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz",
"integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==",
"license": "MIT"
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -6789,6 +6805,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -7459,6 +7481,73 @@
"is-arrayish": "^0.3.1" "is-arrayish": "^0.3.1"
} }
}, },
"node_modules/skia-canvas": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/skia-canvas/-/skia-canvas-2.0.0.tgz",
"integrity": "sha512-wpYkmr9mCxBme5HAnlm6YOEiuaN9tIm9CL+HN8e5AFD4K2FAJXCcWiWvc9+LM8jUXt+AyYXgiwUTBxdQ6P+PEg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"cargo-cp-artifact": "^0.1",
"glob": "^11.0.0",
"path-browserify": "^1.0.1",
"simple-get": "^4.0.1",
"string-split-by": "^1.0.0"
}
},
"node_modules/skia-canvas/node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/skia-canvas/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/skia-canvas/node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/slick": { "node_modules/slick": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz",
@@ -7851,6 +7940,15 @@
} }
] ]
}, },
"node_modules/string-split-by": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz",
"integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==",
"license": "MIT",
"dependencies": {
"parenthesis": "^3.1.5"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",

View File

@@ -62,6 +62,7 @@
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"redis": "^4.7.0", "redis": "^4.7.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6", "soap": "^1.1.6",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5", "socket.io-adapter": "^2.5.5",

View File

@@ -1,43 +1,28 @@
const { createCanvas } = require("canvas"); const { createCanvas } = require("canvas");
const { Canvas, FontLibrary } = require("skia-canvas");
const { performance } = require("perf_hooks");
const Chart = require("chart.js/auto"); const Chart = require("chart.js/auto");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const { backgroundColors, borderColors } = require("./canvas-colors"); const { backgroundColors, borderColors } = require("./canvas-colors");
const { isObject, defaultsDeep, isNumber } = require("lodash"); const { isObject, defaultsDeep, isNumber } = require("lodash");
exports.canvastest = function (req, res) { try {
//console.log("Incoming test request.", req); FontLibrary.use("Montserrat", [
res.status(200).send("OK"); "/usr/share/fonts/Montserrat-Regular.ttf",
}; "/usr/share/fonts/Montserrat-Bold.ttf",
"/usr/share/fonts/Montserrat-Italic.ttf"
]);
} catch (error) {
console.error(
"Error loading fonts Skia Canvas Fonts, please be sure to install Montserrat font package",
error.message
);
}
exports.canvas = function (req, res) { // Utility to create a chart configuration
const { w, h, values, keys, override } = req.body; const getChartConfiguration = (keys, values, override) => {
//console.log("Incoming Canvas Request:", w, h, values, keys, override); const defaultConfiguration = {
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Gate required values
if (!values || !keys) {
res.status(400).send("Missing required data");
return;
}
// Override must be an object if it exists
if (override && !isObject(override)) {
res.status(400).send("Override must be an object");
return;
}
// Set the default Width and Height
let [width, height] = [500, 275];
// Allow for custom width and height
if (isNumber(w)) {
width = w;
}
if (isNumber(h)) {
height = h;
}
const configuration = {
type: "doughnut", type: "doughnut",
data: { data: {
labels: keys, labels: keys,
@@ -53,6 +38,7 @@ exports.canvas = function (req, res) {
options: { options: {
devicePixelRatio: 4, devicePixelRatio: 4,
responsive: false, responsive: false,
animation: false,
maintainAspectRatio: true, maintainAspectRatio: true,
circumference: 180, circumference: 180,
rotation: -90, rotation: -90,
@@ -73,21 +59,106 @@ exports.canvas = function (req, res) {
} }
}; };
// If we have a valid override object, merge it with the default configuration object. return defaultsDeep(override || {}, defaultConfiguration);
// This allows for you to override the default configuration with a custom one. };
const defaults = () => {
if (!override || !isObject(override)) { // Utility to validate input
return configuration; const validateCanvasInput = ({ values, keys, override }, res) => {
} if (!Array.isArray(values) || !Array.isArray(keys)) {
return defaultsDeep(override, configuration); res.status(400).send("Invalid input: 'values' and 'keys' must be arrays.");
}; return false;
}
if (values.some((value) => typeof value !== "number")) {
res.status(400).send("Invalid input: 'values' must be an array of numbers.");
return false;
}
if (keys.some((key) => typeof key !== "string")) {
res.status(400).send("Invalid input: 'keys' must be an array of strings.");
return false;
}
if (override && !isObject(override)) {
res.status(400).send("Override must be an object");
return false;
}
return true;
};
exports.canvastest = function (req, res) {
//console.log("Incoming test request.", req);
res.status(200).send("OK");
};
exports.canvas = function (req, res) {
const startTime = performance.now();
const { w, h, values, keys, override } = req.body;
//console.log("Incoming Canvas Request:", w, h, values, keys, override);
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
if (!validateCanvasInput(req.body, res)) return;
// Set the default Width and Height
let [width, height] = [500, 275];
// Allow for custom width and height
if (isNumber(w)) {
width = w;
}
if (isNumber(h)) {
height = h;
}
const configuration = getChartConfiguration(keys, values, override);
res.status(200).send( res.status(200).send(
(() => { (() => {
const canvas = createCanvas(width, height); const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
new Chart(ctx, defaults()); new Chart(ctx, configuration);
return canvas.toDataURL(); return canvas.toDataURL();
})() })()
); );
console.log("Canvas generation time:", performance.now() - startTime, "ms");
};
exports.canvasSkia = async function (req, res) {
const startTime = performance.now();
const { w, h, values, keys, override } = req.body;
// Log incoming request for debugging
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
if (!validateCanvasInput(req.body, res)) return;
// Default width and height
const width = typeof w === "number" && w > 0 ? w : 500;
const height = typeof h === "number" && h > 0 ? h : 275;
const configuration = getChartConfiguration(keys, values, override);
try {
// Create a canvas and get the 2D rendering context
const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d");
// Render the chart
new Chart(ctx, configuration);
// Convert the canvas to a Base64-encoded image
const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
const dataURL = `data:image/png;base64,${chartImage}`;
// Send the Base64-encoded image as the response
res.status(200).send(dataURL);
console.log("Canvas generation time:", performance.now() - startTime, "ms");
} catch (error) {
// Log and handle rendering errors
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
console.error("Error generating chart:", error.message);
res.status(500).send("Failed to generate canvas.");
}
}; };

View File

@@ -2,10 +2,11 @@ const express = require("express");
const router = express.Router(); const router = express.Router();
const { inlinecss } = require("../render/inlinecss"); const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { canvas } = require("../render/canvas-handler"); const { canvas, canvasSkia } = require("../render/canvas-handler");
// Define the route for inline CSS rendering // Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); router.post("/canvas", canvas);
router.post("/canvas-skia", canvasSkia);
module.exports = router; module.exports = router;