From 670958d52c609dd1a8054b3b50f2c7dba7a21ffd Mon Sep 17 00:00:00 2001 From: codecalm Date: Sun, 14 Dec 2025 19:25:06 +0100 Subject: [PATCH] Refactor build-outline.mjs to optimize SVG processing and remove fix-outline.py. --- .../icons-webfont/.build/build-outline.mjs | 29 +++--- packages/icons-webfont/.build/fix-outline.mjs | 94 +++++++++++++++++++ packages/icons-webfont/.build/fix-outline.py | 17 ---- packages/icons-webfont/package.json | 3 +- pnpm-lock.yaml | 66 +++++++------ 5 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 packages/icons-webfont/.build/fix-outline.mjs delete mode 100644 packages/icons-webfont/.build/fix-outline.py diff --git a/packages/icons-webfont/.build/build-outline.mjs b/packages/icons-webfont/.build/build-outline.mjs index 9b69d24e5..a178ee41f 100644 --- a/packages/icons-webfont/.build/build-outline.mjs +++ b/packages/icons-webfont/.build/build-outline.mjs @@ -4,7 +4,8 @@ import fs from 'fs' import { resolve, basename } from 'path' import crypto from 'crypto' import { glob } from 'glob' -import { execSync } from 'child_process' +import { optimize } from 'svgo' +import { fixOutline } from './fix-outline.mjs' const DIR = getPackageDir('icons-webfont') @@ -75,24 +76,20 @@ const buildOutline = async () => { fixedWidth: false, color: 'black' }).then(outlined => { - // Save file - fs.writeFileSync(resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`), outlined, 'utf-8') + // Fix outline direction (using JS instead of fontforge) + const fixed = fixOutline(outlined) + + // Optimize with svgo (in memory, no subprocess) + const optimized = optimize(fixed, { multipass: true }).data + + // Prepare final content with hash + const finalContent = optimized.replace(/\n/g, ' ').trim() + const hashString = `` - // Fix outline - execSync(`fontforge -lang=py -script .build/fix-outline.py icons-outlined/${strokeName}/${type}/${filename}`).toString() - execSync(`svgo icons-outlined/${strokeName}/${type}/${filename}`).toString() - - // Add hash - const fixedFileContent = fs - .readFileSync(resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`), 'utf-8') - .replace(/\n/g, ' ') - .trim(), - hashString = `` - - // Save file + // Save file (single write instead of 3 file operations) fs.writeFileSync( resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`), - fixedFileContent + hashString, + finalContent + hashString, 'utf-8' ) }).catch(error => console.log(error)) diff --git a/packages/icons-webfont/.build/fix-outline.mjs b/packages/icons-webfont/.build/fix-outline.mjs new file mode 100644 index 000000000..1a429b695 --- /dev/null +++ b/packages/icons-webfont/.build/fix-outline.mjs @@ -0,0 +1,94 @@ +import fs from 'fs' +import SVGPathCommander from 'svg-path-commander' + +/** + * Fix SVG outline directions - replacement for fontforge fix-outline.py + * Operations: + * 1. Round coordinates + * 2. Correct path direction (outer paths counterclockwise, inner paths clockwise) + */ +export function fixOutline(svgContent) { + // Extract all path elements + const pathRegex = /]*\sd="([^"]+)"[^>]*>/g + + let result = svgContent.replace(pathRegex, (match, pathData) => { + try { + // Round coordinates to integers (like fontforge's round()) and optimize + const commander = new SVGPathCommander(pathData, { round: 0 }) + const optimized = commander.optimize().toString() + + // Check and correct direction + // For font glyphs: outer paths should be counterclockwise + const segments = new SVGPathCommander(optimized).segments + const isClockwise = getPathDirection(segments) + + let finalPath = optimized + + // If path is clockwise, reverse it to make it counterclockwise (standard for outer contours) + if (isClockwise) { + finalPath = new SVGPathCommander(optimized, { round: 0 }).reverse().toString() + } + + return match.replace(pathData, finalPath) + } catch (e) { + console.warn('Could not process path:', e.message) + return match + } + }) + + return result +} + +/** + * Calculate path direction using shoelace formula + * Returns true if clockwise, false if counterclockwise + */ +function getPathDirection(segments) { + let sum = 0 + let points = [] + + // Extract points from segments + for (const seg of segments) { + if (seg[0] === 'M' || seg[0] === 'L') { + points.push({ x: seg[1], y: seg[2] }) + } else if (seg[0] === 'C') { + // For curves, use the endpoint + points.push({ x: seg[5], y: seg[6] }) + } else if (seg[0] === 'Q') { + points.push({ x: seg[3], y: seg[4] }) + } else if (seg[0] === 'Z' || seg[0] === 'z') { + // Close path - use first point + if (points.length > 0) { + points.push(points[0]) + } + } + } + + // Calculate signed area using shoelace formula + for (let i = 0; i < points.length - 1; i++) { + sum += (points[i + 1].x - points[i].x) * (points[i + 1].y + points[i].y) + } + + // Positive = clockwise, negative = counterclockwise (in SVG coordinate system where Y increases downward) + return sum > 0 +} + +/** + * Process SVG file + */ +export function fixOutlineFile(inputPath, outputPath = null) { + const content = fs.readFileSync(inputPath, 'utf-8') + const fixed = fixOutline(content) + fs.writeFileSync(outputPath || inputPath, fixed, 'utf-8') + return fixed +} + +// CLI support +if (process.argv[1] && process.argv[1].endsWith('fix-outline.mjs')) { + const file = process.argv[2] + if (file) { + console.log(`Correcting outline for ${file}`) + fixOutlineFile(file) + console.log('Finished fixing svg outline directions!') + } +} diff --git a/packages/icons-webfont/.build/fix-outline.py b/packages/icons-webfont/.build/fix-outline.py deleted file mode 100644 index 0d7f927cb..000000000 --- a/packages/icons-webfont/.build/fix-outline.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -import sys -import fontforge - -file = sys.argv[1] - -font = fontforge.font() -print (f"Correcting outline for {file}") -glyph = font.createChar(123, file) -glyph.importOutlines("./" + file) -glyph.round() -glyph.simplify() -glyph.simplify() -glyph.correctDirection() -glyph.export("./" + file) - -print ("Finished fixing svg outline directions!") diff --git a/packages/icons-webfont/package.json b/packages/icons-webfont/package.json index 997fddb7d..c38d0699d 100644 --- a/packages/icons-webfont/package.json +++ b/packages/icons-webfont/package.json @@ -36,7 +36,8 @@ "style": "./tabler-icons.min.css", "dependencies": { "@tabler/icons": "3.35.0", - "sharp": "^0.33.5" + "sharp": "^0.33.5", + "svg-path-commander": "^2.1.11" }, "keywords": [ "icons", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46f91f941..8d9709e95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,7 +164,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.8.1 - version: 2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@5.4.20) + version: 2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.2.7) '@testing-library/preact': specifier: ^3.2.3 version: 3.2.4(preact@10.27.2) @@ -186,7 +186,7 @@ importers: version: 18.2.60 '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.7.0(vite@5.4.20) + version: 4.7.0(vite@7.2.7) react: specifier: 18.2.0 version: 18.2.0 @@ -211,7 +211,7 @@ importers: version: 18.2.60 '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.7.0(vite@5.4.20) + version: 4.7.0(vite@7.2.7) react: specifier: 18.2.0 version: 18.2.0 @@ -248,7 +248,7 @@ importers: version: 1.9.9 vite-plugin-solid: specifier: ^2.10.1 - version: 2.11.8(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vite@5.4.20) + version: 2.11.8(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vite@7.2.7) packages/icons-sprite: dependencies: @@ -357,6 +357,9 @@ importers: sharp: specifier: ^0.33.5 version: 0.33.5 + svg-path-commander: + specifier: ^2.1.11 + version: 2.1.11 devDependencies: sass: specifier: ^1.71.1 @@ -385,7 +388,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.8.1 - version: 2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@5.4.20) + version: 2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.2.7) test/test-react: dependencies: @@ -407,7 +410,7 @@ importers: version: 18.3.7(@types/react@18.2.60) '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.7.0(vite@5.4.20) + version: 4.7.0(vite@7.2.7) test/test-react-native: dependencies: @@ -429,7 +432,7 @@ importers: version: 18.3.7(@types/react@18.2.60) '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.7.0(vite@5.4.20) + version: 4.7.0(vite@7.2.7) test/test-svelte: dependencies: @@ -3806,7 +3809,7 @@ packages: config-chain: 1.1.13 dev: true - /@preact/preset-vite@2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@5.4.20): + /@preact/preset-vite@2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.2.7): resolution: {integrity: sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==} peerDependencies: '@babel/core': 7.x @@ -3815,13 +3818,13 @@ packages: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.4) - '@prefresh/vite': 2.4.10(preact@10.27.2)(vite@5.4.20) + '@prefresh/vite': 2.4.10(preact@10.27.2)(vite@7.2.7) '@rollup/pluginutils': 4.2.1 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.4) debug: 4.4.3 picocolors: 1.1.1 - vite: 5.4.20(sass@1.92.1) - vite-prerender-plugin: 0.5.12(vite@5.4.20) + vite: 7.2.7(sass@1.92.1) + vite-prerender-plugin: 0.5.12(vite@7.2.7) transitivePeerDependencies: - preact - supports-color @@ -3843,7 +3846,7 @@ packages: resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} dev: true - /@prefresh/vite@2.4.10(preact@10.27.2)(vite@5.4.20): + /@prefresh/vite@2.4.10(preact@10.27.2)(vite@7.2.7): resolution: {integrity: sha512-lt+ODASOtXRWaPplp7/DlrgAaInnQYNvcpCglQBMx2OeJPyZ4IqPRaxsK77w96mWshjYwkqTsRSHoAM7aAn0ow==} peerDependencies: preact: ^10.4.0 || ^11.0.0-0 @@ -3855,7 +3858,7 @@ packages: '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 preact: 10.27.2 - vite: 5.4.20(sass@1.92.1) + vite: 7.2.7(sass@1.92.1) transitivePeerDependencies: - supports-color dev: true @@ -4849,6 +4852,11 @@ packages: - '@vue/server-renderer' dev: true + /@thednp/dommatrix@2.0.12: + resolution: {integrity: sha512-eOshhlSShBXLfrMQqqhA450TppJXhKriaQdN43mmniOCMn9sD60QKF1Axsj7bKl339WH058LuGFS6H84njYH5w==} + engines: {node: '>=20', pnpm: '>=8.6.0'} + dev: false + /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -5033,7 +5041,7 @@ packages: '@types/yargs-parser': 21.0.3 dev: true - /@vitejs/plugin-react@4.7.0(vite@5.4.20): + /@vitejs/plugin-react@4.7.0(vite@7.2.7): resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -5045,7 +5053,7 @@ packages: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.20(sass@1.92.1) + vite: 7.2.7(sass@1.92.1) transitivePeerDependencies: - supports-color dev: true @@ -13259,6 +13267,13 @@ packages: - debug dev: true + /svg-path-commander@2.1.11: + resolution: {integrity: sha512-wmQ6QA3Od+HOcpIzLjPlbv59+x3yd3V5W6xitUOvAHmqZpP7wVrRM2CHqEm5viHUbZu6PjzFsjbTEFtIeUxaNA==} + engines: {node: '>=16', pnpm: '>=8.6.0'} + dependencies: + '@thednp/dommatrix': 2.0.12 + dev: false + /svg-pathdata@7.2.0: resolution: {integrity: sha512-qd+AxqMpfRrRQaWb2SrNFvn69cvl6piqY8TxhYl2Li1g4/LO5F9NJb5wI4vNwRryqgSgD43gYKLm/w3ag1bKvQ==} engines: {node: '>=20.11.1'} @@ -13942,7 +13957,7 @@ packages: - yaml dev: true - /vite-plugin-solid@2.11.8(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vite@5.4.20): + /vite-plugin-solid@2.11.8(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vite@7.2.7): resolution: {integrity: sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* @@ -13959,13 +13974,13 @@ packages: merge-anything: 5.1.7 solid-js: 1.9.9 solid-refresh: 0.6.3(solid-js@1.9.9) - vite: 5.4.20(sass@1.92.1) - vitefu: 1.1.1(vite@5.4.20) + vite: 7.2.7(sass@1.92.1) + vitefu: 1.1.1(vite@7.2.7) transitivePeerDependencies: - supports-color dev: true - /vite-prerender-plugin@0.5.12(vite@5.4.20): + /vite-prerender-plugin@0.5.12(vite@7.2.7): resolution: {integrity: sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==} peerDependencies: vite: 5.x || 6.x || 7.x @@ -13976,7 +13991,7 @@ packages: simple-code-frame: 1.3.0 source-map: 0.7.6 stack-trace: 1.0.0-pre2 - vite: 5.4.20(sass@1.92.1) + vite: 7.2.7(sass@1.92.1) dev: true /vite@5.4.20(sass@1.92.1): @@ -14080,17 +14095,6 @@ packages: vite: 5.4.20(sass@1.92.1) dev: true - /vitefu@1.1.1(vite@5.4.20): - resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - dependencies: - vite: 5.4.20(sass@1.92.1) - dev: true - /vitefu@1.1.1(vite@7.2.7): resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: