"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.parseSVG = exports.fetchSVGContent = exports.warpSvg = exports.getRandomEntityPath = 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 ""; } }); exports.getRandomEntityPath = getRandomEntityPath; /** * 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", "questionMark", "slash", "backslash", "pipe", "exclamationMark", "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 }; }; const specialWidthGlyphs = (glyphPath) => { const symbols = { colon: { width: 5, maxWidth: 7, height: 15, maxHeight: 20, }, lessThan: { width: 15, height: 20, maxWidth: 25, maxHeight: 30, }, greaterThan: { width: 15, height: 20, maxWidth: 25, maxHeight: 30, }, leftParenthesis: { width: 8, height: 20, maxWidth: 16, maxHeight: 40, }, rightParenthesis: { width: 8, height: 20, maxWidth: 16, maxHeight: 40, }, hyphen: { width: 10, height: 2, maxWidth: 20, maxHeight: 4, }, equal: { width: 10, height: 5, maxWidth: 20, maxHeight: 10, }, plus: { width: 10, height: 10, maxWidth: 20, maxHeight: 20, }, leftCurlyBrace: { width: 12, height: 25, maxWidth: 24, maxHeight: 50, }, rightCurlyBrace: { width: 12, height: 25, maxWidth: 24, maxHeight: 50, }, leftSquareBracket: { width: 8, height: 20, maxWidth: 16, maxHeight: 40, }, rightSquareBracket: { width: 8, height: 20, maxWidth: 16, maxHeight: 40, }, exclamationMark: { width: 6, height: 30, maxWidth: 9, maxHeight: 30, }, tilde: { width: 20, height: 8, maxWidth: 30, maxHeight: 10, }, comma: { width: 5, height: 10, maxWidth: 10, maxHeight: 20, }, dot: { width: 5, height: 5, maxWidth: 10, maxHeight: 10, }, questionMark: { width: 10, height: 30, maxWidth: 20, maxHeight: 40, }, underscore: { width: 10, height: 2, maxWidth: 20, maxHeight: 4, }, pipe: { width: 5, height: 30, maxWidth: 10, maxHeight: 40, }, one: { width: 5, height: 30, maxWidth: 10, maxHeight: 40, }, "lower-m": { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }, "lower-q": { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }, "lower-y": { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }, "lower-g": { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }, "lower-p": { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }, "lower-j": { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }, "lower-i": { width: 5, height: 30, maxWidth: 10, maxHeight: 40, }, "lower-l": { width: 10, height: 40, maxWidth: 10, maxHeight: 40, }, }; let isSpecialGlyph = false; let values = { width: 20, height: 30, maxWidth: 30, maxHeight: 40, }; for (const symbol in symbols) { if (glyphPath.includes(symbol)) { isSpecialGlyph = true; values = symbols[symbol]; break; } } return { isSpecialGlyph, values }; }; /** * 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, { contentType: "image/svg+xml" }); const svgElement = dom.window.document.querySelector("g"); return { parent: svgElement, }; }; exports.parseSVG = parseSVG; /** * 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)); 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, char_height: 30, 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: 10, special_char_height_top: 10, special_char_height_middle: 20, special_char_height_bottom: 30, max_char_width: 30, max_char_height: 40, }; 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.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 { isSpecialGlyph, values } = specialWidthGlyphs(charPath); if (isSpecialGlyph) { const { width, height, maxWidth, maxHeight } = values; const letterSizeWidthRandom = getRandomNumber(width, maxWidth, letterSize, scaleFactor); const letterSizeRandomHeight = getRandomNumber(height, maxHeight, letterSize, scaleFactor); return { indentRandom: 0, lineSpacingRandom: 0, kerningDeffects: 0, baselineOffset: 0, letterSizeWidthRandom, letterSizeRandomHeight, }; } 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(5, -5, 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, }; }; const fetchSVGContent = (path) => { return fs_1.default.readFileSync(path, "utf-8"); }; exports.fetchSVGContent = fetchSVGContent; /** * 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 = 30; 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 } = 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 = fetchSVGContent(char); // 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); // Calculate the scaled width and height // const scale = Math.pow( // Math.min(widthScale, heightScale), // 1 / scaleFactor // ); const scale = scaleFactor; const scaledWidth = Number(width) * scale; const scaledHeight = Number(height) * scale; // 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 translation and scale to the SVG content parent === null || parent === void 0 ? void 0 : parent.setAttribute("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`); // 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); } // 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 = []; // Detect the empty line const lineArray = line.flat(); if (lineArray.length === 0) { offsetY += 50 * scaleFactor + lineSpacingRandom; return { lineContent, offsetY, offsetX }; } 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); } // Align the line elements to the bottom of the line const aligned = verticalAlign(lineElements, lineHeight, offsetY, scaleFactor, defects); [lineElements, lineHeight] = [aligned.lineElements, aligned.lineHeight]; offsetY += lineHeight || 10 * scaleFactor + lineSpacingRandom; // Append the line elements to the SVG content lineElements.forEach((e) => { lineContent += e.element.outerHTML; }); return { lineContent, offsetY, offsetX }; }; const verticalAlign = (lineElements, lineHeight, offsetY, scaleFactor, defects) => { // Align the line elements to the bottom of the line let extended = false; const extendValue = 10 * scaleFactor; lineElements.forEach((e) => { const { element, isSpecial, position, extendBelowBaseline } = e; // Match all numbers in style attribute const style = element.getAttribute("transform"); const regex = /[-+]?[0-9]*\.?[0-9]+/g; const { baselineOffset } = getRandomDefects(defects, scaleFactor); const [x, y, s] = style.match(regex); const elementHeight = parseInt(element.getAttribute("height")); const lineYOffset = Math.abs(lineHeight - elementHeight); const elementOffset = Math.abs(lineHeight - elementHeight); if (isSpecial) { if (position === "top") { element.setAttribute("transform", `translate(${x}, ${offsetY}) scale(${s})`); } else if (position === "middle") { element.setAttribute("transform", `translate(${x}, ${Number(y) + lineYOffset / 2}) scale(${s})`); } else { element.setAttribute("transform", `translate(${x}, ${Number(y) + lineYOffset}) scale(${s})`); } } else { if (extendBelowBaseline) { element.setAttribute("transform", `translate(${x}, ${Number(y) + lineYOffset + extendValue}) scale(${s})`); extended = true; } else { element.setAttribute("transform", `translate(${x}, ${Number(y) + elementOffset + baselineOffset}) scale(${s})`); } } }); // Fix the line height if (extended) lineHeight += extendValue; // Add line margin lineHeight += 3 * scaleFactor; return { lineElements, lineHeight }; }; const warpSvg = (svg, width, height) => { // Create empty SVG document const dom = new jsdom_1.default.JSDOM(); const svgWrapper = dom.window.document.createElementNS("http://www.w3.org/2000/svg", "svg"); // Set the SVG width and height svgWrapper.setAttribute("width", String(width)); svgWrapper.setAttribute("height", String(height)); // Add the SVG content to the SVG document svgWrapper.innerHTML = svg; return svgWrapper === null || svgWrapper === void 0 ? void 0 : svgWrapper.outerHTML; }; exports.warpSvg = warpSvg; /** * 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 // const outputFile = svgFlatten(svgContent).pathify().value(); // 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; }); const generateSvg = (options) => __awaiter(void 0, void 0, void 0, function* () { const { paths, scaleFactor = 1, defects } = options; 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, totalWidth, totalHeight }; }); 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}.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;