"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 = `${svgContent}`; // 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;