mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
browser-correctness: fix print, startup flags, and document MIME (#117)
This commit is contained in:
1 parent
788e0eac66
commit
4c6aa4cd4b
8 files changed
+211
-36
No files matched your search
Generated
+73
@@ -635,6 +635,12 @@ dependencies = [
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.5.1"
|
||||
@@ -952,6 +958,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"wasm-bindgen-test",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -1126,9 +1133,11 @@ name = "designcraft-web"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"designcraft-engine",
|
||||
"designcraft-format",
|
||||
"designcraft-ui-egui",
|
||||
"eframe",
|
||||
"egui",
|
||||
"form_urlencoded",
|
||||
"js-sys",
|
||||
"log",
|
||||
"rfd",
|
||||
@@ -2550,6 +2559,16 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2673,6 +2692,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -3022,6 +3050,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.4.4"
|
||||
@@ -4746,6 +4780,45 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae7499dfd45780a0a91d7ee6bb9ac51970a4479a41a89da443fdda5a39547d42"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cast",
|
||||
"js-sys",
|
||||
"libm",
|
||||
"minicov",
|
||||
"nu-ansi-term",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
"wasm-bindgen-test-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f692aa943ccd88363733b77063f32cfed5bc6cbea8e6e8b251b302f881606fe"
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.17"
|
||||
|
||||
@@ -15,6 +15,8 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
designcraft-engine = { workspace = true }
|
||||
designcraft-format = { workspace = true }
|
||||
form_urlencoded = "1.2"
|
||||
designcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Pure browser startup and download metadata policies.
|
||||
|
||||
/// Query flags are case-sensitive decoded keys. Values are ignored: `?sample=false`
|
||||
/// still enables sample startup, and repeated keys have the same presence semantics.
|
||||
pub(crate) fn has_query_flag(query: &str, flag: &str) -> bool {
|
||||
let query = query.strip_prefix('?').unwrap_or(query);
|
||||
form_urlencoded::parse(query.as_bytes()).any(|(key, _)| key == flag)
|
||||
}
|
||||
|
||||
pub(crate) fn mime_for(name: &str) -> &'static str {
|
||||
match name.rsplit('.').next().map(str::to_ascii_lowercase).as_deref() {
|
||||
Some("png") => "image/png",
|
||||
Some("jpg" | "jpeg") => "image/jpeg",
|
||||
Some("pdf") => "application/pdf",
|
||||
Some("designcraft") => designcraft_format::MIME,
|
||||
Some("idml") => "application/vnd.adobe.indesign-idml-package",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{has_query_flag, mime_for};
|
||||
|
||||
#[test]
|
||||
fn startup_flags_match_complete_decoded_keys() {
|
||||
for flag in ["sample", "webgl"] {
|
||||
for query in [format!("?{flag}"), format!("?{flag}=false"), format!("?{flag}=1&{flag}=0"), format!("?other=value&{flag}")] {
|
||||
assert!(has_query_flag(&query, flag), "{query}");
|
||||
}
|
||||
for query in [
|
||||
String::new(),
|
||||
format!("?note={flag}"),
|
||||
format!("?{flag}d=1"),
|
||||
format!("?note=not_a_{flag}"),
|
||||
"?note=%73ample%77ebgl".to_string(),
|
||||
format!("?{flag}+extra=1"),
|
||||
format!("?{flag}%26extra=1"),
|
||||
format!("?{flag}%3Dextra=1"),
|
||||
] {
|
||||
assert!(!has_query_flag(&query, flag), "{query}");
|
||||
}
|
||||
}
|
||||
assert!(has_query_flag("?webgl&sample", "sample"));
|
||||
assert!(has_query_flag("?webgl&sample", "webgl"));
|
||||
assert!(has_query_flag("?%73ample", "sample"));
|
||||
assert!(has_query_flag("?%77ebgl", "webgl"));
|
||||
assert!(!has_query_flag("?Sample&WEBGL", "sample"));
|
||||
assert!(!has_query_flag("?Sample&WEBGL", "webgl"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn download_mime_matches_native_package_and_existing_exports() {
|
||||
for (name, expected) in [
|
||||
("Quarterly.designcraft", designcraft_format::MIME),
|
||||
("日本語.multi.DEsignCraft", designcraft_format::MIME),
|
||||
("page.pdf", "application/pdf"),
|
||||
("page.png", "image/png"),
|
||||
("page.jpg", "image/jpeg"),
|
||||
("page.JPEG", "image/jpeg"),
|
||||
("document.idml", "application/vnd.adobe.indesign-idml-package"),
|
||||
("unknown.bin", "application/octet-stream"),
|
||||
] {
|
||||
assert_eq!(mime_for(name), expected, "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,13 @@
|
||||
//! - dropped files are read asynchronously by `web::WebShell` and delivered through the inbox.
|
||||
//!
|
||||
//! URL query flags: `?webgl` forces the WebGL2 backend instead of WebGPU; `?sample` opens the
|
||||
//! sample magazine on start.
|
||||
//! sample magazine on start. Flags match case-sensitive, percent-decoded query keys; values
|
||||
//! are ignored and repeated keys have the same presence semantics (`?sample=false` enables it).
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod browser_policy;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The browser shell: web `Services`, drag-and-drop, and the eframe web runner.
|
||||
|
||||
use super::browser_policy::{has_query_flag, mime_for};
|
||||
use designcraft_engine::Session;
|
||||
use designcraft_ui_egui::{DesignApp, Inbox, Services};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
@@ -41,7 +42,7 @@ pub fn start() {
|
||||
return;
|
||||
};
|
||||
let mut options = eframe::WebOptions::default();
|
||||
if query().contains("webgl")
|
||||
if has_query_flag(&query(), "webgl")
|
||||
&& let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup
|
||||
{
|
||||
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
|
||||
@@ -56,7 +57,7 @@ pub fn start() {
|
||||
}
|
||||
let inbox: Inbox = Inbox::default();
|
||||
let mut app = DesignApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone()));
|
||||
if query().contains("sample") {
|
||||
if has_query_flag(&query(), "sample") {
|
||||
let _ = app.run("file.newSample", serde_json::json!({}));
|
||||
}
|
||||
Ok(Box::new(WebShell { app, inbox }))
|
||||
@@ -173,14 +174,3 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
window.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000).map_err(js)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mime_for(name: &str) -> &'static str {
|
||||
match name.rsplit('.').next().map(str::to_ascii_lowercase).as_deref() {
|
||||
Some("png") => "image/png",
|
||||
Some("jpg" | "jpeg") => "image/jpeg",
|
||||
Some("pdf") => "application/pdf",
|
||||
Some("designcraft") => "application/json",
|
||||
Some("idml") => "application/vnd.adobe.indesign-idml-package",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
@@ -36,3 +36,6 @@ hayro-syntax = "0.7.2"
|
||||
hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts"] }
|
||||
hayro-cmap = "0.1"
|
||||
kurbo = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, str_param};
|
||||
use crate::{EngineError, Result, Session};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use super::str_param;
|
||||
use super::{CommandSpec, bad, cmd, has_doc};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::EngineError;
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -39,31 +43,32 @@ fn printers() -> Value {
|
||||
fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let opts = super::export::options(p, d.page_count())?;
|
||||
let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999);
|
||||
let printer = str_param(p, "printer").map(str::to_string);
|
||||
let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
|
||||
let pdf = designcraft_pdf::export_pdf(d, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let file = std::env::temp_dir().join(format!("designcraft-print-{}-{}.pdf", std::process::id(), d.title.replace(['/', '\\'], "_")));
|
||||
let mut cmd: Vec<String> = vec!["lpr".into()];
|
||||
if let Some(pr) = &printer {
|
||||
cmd.extend(["-P".into(), pr.clone()]);
|
||||
}
|
||||
if copies > 1 {
|
||||
cmd.extend(["-#".into(), copies.to_string()]);
|
||||
}
|
||||
cmd.extend(["-T".into(), d.title.clone(), file.to_string_lossy().to_string()]);
|
||||
let pages = opts.pages.as_ref().map_or(d.page_count(), |v| v.len());
|
||||
let out = json!({"printer": printer, "copies": copies, "pages": pages, "command": cmd, "bytes": pdf.len()});
|
||||
if dry {
|
||||
return Ok(out);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = pdf;
|
||||
// Even a dry run cannot construct a native spool command in the browser.
|
||||
let _ = opts;
|
||||
Err(bad("file.print", "printing isn't available on the web: export a PDF and print it"))
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999);
|
||||
let printer = str_param(p, "printer").map(str::to_string);
|
||||
let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
|
||||
let pdf = designcraft_pdf::export_pdf(d, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let file = std::env::temp_dir().join(format!("designcraft-print-{}-{}.pdf", std::process::id(), d.title.replace(['/', '\\'], "_")));
|
||||
let mut cmd: Vec<String> = vec!["lpr".into()];
|
||||
if let Some(pr) = &printer {
|
||||
cmd.extend(["-P".into(), pr.clone()]);
|
||||
}
|
||||
if copies > 1 {
|
||||
cmd.extend(["-#".into(), copies.to_string()]);
|
||||
}
|
||||
cmd.extend(["-T".into(), d.title.clone(), file.to_string_lossy().to_string()]);
|
||||
let pages = opts.pages.as_ref().map_or(d.page_count(), |v| v.len());
|
||||
let out = json!({"printer": printer, "copies": copies, "pages": pages, "command": cmd, "bytes": pdf.len()});
|
||||
if dry {
|
||||
return Ok(out);
|
||||
}
|
||||
std::fs::write(&file, &pdf).map_err(|e| EngineError::Other(format!("{}: {e}", file.display())))?;
|
||||
let st = std::process::Command::new(&cmd[0]).args(&cmd[1..]).output().map_err(|e| bad("file.print", format!("lpr: {e}")))?;
|
||||
if !st.status.success() {
|
||||
@@ -73,7 +78,7 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Browser Print must reject native spooler operations without trapping the editor.
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use designcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
fn print_is_safe(dry_run: bool) {
|
||||
let mut session = Session::new();
|
||||
session.execute("file.new", &json!({"pages": 1})).unwrap();
|
||||
let error = session.execute("file.print", &json!({"dryRun": dry_run})).unwrap_err().to_string();
|
||||
assert!(error.contains("printing isn't available on the web"), "{error}");
|
||||
assert!(error.contains("export a PDF"), "{error}");
|
||||
session.execute("frame.create", &json!({"rect": [36, 36, 100, 100], "content": "text"})).unwrap();
|
||||
let exported = session.execute("file.exportPdf", &json!({})).unwrap();
|
||||
assert!(exported["bytes"].as_u64().unwrap() > 500);
|
||||
assert_eq!(exported["pages"], 1);
|
||||
assert!(session.execute("file.print", &json!({"pages": "9", "dryRun": dry_run})).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn print_confirmation_preserves_editing_and_pdf_export() {
|
||||
print_is_safe(false);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn print_dry_run_is_also_unsupported_without_native_operations() {
|
||||
print_is_safe(true);
|
||||
}
|
||||
Reference in new issue
Block a user