From 2bfd09369ab95cd24296e539ee40a7f40d77f93d Mon Sep 17 00:00:00 2001 From: RandomYang Date: Mon, 9 Mar 2026 22:50:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=E5=85=A8=E9=A1=B9=E7=9B=AE=20T?= =?UTF-8?q?ypeScript=20=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将所有 .mjs 源文件和测试文件迁移到 TypeScript: - 新增 src/types.ts 共享类型定义(Matrix, ViewBox, LayerDescriptor, TextInfo 等) - 所有 src/, web/, test/ 下 .mjs → .ts,添加完整类型注解 - 新增 tsconfig.json(Node16 模块)和 web/tsconfig.json(Bundler 模式) - package.json 更新:tsx 运行入口,新增 typecheck 脚本 - 全部 155 个测试通过 Co-Authored-By: Claude Opus 4.6 --- package.json | 10 +- pnpm-lock.yaml | 346 +++++++++++++++++- src/{converter.mjs => converter.ts} | 68 ++-- src/core/converter-core.mjs | 74 ---- src/core/converter-core.ts | 61 +++ src/core/parser-core.mjs | 34 -- src/core/parser-core.ts | 21 ++ .../{renderer-core.mjs => renderer-core.ts} | 73 ++-- src/{index.mjs => index.ts} | 18 +- src/psd/{builder.mjs => builder.ts} | 74 ++-- src/psd/{effects.mjs => effects.ts} | 22 +- src/psd/{text-layer.mjs => text-layer.ts} | 46 +-- .../{batch-renderer.mjs => batch-renderer.ts} | 44 +-- src/render/renderer.mjs | 48 --- src/render/renderer.ts | 40 ++ src/svg/parser.mjs | 39 -- src/svg/parser.ts | 27 ++ .../{style-resolver.mjs => style-resolver.ts} | 51 +-- .../{text-extractor.mjs => text-extractor.ts} | 133 +++---- src/svg/{transforms.mjs => transforms.ts} | 38 +- src/svg/{walker.mjs => walker.ts} | 83 ++--- src/types.ts | 125 +++++++ src/utils/{color.mjs => color.ts} | 29 +- src/utils/{font.mjs => font.ts} | 27 +- src/utils/{validation.mjs => validation.ts} | 11 +- ...r-core.test.mjs => converter-core.test.ts} | 22 +- ...rser-core.test.mjs => parser-core.test.ts} | 4 +- ...er-core.test.mjs => renderer-core.test.ts} | 31 +- .../{convert.test.mjs => convert.test.ts} | 5 +- .../psd/{builder.test.mjs => builder.test.ts} | 44 +-- .../psd/{effects.test.mjs => effects.test.ts} | 2 +- ...text-layer.test.mjs => text-layer.test.ts} | 96 +++-- .../{renderer.test.mjs => renderer.test.ts} | 15 +- test/svg/{parser.test.mjs => parser.test.ts} | 2 +- ...solver.test.mjs => style-resolver.test.ts} | 6 +- ...ractor.test.mjs => text-extractor.test.ts} | 57 +-- ...transforms.test.mjs => transforms.test.ts} | 22 +- test/svg/{walker.test.mjs => walker.test.ts} | 9 +- test/utils/{color.test.mjs => color.test.ts} | 2 +- test/utils/{font.test.mjs => font.test.ts} | 2 +- ...validation.test.mjs => validation.test.ts} | 2 +- tsconfig.json | 20 + web/{app.mjs => app.ts} | 94 +++-- web/index.html | 2 +- web/platform/parser.mjs | 24 -- web/platform/parser.ts | 14 + .../{psd-writer.mjs => psd-writer.ts} | 21 +- web/platform/renderer.mjs | 64 ---- web/platform/renderer.ts | 57 +++ web/tsconfig.json | 14 + web/{vite.config.mjs => vite.config.ts} | 0 51 files changed, 1191 insertions(+), 982 deletions(-) rename src/{converter.mjs => converter.ts} (61%) delete mode 100644 src/core/converter-core.mjs create mode 100644 src/core/converter-core.ts delete mode 100644 src/core/parser-core.mjs create mode 100644 src/core/parser-core.ts rename src/core/{renderer-core.mjs => renderer-core.ts} (56%) rename src/{index.mjs => index.ts} (79%) rename src/psd/{builder.mjs => builder.ts} (59%) rename src/psd/{effects.mjs => effects.ts} (59%) rename src/psd/{text-layer.mjs => text-layer.ts} (69%) rename src/render/{batch-renderer.mjs => batch-renderer.ts} (54%) delete mode 100644 src/render/renderer.mjs create mode 100644 src/render/renderer.ts delete mode 100644 src/svg/parser.mjs create mode 100644 src/svg/parser.ts rename src/svg/{style-resolver.mjs => style-resolver.ts} (58%) rename src/svg/{text-extractor.mjs => text-extractor.ts} (64%) rename src/svg/{transforms.mjs => transforms.ts} (68%) rename src/svg/{walker.mjs => walker.ts} (65%) create mode 100644 src/types.ts rename src/utils/{color.mjs => color.ts} (91%) rename src/utils/{font.mjs => font.ts} (82%) rename src/utils/{validation.mjs => validation.ts} (84%) rename test/core/{converter-core.test.mjs => converter-core.test.ts} (84%) rename test/core/{parser-core.test.mjs => parser-core.test.ts} (96%) rename test/core/{renderer-core.test.mjs => renderer-core.test.ts} (84%) rename test/integration/{convert.test.mjs => convert.test.ts} (96%) rename test/psd/{builder.test.mjs => builder.test.ts} (50%) rename test/psd/{effects.test.mjs => effects.test.ts} (98%) rename test/psd/{text-layer.test.mjs => text-layer.test.ts} (53%) rename test/render/{renderer.test.mjs => renderer.test.ts} (82%) rename test/svg/{parser.test.mjs => parser.test.ts} (96%) rename test/svg/{style-resolver.test.mjs => style-resolver.test.ts} (94%) rename test/svg/{text-extractor.test.mjs => text-extractor.test.ts} (88%) rename test/svg/{transforms.test.mjs => transforms.test.ts} (80%) rename test/svg/{walker.test.mjs => walker.test.ts} (93%) rename test/utils/{color.test.mjs => color.test.ts} (97%) rename test/utils/{font.test.mjs => font.test.ts} (98%) rename test/utils/{validation.test.mjs => validation.test.ts} (95%) create mode 100644 tsconfig.json rename web/{app.mjs => app.ts} (64%) delete mode 100644 web/platform/parser.mjs create mode 100644 web/platform/parser.ts rename web/platform/{psd-writer.mjs => psd-writer.ts} (54%) delete mode 100644 web/platform/renderer.mjs create mode 100644 web/platform/renderer.ts create mode 100644 web/tsconfig.json rename web/{vite.config.mjs => vite.config.ts} (100%) diff --git a/package.json b/package.json index b4b16c8..9851a07 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,14 @@ "packageManager": "pnpm@10.30.2", "version": "1.0.0", "description": "", - "main": "src/index.mjs", + "main": "src/index.ts", "type": "module", "scripts": { - "start": "node src/index.mjs", + "start": "tsx src/index.ts", "test": "vitest run --silent", "dev": "vite web/", - "build:web": "vite build web/" + "build:web": "vite build web/", + "typecheck": "tsc --noEmit" }, "dependencies": { "@resvg/resvg-js": "^2.6.2", @@ -18,6 +19,9 @@ "ag-psd": "^30.1.0" }, "devDependencies": { + "@types/node": "^25.3.5", + "tsx": "^4.21.0", + "typescript": "^5.9.3", "vite": "^6.1.0", "vite-plugin-wasm": "^3.4.1", "vitest": "^4.0.18" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4774fb..243a7c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,15 +21,24 @@ importers: specifier: ^30.1.0 version: 30.1.0 devDependencies: + '@types/node': + specifier: ^25.3.5 + version: 25.3.5 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 vite: specifier: ^6.1.0 - version: 6.4.1 + version: 6.4.1(@types/node@25.3.5)(tsx@4.21.0) vite-plugin-wasm: specifier: ^3.4.1 - version: 3.5.0(vite@6.4.1) + version: 3.5.0(vite@6.4.1(@types/node@25.3.5)(tsx@4.21.0)) vitest: specifier: ^4.0.18 - version: 4.0.18 + version: 4.0.18(@types/node@25.3.5)(tsx@4.21.0) packages: @@ -39,156 +48,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -426,6 +591,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.3.5': + resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -481,6 +649,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -502,6 +675,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -530,6 +706,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -563,6 +742,19 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + vite-plugin-wasm@3.5.0: resolution: {integrity: sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ==} peerDependencies: @@ -652,81 +844,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} '@resvg/resvg-js-android-arm-eabi@2.6.2': @@ -868,6 +1138,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node@25.3.5': + dependencies: + undici-types: 7.18.2 + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -877,13 +1151,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@6.4.1)': + '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@25.3.5)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1 + vite: 6.4.1(@types/node@25.3.5)(tsx@4.21.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -951,6 +1225,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -964,6 +1267,10 @@ snapshots: fsevents@2.3.3: optional: true + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -986,6 +1293,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + resolve-pkg-maps@1.0.0: {} + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -1036,11 +1345,22 @@ snapshots: tinyrainbow@3.0.3: {} - vite-plugin-wasm@3.5.0(vite@6.4.1): + tsx@4.21.0: dependencies: - vite: 6.4.1 + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 - vite@6.4.1: + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + vite-plugin-wasm@3.5.0(vite@6.4.1(@types/node@25.3.5)(tsx@4.21.0)): + dependencies: + vite: 6.4.1(@types/node@25.3.5)(tsx@4.21.0) + + vite@6.4.1(@types/node@25.3.5)(tsx@4.21.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -1049,12 +1369,14 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 25.3.5 fsevents: 2.3.3 + tsx: 4.21.0 - vitest@4.0.18: + vitest@4.0.18(@types/node@25.3.5)(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@6.4.1) + '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@25.3.5)(tsx@4.21.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -1071,8 +1393,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1 + vite: 6.4.1(@types/node@25.3.5)(tsx@4.21.0) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.3.5 transitivePeerDependencies: - jiti - less diff --git a/src/converter.mjs b/src/converter.ts similarity index 61% rename from src/converter.mjs rename to src/converter.ts index 9666bfa..8c8fa0f 100644 --- a/src/converter.mjs +++ b/src/converter.ts @@ -1,19 +1,20 @@ -/** - * 主编排模块(Node.js 版) - * parse → walk → render → writePSD - */ - import fs from "fs"; import { initializeCanvas, writePsdBuffer } from "ag-psd"; -import { parseSvg } from "./svg/parser.mjs"; -import { buildTextLayer } from "./psd/text-layer.mjs"; -import { validateInput } from "./utils/validation.mjs"; -import { renderElement } from "./render/renderer.mjs"; -import { convertSvg } from "./core/converter-core.mjs"; +import { parseSvg } from "./svg/parser.js"; +import { buildTextLayer } from "./psd/text-layer.js"; +import { validateInput } from "./utils/validation.js"; +import { renderElement } from "./render/renderer.js"; +import { convertSvg } from "./core/converter-core.js"; + +interface MinimalCanvas { + width: number; + height: number; + getContext: () => Record; + toBuffer: () => Buffer; +} -// 初始化 ag-psd:提供最小化 canvas 实现(仅用于 imageData 写入) -function createMinimalCanvas(width, height) { - const canvas = { +function createMinimalCanvas(width: number, height: number): MinimalCanvas { + const canvas: MinimalCanvas = { width, height, getContext: () => createMinimalContext(canvas), @@ -22,17 +23,17 @@ function createMinimalCanvas(width, height) { return canvas; } -function createMinimalContext(canvas) { +function createMinimalContext(canvas: MinimalCanvas): Record { return { canvas, drawImage: () => {}, - getImageData: (x, y, w, h) => ({ + getImageData: (_x: number, _y: number, w: number, h: number) => ({ data: new Uint8ClampedArray(w * h * 4), width: w, height: h, }), putImageData: () => {}, - createImageData: (w, h) => ({ + createImageData: (w: number, h: number) => ({ data: new Uint8ClampedArray(w * h * 4), width: w, height: h, @@ -69,49 +70,43 @@ function createMinimalContext(canvas) { } initializeCanvas( - createMinimalCanvas, - (width, height) => ({ + createMinimalCanvas as unknown as Parameters[0], + (width: number, height: number) => ({ data: new Uint8ClampedArray(width * height * 4), width, height, - }) + }), ); -/** - * 将 SVG 文件转换为 PSD - * @param {string} inputPath - 输入 SVG 路径 - * @param {string} [outputPath] - 输出 PSD 路径 - * @param {Object} [options] - * @param {number} [options.scale=1] - 缩放因子 - * @returns {Promise} 输出文件路径 - */ -export async function convertSvgToPsd(inputPath, outputPath, options = {}) { +interface ConvertSvgToPsdOptions { + scale?: number; +} + +export async function convertSvgToPsd( + inputPath: string, + outputPath?: string | null, + options: ConvertSvgToPsdOptions = {}, +): Promise { const { scale = 1 } = options; - // 验证输入 validateInput(inputPath); - // 1. 解析 SVG const { svg, width, height, viewBox } = parseSvg(inputPath); console.log(`解析: ${inputPath} (${Math.round(width)}×${Math.round(height)})`); - // 2. 核心转换 - let completed = 0; const { psd, layerCount } = await convertSvg(svg, width, height, viewBox, { scale, renderElement, buildTextLayer: (desc, svgRoot, w, h, s) => buildTextLayer(desc, svgRoot, w, h, s), onProgress: (current, total) => { - completed = current; console.log(` 渲染: ${current}/${total} 图层`); }, }); console.log(`图层树: ${layerCount} 个图层`); - // 3. 写入 PSD - const buffer = writePsdBuffer(psd, { + const buffer = writePsdBuffer(psd as Parameters[0], { invalidateTextLayers: true, generateThumbnail: true, }); @@ -120,7 +115,8 @@ export async function convertSvgToPsd(inputPath, outputPath, options = {}) { outputPath = inputPath.replace(/\.svg$/i, ".psd"); } fs.writeFileSync(outputPath, buffer); - console.log(`完成: ${outputPath} (${psd.width}×${psd.height}, ${psd.children.length} 个顶层图层)`); + const psdObj = psd as { width: number; height: number; children: unknown[] }; + console.log(`完成: ${outputPath} (${psdObj.width}×${psdObj.height}, ${psdObj.children.length} 个顶层图层)`); return outputPath; } diff --git a/src/core/converter-core.mjs b/src/core/converter-core.mjs deleted file mode 100644 index 19587f6..0000000 --- a/src/core/converter-core.mjs +++ /dev/null @@ -1,74 +0,0 @@ -/** - * 转换核心编排逻辑(平台无关) - * 接收已解析的 SVG DOM,返回 ag-psd 文档对象 - */ - -import { walkSvg } from "../svg/walker.mjs"; -import { buildPsd } from "../psd/builder.mjs"; -import { extractTextInfo } from "../svg/text-extractor.mjs"; - -/** - * 将已解析的 SVG 转换为 ag-psd 文档对象 - * @param {Element} svg - SVG 根元素 - * @param {number} width - 画布宽度 - * @param {number} height - 画布高度 - * @param {{ x: number, y: number, w: number, h: number } | null} viewBox - * @param {Object} options - * @param {number} [options.scale=1] - 缩放因子 - * @param {Function} options.renderElement - 渲染函数 - * @param {Function} [options.buildTextLayer] - 文字图层构建函数 - * @param {Function} [options.onProgress] - 进度回调 (current, total) - * @returns {Promise<{ psd: Object, layerCount: number }>} - */ -export async function convertSvg(svg, width, height, viewBox, options = {}) { - const { scale = 1, renderElement, buildTextLayer, onProgress } = options; - - // 1. 递归遍历,生成 LayerDescriptor 树 - const descriptors = walkSvg(svg); - const layerCount = countAllLayers(descriptors); - - if (layerCount === 0) { - throw new Error("SVG 中没有可渲染的元素"); - } - - // 2. 提取文字信息(增强 text 类型的 descriptors) - enrichTextDescriptors(descriptors, svg, viewBox); - - // 3. 构建 PSD - const psd = await buildPsd(descriptors, svg, width, height, scale, { - buildTextLayer, - onProgress, - renderElement, - }); - - if (!psd.children || psd.children.length === 0) { - throw new Error("没有成功渲染的图层"); - } - - return { psd, layerCount }; -} - -/** - * 递归为 text 类型的 descriptor 填充 textInfo - */ -export function enrichTextDescriptors(descriptors, svgRoot, viewBox) { - for (const desc of descriptors) { - if (desc.type === "group" && desc.children) { - enrichTextDescriptors(desc.children, svgRoot, viewBox); - } else if (desc.type === "text" && desc.element) { - desc.textInfo = extractTextInfo(desc.element, desc.transform, viewBox); - } - } -} - -export function countAllLayers(descriptors) { - let count = 0; - for (const desc of descriptors) { - if (desc.type === "group" && desc.children) { - count += countAllLayers(desc.children); - } else { - count++; - } - } - return count; -} diff --git a/src/core/converter-core.ts b/src/core/converter-core.ts new file mode 100644 index 0000000..921c07d --- /dev/null +++ b/src/core/converter-core.ts @@ -0,0 +1,61 @@ +import type { ViewBox, LayerDescriptor, ConvertOptions } from "../types.js"; +import { walkSvg } from "../svg/walker.js"; +import { buildPsd } from "../psd/builder.js"; +import { extractTextInfo } from "../svg/text-extractor.js"; + +export async function convertSvg( + svg: Element, + width: number, + height: number, + viewBox: ViewBox | null, + options: ConvertOptions = { renderElement: () => null }, +): Promise<{ psd: Record; layerCount: number }> { + const { scale = 1, renderElement, buildTextLayer, onProgress } = options; + + const descriptors = walkSvg(svg); + const layerCount = countAllLayers(descriptors); + + if (layerCount === 0) { + throw new Error("SVG 中没有可渲染的元素"); + } + + enrichTextDescriptors(descriptors, svg, viewBox); + + const psd = await buildPsd(descriptors, svg, width, height, scale, { + buildTextLayer, + onProgress, + renderElement, + }); + + if (!(psd as { children?: unknown[] }).children || ((psd as { children: unknown[] }).children).length === 0) { + throw new Error("没有成功渲染的图层"); + } + + return { psd, layerCount }; +} + +export function enrichTextDescriptors( + descriptors: LayerDescriptor[], + svgRoot: Element, + viewBox: ViewBox | null, +): void { + for (const desc of descriptors) { + if (desc.type === "group" && desc.children) { + enrichTextDescriptors(desc.children, svgRoot, viewBox); + } else if (desc.type === "text" && desc.element) { + desc.textInfo = extractTextInfo(desc.element, desc.transform!, viewBox); + } + } +} + +export function countAllLayers(descriptors: LayerDescriptor[]): number { + let count = 0; + for (const desc of descriptors) { + if (desc.type === "group" && desc.children) { + count += countAllLayers(desc.children); + } else { + count++; + } + } + return count; +} diff --git a/src/core/parser-core.mjs b/src/core/parser-core.mjs deleted file mode 100644 index f555dcb..0000000 --- a/src/core/parser-core.mjs +++ /dev/null @@ -1,34 +0,0 @@ -/** - * SVG 解析核心逻辑(平台无关) - * 从已解析的 Document 中提取 SVG 信息 - */ - -/** - * 从 Document 提取 SVG 根元素和尺寸信息 - * @param {Document} doc - 已解析的 DOM 文档 - * @returns {{ doc: Document, svg: Element, width: number, height: number, viewBox: {x:number,y:number,w:number,h:number}|null }} - */ -export function parseSvgFromDocument(doc) { - const svg = doc.documentElement; - if (!svg || svg.nodeName !== "svg") { - throw new Error("无效的 SVG 文件:缺少 根元素"); - } - - const viewBox = parseViewBox(svg.getAttribute("viewBox")); - const width = parseFloat(svg.getAttribute("width")) || (viewBox ? viewBox.w : 800); - const height = parseFloat(svg.getAttribute("height")) || (viewBox ? viewBox.h : 600); - - return { doc, svg, width, height, viewBox }; -} - -/** - * 解析 viewBox 属性 - * @param {string|null} attr - * @returns {{ x: number, y: number, w: number, h: number } | null} - */ -export function parseViewBox(attr) { - if (!attr) return null; - const parts = attr.split(/[\s,]+/).map(Number); - if (parts.length < 4 || parts.some(isNaN)) return null; - return { x: parts[0], y: parts[1], w: parts[2], h: parts[3] }; -} diff --git a/src/core/parser-core.ts b/src/core/parser-core.ts new file mode 100644 index 0000000..c0633a3 --- /dev/null +++ b/src/core/parser-core.ts @@ -0,0 +1,21 @@ +import type { ViewBox, SvgParseResult } from "../types.js"; + +export function parseSvgFromDocument(doc: Document): SvgParseResult { + const svg = doc.documentElement; + if (!svg || svg.nodeName !== "svg") { + throw new Error("无效的 SVG 文件:缺少 根元素"); + } + + const viewBox = parseViewBox(svg.getAttribute("viewBox")); + const width = parseFloat(svg.getAttribute("width") || "") || (viewBox ? viewBox.w : 800); + const height = parseFloat(svg.getAttribute("height") || "") || (viewBox ? viewBox.h : 600); + + return { doc, svg, width, height, viewBox }; +} + +export function parseViewBox(attr: string | null): ViewBox | null { + if (!attr) return null; + const parts = attr.split(/[\s,]+/).map(Number); + if (parts.length < 4 || parts.some(isNaN)) return null; + return { x: parts[0], y: parts[1], w: parts[2], h: parts[3] }; +} diff --git a/src/core/renderer-core.mjs b/src/core/renderer-core.ts similarity index 56% rename from src/core/renderer-core.mjs rename to src/core/renderer-core.ts index 6c354d9..6c7d861 100644 --- a/src/core/renderer-core.mjs +++ b/src/core/renderer-core.ts @@ -1,35 +1,26 @@ -/** - * 渲染核心逻辑(平台无关) - * 纯函数:SVG 构建、透明检测、边界框计算、像素裁剪 - */ - -/** - * 构建只包含指定元素的完整 SVG 字符串 - * 保留原始 SVG 的 defs、style 和 viewBox - * @param {Element} element - * @param {Element} svgRoot - * @param {number[]} [transform] - 累积变换矩阵 [a,b,c,d,e,f] - * @param {function} serializeFn - 序列化函数 (node) => string - */ -export function buildStandaloneSvg(element, svgRoot, transform, serializeFn) { - const clone = svgRoot.cloneNode(false); // 浅克隆 属性 - - // 复制 和