diff --git a/.vscode/settings.json b/.vscode/settings.json index b5c80331..58df4818 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,8 @@ "editor.detectIndentation": false, "rust-analyzer.linkedProjects": [ "packages/ducrs/Cargo.toml", - "packages/ducpdf/src/duc2pdf/Cargo.toml" + "packages/ducpdf/src/duc2pdf/Cargo.toml", + "packages/ducsvg/src/pdf2svg/Cargo.toml" ], "rust-analyzer.files.exclude": [ "packages/ducrs/src/duc_generated.rs" diff --git a/Cargo.lock b/Cargo.lock index fd081711..fe2ea735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -494,6 +500,76 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "hayro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048488ba88552bb0fb2a7e4001c64d5bed65d1a92167186a1bb9151571f32e60" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo 0.12.0", +] + +[[package]] +name = "hayro-font" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e7e97ce840a6a70e7901e240ec65ba61106b66b37a4a1b899a2ce484248463" +dependencies = [ + "log", + "phf", +] + +[[package]] +name = "hayro-interpret" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56204c972d08e844f3db13b1e14be769f846e576699b46d4f4637cc4f8f70102" +dependencies = [ + "bitflags 2.10.0", + "hayro-font", + "hayro-syntax", + "kurbo 0.12.0", + "log", + "moxcms", + "phf", + "rustc-hash", + "siphasher 1.0.1", + "skrifa", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-svg" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c673304cec6e0dfd3b4f71fccecd45646899aa70279b62d3f933842abc4ac5" +dependencies = [ + "base64 0.22.1", + "hayro-interpret", + "image", + "kurbo 0.12.0", + "siphasher 1.0.1", + "xmlwriter", +] + +[[package]] +name = "hayro-syntax" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9e5c7dbc0f11dc42775d1a6cc00f5f5137b90b6288dd7fe5f71d17b14d10be" +dependencies = [ + "flate2", + "kurbo 0.12.0", + "log", + "rustc-hash", + "smallvec", + "zune-jpeg", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -864,6 +940,18 @@ dependencies = [ "ryu", ] +[[package]] +name = "pdf2svg" +version = "0.0.0-development" +dependencies = [ + "hayro", + "hayro-svg", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "peniko" version = "0.4.1" @@ -882,6 +970,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -1272,6 +1403,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strict-num" version = "0.1.1" @@ -1355,6 +1492,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1820,6 +1968,29 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -1840,6 +2011,27 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 38826d68..37722724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ resolver = "2" members = [ "packages/ducrs", - "packages/ducpdf/src/duc2pdf" + "packages/ducpdf/src/duc2pdf", + "packages/ducsvg/src/pdf2svg", ] [workspace.dependencies] diff --git a/assets/testing/duc-files/blocks_instances.duc b/assets/testing/duc-files/blocks_instances.duc index 59aca168..a2b107c2 100644 Binary files a/assets/testing/duc-files/blocks_instances.duc and b/assets/testing/duc-files/blocks_instances.duc differ diff --git a/bun.lock b/bun.lock index a0f08e0b..ecd09e4e 100644 --- a/bun.lock +++ b/bun.lock @@ -109,7 +109,6 @@ "dependencies": { "ducjs": "workspace:*", "ducpdf": "workspace:*", - "pdf-into-svg": "1.0.1", }, "devDependencies": { "@xmldom/xmldom": "^0.8.11", @@ -1404,8 +1403,6 @@ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "pdf-into-svg": ["pdf-into-svg@1.0.1", "", {}, "sha512-O5y1KiFz5+QYLVIoO64kY8m7QOhfTyexgZysze5jJqkBZ1uqWQTLucmOSBGqphJhdkL+1zQdRMDlM5icHEQfCA=="], - "perfect-freehand": ["perfect-freehand@1.2.2", "", {}, "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ=="], "picocolors": ["picocolors@1.1.1", "", {}, ""], diff --git a/package.json b/package.json index e9783d19..bfa124ff 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "scripts": { "build": "turbo build", "build:packages": "turbo build --filter=ducjs --filter=ducpy --filter=ducrs --filter=ducsvg --filter=ducpdf", - "sst:wrap": "bash -c 'CMD=\"$1\"; shift; [ -n \"$1\" ] && [ \"$1\" != \"_\" ] && export SST_STAGE=\"$1\"; eval \"$CMD\"' _", + "sst:shell": "bash -c 'CMD=\"$1\"; shift; [ -n \"$1\" ] && [ \"$1\" != \"_\" ] && export SST_STAGE=\"$1\"; eval \"$CMD\"' _", "flatc:gen": "bun run flatc:gen:duc", "flatc:gen:duc": "cd schema && bash flatc-duc-generate.sh", "clean": "bun del-cli \"**/dist\" \".turbo\" \"**/node_modules\" \"**/package-lock.json\" \"**/tsconfig.tsbuildinfo\" \"**/tsconfig.node.tsbuildinfo\" \"**/vite.config.js\"", @@ -68,11 +68,11 @@ "ducpdf:build": "turbo build --filter=ducpdf", "ducpdf:test": "turbo run test --filter=ducpdf", "semantic-release": "turbo run semantic-release", - "sst:deploy": "bun sst:wrap 'bun run sst:clean && sst unlock && sst refresh && sst deploy'", + "sst:deploy": "bun sst:shell 'bun run sst:clean && sst unlock && sst refresh && sst deploy'", "sst:clean": "bun del-cli \".sst\" \"**/sst-env.d.ts\" \"**/sst.pyi\"", "sst:dev": "bun run sst dev", - "knip:deps": "bun sst:wrap 'bun run sst unlock && sst shell -- bunx knip --dependencies'", - "knip": "bun sst:wrap 'bun run sst unlock && sst shell -- bunx knip'" + "knip:deps": "bun sst:shell 'bun run sst unlock && sst shell -- bunx knip --dependencies'", + "knip": "bun sst:shell 'bun run sst unlock && sst shell -- bunx knip'" }, "workspaces": [ "apps/*", diff --git a/packages/ducpdf/src/duc2pdf/index.ts b/packages/ducpdf/src/duc2pdf/index.ts index f8dee7a8..b7733403 100644 --- a/packages/ducpdf/src/duc2pdf/index.ts +++ b/packages/ducpdf/src/duc2pdf/index.ts @@ -14,7 +14,7 @@ async function initWasm(): Promise { try { // Dynamically import the WASM JS bindings const wasmBindings: any = await import('../wasm'); - + // The wasm-pack generated module exports a default init function // that handles loading the WASM file using import.meta.url // This is the same pattern used by duc_renderer_bin diff --git a/packages/ducpdf/src/duc2pdf/src/lib.rs b/packages/ducpdf/src/duc2pdf/src/lib.rs index d8a0cd5d..ca5f2880 100644 --- a/packages/ducpdf/src/duc2pdf/src/lib.rs +++ b/packages/ducpdf/src/duc2pdf/src/lib.rs @@ -278,7 +278,11 @@ pub fn convert_duc_to_pdf_rs(duc_data: &[u8]) -> Vec { Ok(pdf_bytes) => pdf_bytes, Err(e) => { // Log error with context - error_handling::log_error_details(&e, duc_data.len(), "Standard conversion (default options)"); + error_handling::log_error_details( + &e, + duc_data.len(), + "Standard conversion (default options)", + ); // Create structured error info and convert to WASM bytes let error_info = error_handling::create_error_info(&e, duc_data.len(), None); @@ -390,7 +394,8 @@ pub fn convert_duc_to_pdf_crop_wasm( background_color: normalized_background, ..Default::default() }; - let error_info = error_handling::create_error_info(&e, duc_data.len(), Some(&crop_options)); + let error_info = + error_handling::create_error_info(&e, duc_data.len(), Some(&crop_options)); error_handling::error_to_wasm_bytes(&error_info) } } diff --git a/packages/ducpdf/src/duc2pdf/tests/test_integration.rs b/packages/ducpdf/src/duc2pdf/tests/test_integration.rs index 0535c00c..e5e79fdd 100644 --- a/packages/ducpdf/src/duc2pdf/tests/test_integration.rs +++ b/packages/ducpdf/src/duc2pdf/tests/test_integration.rs @@ -108,6 +108,7 @@ mod integration_tests { metadata_title: Some("Basic CROP Test".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Basic CROP Mode Testing".to_string()), + background_color: Some("#FFFFFF".to_string()), }; match convert_duc_to_pdf_with_options(&test_data, options) { @@ -137,6 +138,7 @@ mod integration_tests { metadata_title: Some("Basic PLOT Test".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Basic PLOT Mode Testing".to_string()), + background_color: Some("#FFFFFF".to_string()), }; match convert_duc_to_pdf_with_options(&test_data, options) { @@ -170,6 +172,7 @@ mod integration_tests { metadata_title: Some(format!("Error Test - {}", test_name)), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Error handling validation".to_string()), + background_color: Some("oklch(0.55 0.04 257)".to_string()), }; match convert_duc_to_pdf_with_options(&test_data, options) { @@ -222,6 +225,7 @@ mod integration_tests { metadata_title: Some(format!("Bounds Test - {}", test_name)), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Coordinate bounds testing".to_string()), + background_color: Some("".to_string()), }; match convert_duc_to_pdf_with_options(&test_data, options) { @@ -283,6 +287,7 @@ mod integration_tests { metadata_title: title, metadata_author: author, metadata_subject: subject, + background_color: Some("transparent".to_string()), }; match convert_duc_to_pdf_with_options(&test_data, options) { @@ -379,6 +384,7 @@ mod integration_tests { metadata_title: Some("Auto-scaling Test".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Automatic Scaling Testing".to_string()), + background_color: Some("transparent".to_string()), }; // Test with user-provided scale @@ -388,6 +394,7 @@ mod integration_tests { metadata_title: Some("User Scale Test".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("User-provided Scaling Testing".to_string()), + background_color: Some("transparent".to_string()), }; // Auto-scaling test @@ -434,6 +441,7 @@ mod integration_tests { metadata_title: Some("Auto-scale Real Data".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Auto-scaling with real DUC data".to_string()), + background_color: Some("transparent".to_string()), }; // Test 2: User provides scale but it's still too large @@ -443,6 +451,7 @@ mod integration_tests { metadata_title: Some("Insufficient Scale Test".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Scale still exceeds bounds".to_string()), + background_color: Some("oklch(0.55 0.04 257)".to_string()), }; // Test 3: User provides appropriate scale @@ -452,6 +461,7 @@ mod integration_tests { metadata_title: Some("Good Scale Test".to_string()), metadata_author: Some("DUC2PDF Test Suite".to_string()), metadata_subject: Some("Appropriate user-provided scale".to_string()), + background_color: Some("oklch(0.55 0.04 257)".to_string()), }; // Auto-scale test @@ -532,6 +542,7 @@ mod integration_tests { is_deleted: false, group_ids: vec![], region_ids: vec![], + layer_id: None, frame_id: None, bound_elements: None, @@ -539,6 +550,8 @@ mod integration_tests { link: None, locked: false, custom_data: None, + block_ids: vec![], + instance_id: None, } } diff --git a/packages/ducsvg/README.md b/packages/ducsvg/README.md index 21a0033e..d7d95ec7 100644 --- a/packages/ducsvg/README.md +++ b/packages/ducsvg/README.md @@ -13,9 +13,7 @@ `ducsvg` is a lightweight adapter for rendering `duc` CAD documents to SVG. It’s built on top of `ducjs` and focuses on accurate, standards-compliant SVG output you can embed in browsers or export from Node.js. -> This library leverages the stable renderer from `ducpdf`, first converting the `duc` document to PDF format before transforming it into SVG using `pdf-into-svg`. This approach ensures high fidelity in the SVG output by utilizing the robust rendering capabilities of the PDF format. - -> 💡 Maybe in the future we could refactor `ducpdf` to leverage the growing library [`hayro_svg`](https://docs.rs/hayro-svg/latest/hayro_svg/) to convert PDF to SVG more seamlessly instead of using `pdf-into-svg` (C# library). Reference: [hayro project](https://github.com/LaurenzV/hayro?tab=readme-ov-file) +> This library leverages the stable renderer from `ducpdf`, first converting the `duc` document to PDF format before transforming it into SVG using `hayro` and `hayro_svg`. This approach ensures high fidelity in the SVG output by utilizing the robust rendering capabilities of the PDF format. ## Documentation diff --git a/packages/ducsvg/package.json b/packages/ducsvg/package.json index 063bb6c6..e0b0fc36 100644 --- a/packages/ducsvg/package.json +++ b/packages/ducsvg/package.json @@ -2,6 +2,8 @@ "name": "ducsvg", "version": "0.0.0-development", "description": "Adapter for converting duc CAD documents to SVG. Built on top of ducjs.", + "private": false, + "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -9,26 +11,21 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", - "require": "./dist/index.js" + "default": "./dist/index.js" }, - "./*": { - "types": [ - "./dist/*.d.ts", - "./dist/*/index.d.ts" - ], - "import": [ - "./dist/*.js", - "./dist/*/index.js" - ], - "require": [ - "./dist/*.js", - "./dist/*/index.js" - ] + "./pkg": { + "types": "./pkg/pdf2svg.d.ts", + "import": "./pkg/pdf2svg.js", + "default": "./pkg/pdf2svg.js" } }, "sideEffects": false, "scripts": { - "build": "vite build && pwd && tsc --project ./tsconfig.json", + "build": "bun run clean && bun run build:wasm && bun run copy:wasm-to-dist && bun run build:ts", + "build:ts": "vite build && pwd && tsc -p tsconfig.json", + "build:wasm": "cd src/pdf2svg && wasm-pack build --target web --out-dir ../../pkg --release", + "copy:wasm-to-dist": "mkdir -p dist && cp pkg/pdf2svg_bg.wasm dist/ && cp pkg/pdf2svg.js dist/ && cp pkg/pdf2svg.d.ts dist/ && cp pkg/pdf2svg_bg.wasm.d.ts dist/", + "clean": "rm -rf dist pkg && cd src/pdf2svg && cargo clean", "check-types": "tsc --noEmit", "test": "bun test", "semantic-release": "semantic-release" @@ -37,8 +34,7 @@ "license": "MIT", "dependencies": { "ducjs": "workspace:*", - "ducpdf": "workspace:*", - "pdf-into-svg": "1.0.1" + "ducpdf": "workspace:*" }, "devDependencies": { "@xmldom/xmldom": "^0.8.11", diff --git a/packages/ducsvg/src/ducToSvg.ts b/packages/ducsvg/src/ducToSvg.ts index 6327fb8d..03189d5e 100644 --- a/packages/ducsvg/src/ducToSvg.ts +++ b/packages/ducsvg/src/ducToSvg.ts @@ -1,14 +1,21 @@ import { convertDucToPdf, type ConversionOptions } from "ducpdf"; -import { convertPdfToSvg } from "pdf-into-svg"; +import { convertPdfToSvg, type SvgDocument } from "./pdf2svg"; +export type { SvgDocument }; + +/** + * Convert DUC data to SVG pages. + * Uses ducpdf to convert DUC to PDF, then pdf2svg to convert PDF to SVG. + */ export const ducToSvg = async ( ducData: Uint8Array, options?: ConversionOptions, -) => { - // Convert DUC to PDF first - const pdfData = await convertDucToPdf(ducData, options); +): Promise => { + // Step 1: Convert DUC to PDF using the robust ducpdf converter + const pdfBytes = await convertDucToPdf(ducData, options); + + // Step 2: Convert PDF to SVG pages using hayro-svg + const svgDocument = await convertPdfToSvg(pdfBytes); - // const svgDocument = await convertPdfToSvg(pdfData, svgOptions); - const result = await convertPdfToSvg(pdfData); - return result; + return svgDocument; }; \ No newline at end of file diff --git a/packages/ducsvg/src/pdf2svg/Cargo.toml b/packages/ducsvg/src/pdf2svg/Cargo.toml new file mode 100644 index 00000000..193ad750 --- /dev/null +++ b/packages/ducsvg/src/pdf2svg/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "pdf2svg" +version = "0.0.0-development" +edition = "2021" +authors = ["Jorge Soares "] +description = "A library to convert PDF files to SVG format using hayro." +license = "MIT" +repository = "https://github.com/ducflair/duc/tree/main/packages/ducsvg/src/pdf2svg" +readme = "README.md" +keywords = ["pdf", "svg", "converter", "hayro"] + +[lib] +name = "pdf2svg" +path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + +[dependencies] +hayro = "0.4.0" +hayro-svg = "0.2.0" +wasm-bindgen = "0.2.92" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +web-sys = { version = "0.3", features = ["console"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true +panic = "abort" diff --git a/packages/ducsvg/src/pdf2svg/index.ts b/packages/ducsvg/src/pdf2svg/index.ts new file mode 100644 index 00000000..d37e6cfe --- /dev/null +++ b/packages/ducsvg/src/pdf2svg/index.ts @@ -0,0 +1,64 @@ +let wasmModule: any = null; +let wasmInitPromise: Promise | null = null; + +async function initWasm(): Promise { + if (wasmModule) return wasmModule; + + if (wasmInitPromise) { + return wasmInitPromise; + } + + wasmInitPromise = (async () => { + try { + const wasmBindings: any = await import('../wasm'); + + if (typeof wasmBindings.default === 'function') { + await wasmBindings.default(); + } else { + throw new Error('WASM module does not have a default initialization function'); + } + + if (typeof wasmBindings['convert_pdf_to_svg_rs'] !== 'function') { + throw new Error("Required WASM function 'convert_pdf_to_svg_rs' not found"); + } + + wasmModule = wasmBindings; + return wasmModule; + } catch (error) { + console.error('Failed to initialize pdf2svg WASM module:', error); + wasmInitPromise = null; + throw error; + } + })(); + + return wasmInitPromise; +} + +export interface SvgPage { + svg: string; +} + +export interface SvgDocument { + pages: SvgPage[]; +} + +/** + * Convert PDF bytes to SVG pages using hayro-svg WASM. + */ +export async function convertPdfToSvg(pdfData: Uint8Array): Promise { + const wasm = await initWasm(); + + const resultJson = wasm.convert_pdf_to_svg_rs(pdfData); + const result: SvgDocument = JSON.parse(resultJson); + + return result; +} + +export function isWasmInitialized(): boolean { + return wasmModule !== null; +} + +export function resetWasmModule(): void { + wasmModule = null; + wasmInitPromise = null; +} diff --git a/packages/ducsvg/src/pdf2svg/src/lib.rs b/packages/ducsvg/src/pdf2svg/src/lib.rs new file mode 100644 index 00000000..001818a9 --- /dev/null +++ b/packages/ducsvg/src/pdf2svg/src/lib.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; +use wasm_bindgen::prelude::*; + +/// Result type for conversion operations +#[derive(serde::Serialize)] +pub struct SvgPage { + pub svg: String, +} + +#[derive(serde::Serialize)] +pub struct ConversionResult { + pub pages: Vec, +} + +/// Convert PDF bytes to an array of SVG strings (one per page) +pub fn convert_pdf_to_svg_pages(pdf_bytes: &[u8]) -> Result, String> { + // Wrap bytes in Arc for hayro API + let data: Arc + Send + Sync> = Arc::new(pdf_bytes.to_vec()); + + // Parse PDF + let pdf = hayro::Pdf::new(data).map_err(|e| format!("Failed to parse PDF: {:?}", e))?; + + let settings = hayro::InterpreterSettings::default(); + let mut svg_pages = Vec::new(); + + // Convert each page to SVG + for page in pdf.pages().iter() { + let svg_string = hayro_svg::convert(&page, &settings); + svg_pages.push(svg_string); + } + + Ok(svg_pages) +} + +/// WASM binding: Convert PDF bytes to JSON array of SVG pages +#[wasm_bindgen] +pub fn convert_pdf_to_svg_rs(pdf_data: &[u8]) -> Result { + match convert_pdf_to_svg_pages(pdf_data) { + Ok(svg_strings) => { + let pages: Vec = svg_strings.into_iter().map(|svg| SvgPage { svg }).collect(); + + let result = ConversionResult { pages }; + + serde_json::to_string(&result) + .map_err(|e| JsValue::from_str(&format!("JSON serialization failed: {}", e))) + } + Err(e) => Err(JsValue::from_str(&e)), + } +} diff --git a/packages/ducsvg/src/wasm.ts b/packages/ducsvg/src/wasm.ts new file mode 100644 index 00000000..5bb7dd84 --- /dev/null +++ b/packages/ducsvg/src/wasm.ts @@ -0,0 +1,3 @@ +// Re-export WASM bindings from dist for bundled distribution +export * from '../dist/pdf2svg'; +export { default } from '../dist/pdf2svg'; \ No newline at end of file diff --git a/packages/ducsvg/tsconfig.json b/packages/ducsvg/tsconfig.json index f599d413..07c5ee56 100644 --- a/packages/ducsvg/tsconfig.json +++ b/packages/ducsvg/tsconfig.json @@ -23,6 +23,8 @@ ], "exclude": [ "node_modules", - "dist" + "pkg", + "dist", + "src/pdf2svg/src" ] } \ No newline at end of file diff --git a/packages/ducsvg/vite.config.mts b/packages/ducsvg/vite.config.mts index 353d0203..e1e8201c 100644 --- a/packages/ducsvg/vite.config.mts +++ b/packages/ducsvg/vite.config.mts @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; export default defineConfig({ build: { + emptyOutDir: false, lib: { entry: 'src/index.ts', name: 'DucSvg', @@ -13,13 +14,11 @@ export default defineConfig({ rollupOptions: { // Externalize dependencies that have WASM runtimes or shouldn't be bundled external: [ - 'pdf-into-svg', // Uses .NET Blazor WASM - can't be bundled 'ducpdf', // Uses Rust WASM - let consuming app handle 'ducjs', // Workspace dependency ], output: { globals: { - 'pdf-into-svg': 'PdfIntoSvg', 'ducpdf': 'DucPdf', 'ducjs': 'DucJs', }, diff --git a/sst.config.ts b/sst.config.ts index 485ed2de..401e588e 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -15,7 +15,7 @@ export default $config({ protect: ["production"].includes(input?.stage), home: "aws", aws: { - region: "us-east-2", + region, }, cloudflare: "5.41.0", }; @@ -38,7 +38,7 @@ export default $config({ // vpc domain: domainName && { name: domainName, - dns: sst.cloudflare.dns(), + dns: sst.cloudflare.dns({ proxy: true }), }, path: "apps/web", link: [ @@ -62,7 +62,7 @@ export default $config({ }, domain: domainName && { name: `python.${domainName}`, - dns: sst.cloudflare.dns(), + dns: sst.cloudflare.dns({ proxy: true }), }, }); },