Skip to content
Merged

Dev #224

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ jobs:
check_release "duc2pdf" "./packages/ducpdf/src/duc2pdf"

# ──────────────────────────────────────────────────────────────────────
# Phase 2: Trigger release workflows for packages with pending releases
# Each workflow handles its own build, test, and publish steps.
# Packages without pending releases are skipped entirely.
# Phase 2: Release in dependency order (mirrors turbo.json build graph)
# ──────────────────────────────────────────────────────────────────────
release-ducrs:
needs: release-gate
Expand All @@ -87,32 +85,32 @@ jobs:
secrets: inherit

release-ducjs:
needs: release-gate
if: needs.release-gate.outputs.ducjs == 'true'
needs: [release-gate, release-ducrs]
if: always() && needs.release-gate.outputs.ducjs == 'true' && needs.release-gate.result == 'success' && (needs.release-ducrs.result == 'success' || needs.release-ducrs.result == 'skipped')
uses: ./.github/workflows/release-ducjs.yml
secrets: inherit

release-duc2pdf:
needs: release-gate
if: needs.release-gate.outputs.duc2pdf == 'true'
uses: ./.github/workflows/release-duc2pdf.yml
secrets: inherit

release-ducpy:
needs: release-gate
if: needs.release-gate.outputs.ducpy == 'true'
needs: [release-gate, release-ducrs]
if: always() && needs.release-gate.outputs.ducpy == 'true' && needs.release-gate.result == 'success' && (needs.release-ducrs.result == 'success' || needs.release-ducrs.result == 'skipped')
uses: ./.github/workflows/release-ducpy.yml
secrets: inherit

release-duc2pdf:
needs: [release-gate, release-ducrs]
if: always() && needs.release-gate.outputs.duc2pdf == 'true' && needs.release-gate.result == 'success' && (needs.release-ducrs.result == 'success' || needs.release-ducrs.result == 'skipped')
uses: ./.github/workflows/release-duc2pdf.yml
secrets: inherit

release-ducpdf:
needs: release-gate
if: needs.release-gate.outputs.ducpdf == 'true'
needs: [release-gate, release-ducjs, release-duc2pdf]
if: always() && needs.release-gate.outputs.ducpdf == 'true' && needs.release-gate.result == 'success' && (needs.release-ducjs.result == 'success' || needs.release-ducjs.result == 'skipped') && (needs.release-duc2pdf.result == 'success' || needs.release-duc2pdf.result == 'skipped')
uses: ./.github/workflows/release-ducpdf.yml
secrets: inherit

release-ducsvg:
needs: release-gate
if: needs.release-gate.outputs.ducsvg == 'true'
needs: [release-gate, release-ducpdf]
if: always() && needs.release-gate.outputs.ducsvg == 'true' && needs.release-gate.result == 'success' && (needs.release-ducpdf.result == 'success' || needs.release-ducpdf.result == 'skipped')
uses: ./.github/workflows/release-ducsvg.yml
secrets: inherit

Expand Down
14 changes: 8 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ members = [
"packages/ducsvg/src/pdf2svg",
]


[profile.release]
lto = true
opt-level = "s"
Binary file modified assets/testing/duc-files/universal.duc
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/ducpdf/src/duc2pdf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
# Local dev: uses path. Publish: release script updates to crates.io version
duc = { version = "0.0.0-development", path = "../../../ducrs" }
hipdf = { version = "1.3.5", features = ["wasm_js"] }
hipdf = { version = "1.3.7", features = ["wasm_js"] }
svg2pdf = "0.13.0"
wasm-bindgen = "0.2.92"
js-sys = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion packages/ducpdf/src/duc2pdf/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ impl DucToPdfBuilder {
// SVG files are stored as embedded_pdfs since they're converted to PDF
// (already stored in process_svg_file, no need to duplicate here)
}
"image/png" | "image/jpeg" | "image/jpg" | "image/gif" => {
"image/png" | "image/jpeg" | "image/jpg" | "image/gif" | "image/webp" => {
// Handle image files using hipdf::images
let _object_id = self.process_image_file(&file)?;
// Note: image ID and element streamer registration happens in process_image_file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub enum ResourceType {
Svg,
Png,
Jpeg,
WebP,
Pdf,
Unsupported,
}
Expand Down Expand Up @@ -122,7 +123,7 @@ impl ResourceStreamer {

match resource_type {
ResourceType::Svg => self.process_svg_file(file),
ResourceType::Png | ResourceType::Jpeg => {
ResourceType::Png | ResourceType::Jpeg | ResourceType::WebP => {
self.process_image_file(file, &resource_type)
}
ResourceType::Pdf => self.process_pdf_file(file),
Comment on lines 123 to 129
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image/webp is now detected as ResourceType::WebP and routed through process_image_file(), but create_image_xobject() currently only supports Png and Jpeg and returns an error for other variants. This means WebP resources will fail to process at runtime if they go through ResourceStreamer. Either add WebP support in the image embedding path, or don’t classify image/webp as a supported image type here until it’s implemented end-to-end.

Copilot uses AI. Check for mistakes.
Expand All @@ -139,6 +140,7 @@ impl ResourceStreamer {
"image/svg+xml" => ResourceType::Svg,
"image/png" => ResourceType::Png,
"image/jpeg" | "image/jpg" => ResourceType::Jpeg,
"image/webp" => ResourceType::WebP,
"application/pdf" => ResourceType::Pdf,
_ => ResourceType::Unsupported,
}
Expand Down
42 changes: 40 additions & 2 deletions packages/ducpdf/src/duc2pdf/tests/test_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,27 @@ mod integration_tests {
use hipdf::fonts::{Font, StandardFont};

fn get_assets_dir() -> String {
// Use environment variable if set, otherwise use relative path
if let Ok(path) = std::env::var("DUC_ASSETS_DIR") {
path
} else {
// Rust tests run from the crate directory, so we need to adjust the path
let current_dir = std::env::current_dir().unwrap();
let assets_path = current_dir.join("../../../../assets/testing/duc-files");
assets_path.to_string_lossy().to_string()
}
}

fn list_duc_files() -> Vec<String> {
let dir = get_assets_dir();
let mut files: Vec<String> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read assets dir {dir}: {e}"))
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.filter(|name| name.ends_with(".duc"))
.collect();
files.sort();
files
}

/// Load a DUC file from the assets directory
fn load_duc_file(filename: &str) -> Vec<u8> {
let assets_dir = get_assets_dir();
Expand Down Expand Up @@ -512,6 +523,33 @@ mod integration_tests {

println!("🔧 Real data scaling tests completed");
}

#[test]
fn test_plot_all_assets() {
for file in list_duc_files() {
let duc_data = load_duc_file(&file);

let options = ConversionOptions {
scale: None,
mode: ConversionMode::Plot,
metadata_title: Some(file.clone()),
metadata_author: Some("DUC2PDF Test Suite".to_string()),
metadata_subject: Some("Asset plot conversion".to_string()),
background_color: Some("transparent".to_string()),
};

match convert_duc_to_pdf_with_options(&duc_data, options) {
Ok(pdf_bytes) => {
validate_pdf_structure(&pdf_bytes, Path::new(&format!("memory:{file}")));
println!("✅ {file}: plot OK, {} bytes", pdf_bytes.len());
}
Err(e) => {
println!("⚠️ {file}: plot failed: {e}");
}
}
}
}
Comment on lines +527 to +551
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_plot_all_assets logs conversion errors but doesn’t fail the test, and will also trivially pass if the assets directory contains no .duc files. This makes the test ineffective in CI (regressions can be masked). Suggest either asserting each conversion succeeds, or at least failing when any conversion fails / when no assets are found (or marking the test #[ignore] if it’s meant to be a best-effort local smoke test).

Copilot uses AI. Check for mistakes.

use duc::types::{
ELEMENT_CONTENT_PREFERENCE, STROKE_CAP, STROKE_JOIN, STROKE_PREFERENCE,
};
Expand Down
6 changes: 5 additions & 1 deletion packages/ducpdf/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';

export function getAssetsDir(): string {
Expand All @@ -14,6 +14,10 @@ export function ensureDir(path: string) {
}
}

export function listDucFiles(): string[] {
return readdirSync(getAssetsDir()).filter((f) => f.endsWith('.duc')).sort();
}

export function loadDucFile(filename: string): Uint8Array {
const p = join(getAssetsDir(), filename);
const buf = readFileSync(p);
Expand Down
7 changes: 2 additions & 5 deletions packages/ducpdf/tests/plots.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'bun:test';
import { join } from 'node:path';
import { convertDucToPdf } from '../src/duc2pdf/index';
import { ensureDir, loadDucFile, savePdfOutput, validatePdf } from './helpers';
import { ensureDir, listDucFiles, loadDucFile, savePdfOutput, validatePdf } from './helpers';

const OUTPUT_DIR = 'tests_output/plots';

Expand All @@ -12,10 +12,7 @@ function optionsPlot() {
}

describe('PLOTS mode conversions', () => {
const assets = [
'blocks_instances.duc',
'universal.duc',
];
const assets = listDucFiles();

for (const file of assets) {
it(`converts ${file} (PLOT)`, async () => {
Expand Down
Loading
Loading