ecliptica/server/routers/pen-plotter/utilities/svg.js

557 lines
22 KiB
JavaScript
Raw Normal View History

2023-11-25 11:56:14 +03:00
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeSVGToFile = exports.generateUniqueSVGPath = exports.createCharacterDirectory = exports.validateName = exports.numberOfFiles = exports.svgLinePathNames = exports.generateSvg = exports.printSVG = void 0;
const fs_1 = __importDefault(require("fs"));
const paths_1 = require("../paths");
const jsdom_1 = __importDefault(require("jsdom"));
/**
* Returns the number of files in a directory.
* @param dir - The directory path.
* @returns A promise that resolves to the number of files in the directory.
*/
const numberOfFiles = (dir) => __awaiter(void 0, void 0, void 0, function* () {
try {
const files = yield fs_1.default.promises.readdir(dir);
return files.length;
}
catch (err) {
// File is not found
return 0;
}
});
exports.numberOfFiles = numberOfFiles;
/**
* Returns a string with a valid name based on the input string.
* Replaces invalid characters with their corresponding names or "lower-" / "upper-" prefix.
* @param name - The input string to be validated.
* @returns A string with a valid name.
*/
const validateName = (name) => {
const map = {
"?": "questionMark",
"*": "asterisk",
"/": "slash",
"\\": "backslash",
":": "colon",
"|": "pipe",
"<": "lessThan",
">": "greaterThan",
'"': "doubleQuote",
"'": "singleQuote",
"@": "at",
"#": "hash",
$: "dollar",
"%": "percent",
"^": "caret",
"&": "ampersand",
"(": "leftParenthesis",
")": "rightParenthesis",
"-": "hyphen",
_: "underscore",
"=": "equal",
"+": "plus",
"{": "leftCurlyBrace",
"}": "rightCurlyBrace",
"[": "leftSquareBracket",
"]": "rightSquareBracket",
",": "comma",
".": "period",
"!": "exclamationMark",
"~": "tilde",
"`": "graveAccent",
};
const numbers = {
"0": "zero",
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five",
"6": "six",
"7": "seven",
"8": "eight",
"9": "nine",
};
if (name in map)
return map[name];
if (name in numbers)
return numbers[name];
// distingush between upper and lower case
if (isUpperCase(name))
return `upper-${name}`;
return `lower-${name}`;
};
exports.validateName = validateName;
const isUpperCase = (char) => {
return char === char.toUpperCase();
};
/**
* Returns a random entity path for a given user and character name.
* @param userName - The name of the user.
* @param charName - The title of the character.
* @returns A promise that resolves to a string representing the path to the random entity or empty.
*/
const getRandomEntityPath = (userName, charName) => __awaiter(void 0, void 0, void 0, function* () {
try {
const basePath = `${paths_1.PROFILES_PATH}/${userName}/${charName}`;
const characters = yield fs_1.default.promises.readdir(basePath);
const randomIndex = Math.floor(Math.random() * characters.length);
return `${basePath}/${characters[randomIndex]}`;
}
catch (err) {
console.error("Could not get random entity path");
return "";
}
});
/**
* Checks if a given character path contains any special characters.
* Special characters include: questionMark, asterisk, slash, backslash, colon, pipe, lessThan, greaterThan,
* doubleQuote, singleQuote, at, hash, dollar, percent, caret, ampersand, leftParenthesis, rightParenthesis,
* hyphen, underscore, equal, plus, leftCurlyBrace, rightCurlyBrace, leftSquareBracket, rightSquareBracket,
* comma, period, exclamationMark, tilde, graveAccent.
*
* @param charPath - The character path to check.
* @returns True if the character path contains any special characters, false otherwise.
*/
const isSpecialChar = (charPath) => {
const specialLocatedTop = [
"singleQuote",
"doubleQuote",
"graveAccent",
"asterisk",
"caret",
];
const specialLocatedMiddle = [
"colon",
"lessThan",
"greaterThan",
"leftParenthesis",
"rightParenthesis",
"hyphen",
"equal",
"plus",
"leftCurlyBrace",
"rightCurlyBrace",
"leftSquareBracket",
"rightSquareBracket",
"exclamationMark",
"tilde",
];
const specialLocatedBottom = ["underscore", "comma", "period"];
let isSpecial = false;
let position = "bottom";
for (const special of specialLocatedTop) {
if (charPath.includes(special)) {
isSpecial = true;
position = "top";
break;
}
}
if (!isSpecial) {
for (const special of specialLocatedMiddle) {
if (charPath.includes(special)) {
isSpecial = true;
position = "middle";
break;
}
}
}
if (!isSpecial) {
for (const special of specialLocatedBottom) {
if (charPath.includes(special)) {
isSpecial = true;
position = "bottom";
break;
}
}
}
return { isSpecial, position };
};
/**
* Determines if the given character path should extend below the baseline.
* @param charPath The character path to check.
* @returns A boolean indicating whether the character path should extend below the baseline.
*/
const extendBelowBaseline = (charPath) => {
const extendBelowBaseline = [
"lower-q",
"lower-y",
"lower-g",
"lower-p",
"lower-j",
];
let extend = false;
for (const char of extendBelowBaseline) {
if (charPath.includes(char)) {
extend = true;
break;
}
}
return extend;
};
/**
* Returns an object containing an array of SVG paths for each word in the input text and an array of missing characters.
* @param userName - The name of the user.
* @param text - The input text to generate SVG paths for.
* @returns An object containing an array of SVG paths for each word in the input text and an array of missing characters.
*/
const svgLinePathNames = (userName, text) => __awaiter(void 0, void 0, void 0, function* () {
let paths = [];
let missing = [];
const words = text.split(" ");
for (const word of words) {
let wordPath = [];
const chars = word.trim().split("");
for (const c of chars) {
const cName = validateName(c);
const path = yield getRandomEntityPath(userName, cName);
if (path === "") {
missing.push(c);
}
else {
wordPath.push(path);
}
}
paths.push(wordPath);
}
return {
missing,
paths,
};
});
exports.svgLinePathNames = svgLinePathNames;
/**
* Parses an SVG string and returns the SVG element and its paths.
* @param svg - The SVG string to parse.
* @returns An object containing the SVG element and its paths.
*/
const parseSVG = (svg) => {
const dom = new jsdom_1.default.JSDOM(svg);
const svgElement = dom.window.document.querySelector("svg");
const svgPaths = svgElement === null || svgElement === void 0 ? void 0 : svgElement.querySelectorAll("path");
return {
parent: svgElement,
paths: svgPaths,
};
};
/**
* Returns a random number between the given minimum and maximum values, with an optional percentage range.
* @param min The minimum value for the random number.
* @param max The maximum value for the random number.
* @param percentage The percentage range for the random number. Defaults to 25%.
* @returns A random number between the given minimum and maximum values.
*/
const getRandomNumber = (min, max, percentage = 25, scale = 1) => {
const howRandom = Math.round((max - min) * (percentage / 100));
const randomNumber = Math.floor(Math.random() * (howRandom + 1));
// const randomSign = Math.random() < 0.5 ? -1 : 1;
return Math.round(min + randomNumber) * scale;
};
// Get standard values for the characters
const getStandardValues = (isSpecial, position) => {
// Standard values for the characters
const standard = {
char_width: 20,
space_width: 10,
special_char_located_top_width: 5,
special_char_located_middle_width: 15,
special_char_located_top_max_width: 10,
special_char_located_middle_max_width: 20,
special_char_located_bottom_width: 5,
special_char_located_bottom_max_width: 15,
special_char_height_top: 10,
special_char_height_middle: 20,
special_char_height_bottom: 30,
max_char_width: 30,
max_char_height: 30,
};
const standerdWidth = isSpecial
? position === "top"
? standard.special_char_located_top_width
: position === "middle"
? standard.special_char_located_middle_width
: standard.special_char_located_bottom_width
: standard.char_width;
const standerdMaxWidth = isSpecial
? position === "top"
? standard.special_char_located_top_max_width
: position === "middle"
? standard.special_char_located_middle_max_width
: standard.special_char_located_bottom_max_width
: standard.max_char_width;
const standerdHeight = isSpecial
? position === "top"
? standard.special_char_height_top
: position === "middle"
? standard.special_char_height_middle
: standard.special_char_height_bottom
: standard.max_char_height;
const standerdMaxHeight = isSpecial
? position === "top"
? standard.special_char_height_top
: position === "middle"
? standard.special_char_height_middle
: standard.special_char_height_bottom
: standard.max_char_height;
return { standerdWidth, standerdMaxWidth, standerdHeight, standerdMaxHeight };
};
// Get Random Defects
const getRandomDefects = (defects, scaleFactor, charPath = "") => {
const { baseline, kerning, letterSize, lineSpacing, indent } = defects;
const { isSpecial, position } = isSpecialChar(charPath);
const { standerdWidth, standerdMaxWidth, standerdHeight, standerdMaxHeight } = getStandardValues(isSpecial, position);
const indentRandom = getRandomNumber(0, 80, indent, scaleFactor);
const lineSpacingRandom = getRandomNumber(0, 30, lineSpacing, scaleFactor);
const kerningDeffects = getRandomNumber(0, 10, kerning, scaleFactor);
const baselineOffset = getRandomNumber(0, 10, baseline, scaleFactor);
const letterSizeWidthRandom = getRandomNumber(standerdWidth, standerdMaxWidth, letterSize, scaleFactor);
const letterSizeRandomHeight = getRandomNumber(standerdHeight, standerdMaxHeight, letterSize, scaleFactor);
return {
indentRandom,
lineSpacingRandom,
kerningDeffects,
baselineOffset,
letterSizeWidthRandom,
letterSizeRandomHeight,
};
};
/**
* Assembles a word by processing each character and generating SVG elements.
*
* @param {AssembleWord} options - The options for assembling the word.
* @param {string} options.word - The word to assemble.
* @param {number} options.offsetX - The initial X offset.
* @param {number} options.offsetY - The initial Y offset.
* @param {number} options.scaleFactor - The scale factor for the word.
* @param {number} options.indentRandom - The random indentation for the word.
* @param {Defects} options.defects - The defects for the word.
*
* @returns {Object} - The assembled word elements, the height of the word, and the updated X offset.
*/
const assembleWord = ({ word, offsetX, offsetY, scaleFactor, indentRandom, defects, }) => {
const space_width = 10 * scaleFactor;
let wordElements = [];
let wordHeight = 0;
if (word.length === 0) {
offsetX += space_width;
}
else {
offsetX += indentRandom;
for (let j = 0; j < word.length; j++) {
const char = word[j];
const { kerningDeffects, baselineOffset } = getRandomDefects(defects, scaleFactor);
const { isSpecial, position } = isSpecialChar(char);
const { letterSizeWidthRandom, letterSizeRandomHeight } = getRandomDefects(defects, scaleFactor, char);
// You need to load the SVG content from the file
const svgFileContent = fs_1.default.readFileSync(char, "utf-8");
// Get the width and height of the SVG and its paths children
const { parent } = parseSVG(svgFileContent);
const width = parent === null || parent === void 0 ? void 0 : parent.getAttribute("width");
const height = parent === null || parent === void 0 ? void 0 : parent.getAttribute("height");
// Scale down the width to the standerd width while keeping the aspect ratio
const widthScale = letterSizeWidthRandom / Number(width);
const heightScale = letterSizeRandomHeight / Number(height);
const scale = Math.min(widthScale, heightScale);
// Calculate the scaled width and height
const scaledHeight = Number(height) * scale * scaleFactor;
const scaledWidth = Number(width) * scale * scaleFactor;
// Change the width and height of the SVG
parent === null || parent === void 0 ? void 0 : parent.setAttribute("width", String(scaledWidth));
parent === null || parent === void 0 ? void 0 : parent.setAttribute("height", String(scaledHeight));
// Add viewBox attribute to scale the paths inside the SVG
parent === null || parent === void 0 ? void 0 : parent.setAttribute("viewBox", `0 0 ${width} ${height}`);
// Change the position of the SVG
parent === null || parent === void 0 ? void 0 : parent.setAttribute("x", offsetX.toString());
parent === null || parent === void 0 ? void 0 : parent.setAttribute("y", String(offsetY + baselineOffset));
// Add the SVG content to the SVG content variable
offsetX += scaledWidth + kerningDeffects;
wordElements.push({
element: parent,
isSpecial,
position,
extendBelowBaseline: extendBelowBaseline(char),
});
wordHeight = Math.max(wordHeight, scaledHeight + baselineOffset);
}
// Align the line elements to the bottom of the line
let extended = false;
wordElements.forEach((e) => {
const { element, isSpecial, position, extendBelowBaseline } = e;
const elementHeight = parseInt(element.getAttribute("height"));
const lineYOffset = wordHeight - elementHeight;
if (isSpecial) {
if (position === "top") {
element.setAttribute("y", String(offsetY));
}
else if (position === "middle") {
element.setAttribute("y", String(offsetY + lineYOffset / 2));
}
else {
element.setAttribute("y", String(offsetY + lineYOffset));
}
}
else {
if (extendBelowBaseline) {
element.setAttribute("y", String(offsetY + lineYOffset + wordHeight / 2));
extended = true;
}
else {
element.setAttribute("y", String(offsetY + lineYOffset));
}
}
});
// Fix the line height
if (extended)
wordHeight += wordHeight / 2;
// Add a space between words
offsetX += space_width * scaleFactor;
}
return { wordElements, wordHeight, offsetX };
};
/**
* Assembles a line of text into SVG elements.
*
* @param {AssembleLine} options - The options for assembling the line.
* @param {number} options.defects - The number of defects in the line.
* @param {number} options.scaleFactor - The scale factor for the line.
* @param {string[]} options.line - The words in the line.
* @param {number} options.offsetY - The vertical offset of the line.
* @returns {Object} - The assembled line content and updated vertical offset.
*/
const assembleLine = ({ defects, scaleFactor, line, offsetY, }) => {
const { indentRandom, lineSpacingRandom } = getRandomDefects(defects, scaleFactor);
let lineContent = "";
// Add a line container for each line of text
let lineHeight = 0;
let offsetX = indentRandom;
let lineElements = [];
for (let i = 0; i < line.length; i++) {
const word = line[i];
const { wordElements, wordHeight, offsetX: newOffsetX, } = assembleWord({
word,
offsetX,
offsetY,
scaleFactor,
indentRandom,
defects,
});
// Update the offset
offsetX = newOffsetX;
lineHeight = Math.max(lineHeight, wordHeight);
lineElements = lineElements.concat(wordElements);
}
// Update the offset
offsetY += lineHeight + lineSpacingRandom;
// Append the line elements to the SVG content
lineElements.forEach((e) => {
lineContent += e.element.outerHTML;
});
return { lineContent, offsetY, offsetX };
};
/**
* Writes the SVG content to a file and returns the server file path.
* @param svgContent - The SVG content to be written to the file.
* @returns The server file path of the generated SVG file.
*/
const writeSVG = (svgContent, totalHeight, totalWidth) => __awaiter(void 0, void 0, void 0, function* () {
// wrap the SVG content in an SVG document
const outputFile = `<svg width="${totalWidth}" height="${totalHeight}" xmlns="http://www.w3.org/2000/svg">${svgContent}</svg>`; // Change this to your desired SVG content
// Write the SVG content to a file
const svgFilePath = `${paths_1.STATIC_PATH}/generated.svg`; // Change this to your desired file path
fs_1.default.writeFileSync(svgFilePath, outputFile);
// Return the SVG file path
const basePath = `${process.env.BASE_URL || "http://localhost"}`;
const port = process.env.PORT || "5000";
// Date.now() is used to prevent caching (cache busting)
const serverFilePath = `${basePath}:${port}/static/generated.svg?v=${Date.now()}`;
return serverFilePath;
});
/**
* Generates an SVG file based on the provided paths, scale factor, and defects.
* @param paths - A 3D array of file paths representing the characters to be included in the SVG.
* @param scaleFactor - The scale factor to apply to the SVG. Default is 1.
* @param defects - An object containing defect values for line spacing, kerning, letter size, and baseline offset.
* @returns A Promise that resolves to the file path of the generated SVG.
*/
const generateSvg = (paths, scaleFactor = 1, defects) => __awaiter(void 0, void 0, void 0, function* () {
let svgContent = "";
let offsetY = 0;
let totalHeight = 0;
let totalWidth = 0;
// Iterate over the lines, words and chars creating the SVG content
paths.forEach((line) => {
const { lineContent, offsetY: newOffsetY, offsetX, } = assembleLine({
defects,
scaleFactor,
line,
offsetY,
});
svgContent += lineContent;
offsetY = newOffsetY;
totalHeight = Math.max(totalHeight, offsetY);
totalWidth = Math.max(totalWidth, offsetX);
});
// Write the SVG content to a file
const serverFilePath = yield writeSVG(svgContent, totalHeight, totalWidth);
return serverFilePath;
});
exports.generateSvg = generateSvg;
/**
* Renders an SVG file to a connected plotter.
* @param inputPath - The path to the input SVG file.
* @param outputPath - Optional. The path to the output SVG file.
* @returns An error if one occurs during the rendering process.
*/
const printSVG = (inputPath) => __awaiter(void 0, void 0, void 0, function* () {
// Execute the following command : axicli inputPath usin os.system
const { execSync } = require("child_process");
try {
const command = `axicli ${inputPath}`;
const result = execSync(command, { encoding: "utf-8" });
// Process the result and return an object
return {
success: true,
message: `success: ${result}`,
};
}
catch (error) {
const errorMessage = error;
return {
success: false,
message: `error: ${errorMessage.message}`,
};
}
});
exports.printSVG = printSVG;
// Function to create the character directory
const createCharacterDirectory = (userPath, charTitle) => __awaiter(void 0, void 0, void 0, function* () {
const characters = yield fs_1.default.promises.readdir(userPath);
if (!characters.includes(charTitle)) {
yield fs_1.default.promises.mkdir(`${userPath}/${charTitle}`);
}
});
exports.createCharacterDirectory = createCharacterDirectory;
// Function to generate a unique SVG filename
const generateUniqueSVGPath = (characterDir) => __awaiter(void 0, void 0, void 0, function* () {
const characterLength = yield numberOfFiles(characterDir);
return `${characterDir}/${characterLength + 1}.svg`;
});
exports.generateUniqueSVGPath = generateUniqueSVGPath;
// Function to write SVG content to a file
const writeSVGToFile = (svgPath, svgContent) => __awaiter(void 0, void 0, void 0, function* () {
yield fs_1.default.promises.writeFile(svgPath, svgContent);
});
exports.writeSVGToFile = writeSVGToFile;