557 lines
22 KiB
JavaScript
557 lines
22 KiB
JavaScript
"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;
|