Liking cljdoc? Tell your friends :D

Photocraft native seams — reference 47f9306

Evidence paths below are relative to /home/klein/PP/hive/clones-ref/photocraft; all source findings are from that checkout. INFERRED labels proposals/implications rather than an existing API. This is a read-only study, not an implementation.

Entry points and lifetime

Host operationActual in-process entryState / qualification
Constructphotocraft_automation::Headless::new() denies filesystem access; with_workspace(AuthorizedWorkspace) accepts read/write capabilities; trusted_local() grants ambient local paths.Holds Session, incremental .pcraft writers indexed by document id, and filesystem policy; keep the same Headless across requests. crates/automation/src/headless.rs:13-45; crates/automation/src/workspace.rs:20-41
New/openHeadless::command_run("file.new", json params) or Headless::open(&Path); for no ambient paths files::open_bytes(name, &[u8]) plus session.add_document.open makes imported document active and returns index/name/size/layer count/warnings. crates/automation/src/rpc.rs:88-99; crates/automation/src/headless.rs:60-83; crates/automation/src/files.rs:16-39
CommandHeadless::command_run(id, serde_json::Value) -> command_start(id, params, true) -> Session::execute(id, params) -> Result<Value, EngineError>.wait=false uses Session::start and returns {job,pending:true} for job-capable commands; call sync_jobs/jobs.list before reading results. crates/automation/src/headless.rs:197-227; crates/engine/src/lib.rs:368-380; crates/automation/src/rpc.rs:70-78
Save/export/renderHeadless::save(index?, path?, format?, &ExportOptions) chooses .pcraft or extension export; render_png(index?, max_side) returns PNG Vec<u8>, files::save_bytes returns (Vec<u8>, warnings) without ambient path.No-path save requires an existing layered file in its own format; new/flat/converted output needs path. crates/automation/src/headless.rs:86-141,159-169; crates/automation/src/files.rs:41-91
Inspect/select/closeinspect, session_list, select, close; close evicts that document's incremental writer.Session holds vector of DocState and active index; closing cancels jobs against the document. crates/automation/src/headless.rs:171-195; crates/engine/src/lib.rs:265-277,305-359

photocraft-cli run already proves the no-GUI composition: construct trusted-local Headless, open/new, repeat command_run, save (apps/photocraft-cli/src/lib.rs:239-266). photocraft-cli serve wraps Headless::with_workspace in Arc<Mutex<_>> for JSON-lines over stdio or authenticated loopback TCP (apps/photocraft-cli/src/lib.rs:379-409; crates/automation/src/rpc.rs:217-300). These CLI paths are evidence of callable Rust functions, not a proposal to shell out.

Command dispatch, JSON, errors

  • The registry is a lazily built static Vec<CommandSpec>; each spec holds stable string id, label, menu, shortcut, human-readable params string, enablement fn(&Session)->Result<(),String>, run fn(&mut Session,&Value)->Result<Value> and journal bit. find linearly finds an exact id. It is function pointers, neither a trait-object registry nor a Rust enum of all commands; the descriptor is not a machine-validated parameter schema. crates/engine/src/commands.rs:11-29,102-123.
  • Session::dispatch resolves id, requires JSON object or null, checks enabled/job conflict, injects channel/kind target, runs the pointer under catch_unwind, then records bookkeeping or a pending job. Session::execute runs inline, start allows background work. The host's nearest real execute(command-id,params-json)->result-json is Headless::command_run(&str, Value)->Result<Value, AutomationError>, not a pre-existing C ABI or a function taking JSON strings. crates/engine/src/jobs.rs:393-397,586-640; crates/engine/src/lib.rs:368-380; crates/automation/src/headless.rs:197-227.
  • Reusable control-shaped entry already exists: Headless::handle(method: &str, params: Value) routes engine.execute, engine.commands, doc.new/open/save/inspect/render/select/close, session.list, jobs.*, batch, methods. This is the exact headless JSON-lines dispatch (rpc::respond locks the Headless and invokes .handle(method,params)). doc.render returns base64 PNG or writes through policy. crates/automation/src/rpc.rs:32-55,65-149,184-210. INFERRED: a cdylib shim can deserialize one JSON request, invoke this method on an owned handle, serialize the returned Value, and preserve one vocabulary for engine and document lifecycle. An engine-only execute ABI cannot open/save by itself (crates/automation/src/rpc.rs:70-126).
  • Do not confuse desktop control with headless RPC. Desktop apps/photocraft/src/control_server.rs::serve receives authenticated lines, creates ControlRequest, sends it to the egui UI thread and waits up to 60 s (apps/photocraft/src/control_server.rs:13-20,48-102). Its exact handler is photocraft_ui_egui::control::handle(&mut PhotocraftApp,&egui::Context,&ControlRequest): engine ids call app.run, otherwise UI/menu invocation; engine.commands calls app.run("command.list",{}) (crates/ui-egui/src/control.rs:143-201). This handler needs UI state, not suitable for a headless cdylib.
  • TCP on both app and headless server requires auth as the first request; authentication_reply checks a 64-hex bearer token, rejects/terminates failed clients. Headless stdio serve_lines does not authenticate (different transport). Limits: 1 MiB request line, 16 TCP connections, 256 batch steps; token-file creation mode 0600 Unix. crates/automation/src/security.rs:12-23,76-111,131-145; crates/automation/src/rpc.rs:217-300; apps/photocraft/src/control_server.rs:60-74. Headless::new denies read/write and with_workspace permits only scoped relative paths, rejecting traversal (crates/automation/src/headless.rs:25-45,65-75,115-124; crates/automation/src/workspace.rs:20-33,111-142). INFERRED: don't bypass this policy by replacing it with trusted_local for untrusted callers.
  • EngineError variants include UnknownCommand, Disabled, BadParams, NoDocument, NoLayer, Other, Cancelled; AutomationError adds BadRequest, Io, Engine, Import, Format, Bridge, App, Other; IoError distinguishes PSD, codec, unknown, unsupported, pcraft, raw and cancelled. crates/engine/src/lib.rs:93-112; crates/automation/src/lib.rs:30-49; crates/io/src/lib.rs:65-88. Engine dispatch catches panics around command run, document edits commit after successful closure; outer rpc::respond catches additional panics and maps to an error, recovering a poisoned mutex (crates/engine/src/jobs.rs:606-628; crates/engine/src/lib.rs:397-420; crates/automation/src/rpc.rs:184-210). INFERRED: any new FFI exported function still needs its own panic guard and error envelope; do not unwind across the C boundary. A batch reply explicitly warns when the budget is exceeded that the current step may have completed (crates/automation/src/rpc.rs:153-181).

Ownership, pixels, headless viability

  • Session owns document states and global tools/clipboard/prefs/jobs, including mutable selection and history. DocState holds Arc<Document>, history, selected/active layer ids, revision and path. Session::edit clones the document and publishes it only after the edit returns; background jobs operate on snapshots, worker thread results are applied by poll_jobs on the caller thread; close cancels related jobs (crates/engine/src/lib.rs:128-154,265-301,305-359,397-420; crates/engine/src/jobs.rs:1-17,476-495). INFERRED: retain one opaque session handle, serialize its mutable calls (as existing Mutex servers do), and poll/wait jobs; no proven Send/Sync guarantee for an unwrapped Session. PhotocraftMcp uses Arc<Mutex<Headless>>, spawn_blocking within tokio for operations (crates/automation/src/server.rs:21-27,251-303); the synchronous Headless itself has no tokio requirement (crates/automation/src/headless.rs:1-2,19-45).
  • The document model is pure data; Document has id/name/size/mode/depth/layers/channels/selection/paths/patterns/comps; LayerContent is Raster/Group/Adjustment/Fill/Text/Shape/Smart. Surface tiles are sparse 256x256 Arc-shared byte buffers, changed tiles copy-on-write (crates/doc/src/lib.rs:1-7,407-427,627-674; crates/raster/src/lib.rs:1-6,18-49,85-92). Those document/layer/surface values are not JSON-derived wholesale; LayerId/DocId, Guides, Fill, SmartFilter, PixelFormat have serde derives, while Document, Layer, LayerContent, Surface, LayerMask do not (crates/doc/src/lib.rs:46-51,64-88,92-99,175-176,379-386,407-435,558-562,627-631; crates/color/src/lib.rs:29-86; crates/raster/src/lib.rs:40-49). INFERRED: use opaque document/session identifiers and existing inspect JSON for structure; pass PNG/base64 or bounded bytes rather than marshaling live tile pointers.
  • Byte paths: files::open_bytes(name,&[u8]) imports .pcraft or delegates to io::import; io::import recognizes native extension/signature, 8BPS PSD/PSB, raw then flat; io::export and files::save_bytes return owned bytes and warnings. render_png runs CPU photocraft_compose::thumbnail + codec PNG, while RPC base64-encodes it (crates/automation/src/files.rs:19-29,78-102; crates/io/src/lib.rs:111-164; crates/automation/src/rpc.rs:105-125). GPU crate is a separate wgpu compositor with wgpu::Texture/view chunk outputs; it can fall back to CPU for unsupported content (crates/gpu/src/lib.rs:1-32,48-53,91-97). INFERRED: avoid linking photocraft-gpu for the headless ABI; it would force device/texture lifetime and GPU setup absent from this CPU path.
  • Headless check by reading, not by GUI launch: crate explicitly declares no UI-toolkit dependency, synchronous UI-free Headless, and CLI run uses it (crates/automation/src/lib.rs:1-16; crates/automation/src/headless.rs:1-2; apps/photocraft-cli/src/lib.rs:239-266). Rendering uses CPU thumbnail (crates/automation/src/files.rs:94-102). Job-capable commands spawn native worker threads; no window/event loop is required for that path (crates/engine/src/jobs.rs:1-17). Not verified live: no GUI or test binary launched for this study; the prewarmed target contains library build artifacts but no discovered executable photocraft_automation-* test binary (local artifact inspection only).

Build and domain vocabulary

  • Workspace edition 2024 / Rust >=1.95 / version 0.3.0 / MIT OR Apache-2.0. Existing photocraft-automation package depends on engine, doc, compose, codecs, io, format, serde JSON, rmcp 3.5, schemars, tokio (MCP/bridge); photocraft-engine depends on rayon and plugins, not gpu. photocraft-gpu alone lists wgpu 30; photocraft-psd has optional serde and testgen features. Crate manifests specify package names, with no crate-type = ["cdylib"] declared; INFERRED: a separate cdylib crate/shim is required, not a feature toggle on the existing library. Cargo.toml:1-47; crates/automation/Cargo.toml:1-29; crates/engine/Cargo.toml:1-41; crates/gpu/Cargo.toml:1-26; crates/psd/Cargo.toml:1-24.
  • Domain nouns for value-object definitions: Document / DocId (crates/doc/src/lib.rs:64-70,627-674); DocState / Session / history / active layer (crates/engine/src/lib.rs:128-154,265-301); Layer / LayerId / Group / LayerContent / LayerMask (crates/doc/src/lib.rs:46-51,92-100,157-162,407-435); Artboard / LayerComp (crates/doc/src/comps.rs:35-46,110-120); path / subpath / knot (crates/doc/src/vector.rs:1-13,23-30,78-99); surface / tile / pixel format (crates/raster/src/lib.rs:1-6,18-49; crates/color/src/lib.rs:71-90); job / JobId / JobInfo (crates/engine/src/jobs.rs:27-30,129-152); PSD/PSB / PsdFile (crates/psd/src/lib.rs:1-14,68-79); import/export warnings (crates/io/src/lib.rs:90-106).
  • Open vocabularies: command ids are strings from specs (not a closed enum), extensible operationally via plugin.run {id} and installed wasm plug-in manifest params; saved SmartFilter records a command string plus Value params (crates/engine/src/commands.rs:13-29,113-123; crates/engine/src/plugin_cmds.rs:1-7,25-53,60-103; crates/doc/src/lib.rs:379-386). INFERRED: dynamic command id/catalog and plug-in id fit a registry/multimethod, not a hard-coded enum. Closed vocabularies: LayerContent variants, ColorMode, SampleType, PathOp, FillRule, LabelColor are enums (crates/doc/src/lib.rs:82-93,407-416; crates/color/src/lib.rs:30-61; crates/doc/src/vector.rs:60-89). INFERRED: malli closed alternatives for these; UI ui.set fields are a separately closed whitelist of 18, only in the GUI channel (crates/ui-egui/src/control.rs:91-114,204-209).

Ranked cut points (proposed; no code changed)

  1. INFERRED preferred: own Headless opaque handle, call Headless::handle(method, Value). Pros: reuse existing method dispatcher, session/document lifecycle, policy, CPU preview, jobs and JSON value shape. Cons: must bind lifetime, workspace capability, UTF-8/string JSON conversion, panic guard and bounds in the new cdylib; unlike wire transport, no TCP auth inside ABI. Protocol shape {method,params} plus session handle (or one process singleton); engine.execute carries {command,params,wait}. crates/automation/src/headless.rs:19-45; crates/automation/src/rpc.rs:32-55,65-149,184-210; crates/automation/src/workspace.rs:20-41.
  2. INFERRED narrower: own Session plus files::open_bytes/save_bytes / io::import/export. Pros: synchronous engine execute(&str,Value)->Result<Value> and owned bytes without rmcp/tokio at seam. Cons: implement document add/index/save, CPU render, writer and filesystem policy yourself; mismatches the reference's Headless lifecycle and exports. Protocol needs explicit document handle or active index and separate open/save bytes calls. crates/engine/src/lib.rs:305-337,368-380; crates/automation/src/files.rs:19-39,78-102; crates/automation/src/headless.rs:19-45.
  3. INFERRED fallback: invoke rpc::respond/serve_lines in-process on Mutex<Headless>. Pros: already serializes JSON envelope, catches panics and budgets responses; one reply JSON per request. Cons: incurs JSON frame parse/serialize and mutex, respond takes a string and serve_lines runs a stream loop; stdio has no auth while TCP requires it. Not a sidecar/subprocess recommendation. crates/automation/src/rpc.rs:184-260,264-300; crates/automation/src/security.rs:12-23,98-111.

Questions for wave 2: Should the cdylib expose full Headless::handle methods or only engine.execute with separate lifecycle methods? Which trust level (Denied, capability-scoped roots, or explicitly trusted local) can the caller grant? Should large image ingress/egress be bounded raw byte buffers or path-based capabilities? These are decisions, not facts (crates/automation/src/headless.rs:13-45,60-83,86-141; crates/automation/src/rpc.rs:65-126).

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close