Mayo commited on
Commit
9e7ae43
·
unverified ·
1 Parent(s): aa02536

fix: make clippy happy

Browse files
bun.lock CHANGED
@@ -39,6 +39,7 @@
39
  "@tanstack/react-query": "^5.99.0",
40
  "@tanstack/react-virtual": "^3.13.24",
41
  "@tauri-apps/api": "^2.10.1",
 
42
  "@tauri-apps/plugin-opener": "^2.4.0",
43
  "@tauri-apps/plugin-process": "^2.3.1",
44
  "@tauri-apps/plugin-updater": "^2.10.1",
@@ -855,6 +856,8 @@
855
 
856
  "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="],
857
 
 
 
858
  "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ=="],
859
 
860
  "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="],
 
39
  "@tanstack/react-query": "^5.99.0",
40
  "@tanstack/react-virtual": "^3.13.24",
41
  "@tauri-apps/api": "^2.10.1",
42
+ "@tauri-apps/plugin-dialog": "^2.7.0",
43
  "@tauri-apps/plugin-opener": "^2.4.0",
44
  "@tauri-apps/plugin-process": "^2.3.1",
45
  "@tauri-apps/plugin-updater": "^2.10.1",
 
856
 
857
  "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="],
858
 
859
+ "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw=="],
860
+
861
  "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ=="],
862
 
863
  "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="],
koharu-app/bin/pipeline.rs CHANGED
@@ -253,10 +253,8 @@ async fn run() -> Result<()> {
253
  Err(e) => eprintln!("=> pipeline failed: {e:#}"),
254
  }
255
 
256
- if ensure_translation_fallback {
257
- if let Err(e) = synthesize_translations(&app, page_id).await {
258
- eprintln!("warn: failed to synthesize translations: {e:#}");
259
- }
260
  }
261
 
262
  dump_artifacts(&session, page_id, &cli.output_dir)
 
253
  Err(e) => eprintln!("=> pipeline failed: {e:#}"),
254
  }
255
 
256
+ if ensure_translation_fallback && let Err(e) = synthesize_translations(&app, page_id).await {
257
+ eprintln!("warn: failed to synthesize translations: {e:#}");
 
 
258
  }
259
 
260
  dump_artifacts(&session, page_id, &cli.output_dir)
koharu-app/src/app.rs CHANGED
@@ -257,7 +257,7 @@ pub fn shared_llama_backend(runtime: &RuntimeManager) -> Result<Arc<LlamaBackend
257
  if let Some(backend) = LLAMA_BACKEND.get() {
258
  return Ok(backend.clone());
259
  }
260
- koharu_llm::sys::initialize(runtime).map_err(anyhow::Error::from)?;
261
  let backend = Arc::new(LlamaBackend::init().map_err(anyhow::Error::from)?);
262
  let _ = LLAMA_BACKEND.set(backend.clone());
263
  Ok(backend)
 
257
  if let Some(backend) = LLAMA_BACKEND.get() {
258
  return Ok(backend.clone());
259
  }
260
+ koharu_llm::sys::initialize(runtime)?;
261
  let backend = Arc::new(LlamaBackend::init().map_err(anyhow::Error::from)?);
262
  let _ = LLAMA_BACKEND.set(backend.clone());
263
  Ok(backend)
koharu-app/src/config.rs CHANGED
@@ -187,10 +187,10 @@ pub fn save(config: &AppConfig) -> Result<()> {
187
  /// Apply a `ConfigPatch` in-place. Missing fields leave the existing value
188
  /// alone. Providers are replaced wholesale (the list, not field-by-field).
189
  pub fn apply_patch(config: &mut AppConfig, patch: koharu_core::ConfigPatch) {
190
- if let Some(data) = patch.data {
191
- if let Some(path) = data.path {
192
- config.data.path = camino::Utf8PathBuf::from(path);
193
- }
194
  }
195
  if let Some(http) = patch.http {
196
  if let Some(v) = http.connect_timeout {
 
187
  /// Apply a `ConfigPatch` in-place. Missing fields leave the existing value
188
  /// alone. Providers are replaced wholesale (the list, not field-by-field).
189
  pub fn apply_patch(config: &mut AppConfig, patch: koharu_core::ConfigPatch) {
190
+ if let Some(data) = patch.data
191
+ && let Some(path) = data.path
192
+ {
193
+ config.data.path = camino::Utf8PathBuf::from(path);
194
  }
195
  if let Some(http) = patch.http {
196
  if let Some(v) = http.connect_timeout {
koharu-app/src/pipeline/engines/support.rs CHANGED
@@ -18,7 +18,7 @@ use crate::blobs::BlobStore;
18
 
19
  /// Find the Source image node on `page`. Returns `(node_id, image_data)`.
20
  /// Every valid page has exactly one; absence means the page is malformed.
21
- pub fn source_node<'a>(scene: &'a Scene, page: PageId) -> Result<(NodeId, &'a ImageData)> {
22
  let page = scene
23
  .page(page)
24
  .with_context(|| format!("page {} not found", page))?;
@@ -58,10 +58,7 @@ pub fn find_mask_node(scene: &Scene, page: PageId, role: MaskRole) -> Option<(No
58
 
59
  /// Collect `(NodeId, &Transform, &TextData)` for every text node on `page`,
60
  /// in stacking order.
61
- pub fn text_nodes<'a>(
62
- scene: &'a Scene,
63
- page: PageId,
64
- ) -> Vec<(NodeId, &'a Transform, &'a TextData)> {
65
  let Some(page) = scene.page(page) else {
66
  return Vec::new();
67
  };
@@ -122,6 +119,7 @@ pub fn core_text_direction_to_ml(d: koharu_core::TextDirection) -> koharu_ml::ty
122
  // ---------------------------------------------------------------------------
123
 
124
  /// Build an `AddNode` for a new `Image { role }` layer.
 
125
  pub fn add_image_node_op(
126
  page: PageId,
127
  role: ImageRole,
@@ -309,13 +307,12 @@ pub fn clear_text_nodes_ops(scene: &Scene, page: PageId) -> Vec<Op> {
309
  .nodes
310
  .iter()
311
  .enumerate()
312
- .filter_map(|(idx, (id, node))| {
313
- matches!(&node.kind, NodeKind::Text(_)).then(|| Op::RemoveNode {
314
- page,
315
- id: *id,
316
- prev_node: node.clone(),
317
- prev_index: idx,
318
- })
319
  })
320
  .collect()
321
  }
 
18
 
19
  /// Find the Source image node on `page`. Returns `(node_id, image_data)`.
20
  /// Every valid page has exactly one; absence means the page is malformed.
21
+ pub fn source_node(scene: &Scene, page: PageId) -> Result<(NodeId, &ImageData)> {
22
  let page = scene
23
  .page(page)
24
  .with_context(|| format!("page {} not found", page))?;
 
58
 
59
  /// Collect `(NodeId, &Transform, &TextData)` for every text node on `page`,
60
  /// in stacking order.
61
+ pub fn text_nodes(scene: &Scene, page: PageId) -> Vec<(NodeId, &Transform, &TextData)> {
 
 
 
62
  let Some(page) = scene.page(page) else {
63
  return Vec::new();
64
  };
 
119
  // ---------------------------------------------------------------------------
120
 
121
  /// Build an `AddNode` for a new `Image { role }` layer.
122
+ #[allow(clippy::too_many_arguments)]
123
  pub fn add_image_node_op(
124
  page: PageId,
125
  role: ImageRole,
 
307
  .nodes
308
  .iter()
309
  .enumerate()
310
+ .filter(|(_, (_, node))| matches!(&node.kind, NodeKind::Text(_)))
311
+ .map(|(idx, (id, node))| Op::RemoveNode {
312
+ page,
313
+ id: *id,
314
+ prev_node: node.clone(),
315
+ prev_index: idx,
 
316
  })
317
  .collect()
318
  }
koharu-app/src/pipeline/mod.rs CHANGED
@@ -89,6 +89,7 @@ pub enum Scope {
89
 
90
  /// Execute `spec` against `session`. Each engine step becomes one `Op::Batch`
91
  /// applied via the session's history (one undo step per step per page).
 
92
  #[tracing::instrument(level = "info", skip_all)]
93
  pub async fn run(
94
  session: Arc<ProjectSession>,
 
89
 
90
  /// Execute `spec` against `session`. Each engine step becomes one `Op::Batch`
91
  /// applied via the session's history (one undo step per step per page).
92
+ #[allow(clippy::too_many_arguments)]
93
  #[tracing::instrument(level = "info", skip_all)]
94
  pub async fn run(
95
  session: Arc<ProjectSession>,
koharu-app/src/projects.rs CHANGED
@@ -167,11 +167,9 @@ fn slugify(input: &str) -> String {
167
  out.push('-');
168
  prev_dash = true;
169
  }
170
- } else if c.is_whitespace() {
171
- if !out.is_empty() && !prev_dash {
172
- out.push('-');
173
- prev_dash = true;
174
- }
175
  }
176
  // Other chars dropped silently.
177
  }
 
167
  out.push('-');
168
  prev_dash = true;
169
  }
170
+ } else if c.is_whitespace() && !out.is_empty() && !prev_dash {
171
+ out.push('-');
172
+ prev_dash = true;
 
 
173
  }
174
  // Other chars dropped silently.
175
  }
koharu-app/src/renderer.rs CHANGED
@@ -117,7 +117,7 @@ impl Renderer {
117
  .first()
118
  .map(|(family, _)| family.clone())
119
  .unwrap_or_else(|| face.post_script_name.clone());
120
- seen.insert(family_name.clone()).then(|| FontFaceInfo {
121
  family_name,
122
  post_script_name: face.post_script_name,
123
  source: FontSource::System,
@@ -146,6 +146,7 @@ impl Renderer {
146
  /// the full page + per-block sprites. Blocks with an empty translation
147
  /// are skipped (they appear as holes in the composite, falling through to
148
  /// the inpainted plane).
 
149
  #[tracing::instrument(level = "info", skip_all, fields(blocks = blocks.len()))]
150
  pub fn render_page(
151
  &self,
@@ -186,8 +187,7 @@ impl Renderer {
186
  imageops::overlay(&mut canvas, &brush.to_rgba8(), 0, 0);
187
  }
188
  for out in &rendered_blocks {
189
- let (x, y) =
190
- placement_origin(&find_input(blocks, out.node_id), &out.expanded_transform);
191
  imageops::overlay(&mut canvas, &out.sprite.to_rgba8(), x as i64, y as i64);
192
  }
193
  Ok(RenderOutput {
@@ -237,11 +237,10 @@ impl Renderer {
237
 
238
  let font = self.select_font(&style)?;
239
  let block_effect = style.effect.unwrap_or(*effect);
240
- let color = style
241
- .effect
242
- .is_some()
243
- .then_some(style.color)
244
- .unwrap_or_else(|| {
245
  if block.style.is_some() {
246
  style.color
247
  } else if let Some(pred) = &block.font_prediction {
@@ -254,7 +253,8 @@ impl Renderer {
254
  } else {
255
  [0, 0, 0, 255]
256
  }
257
- });
 
258
 
259
  let writing_mode = writing_mode_for_block(&layout_source);
260
  // Translations default to centre alignment inside a bubble — each
@@ -296,13 +296,12 @@ impl Renderer {
296
  // Re-run the layout with `max_width = actual_content_width` so
297
  // every line is centred relative to the block's widest line.
298
  let layout = if layout.width > layout_box.width + 0.5 {
299
- let expanded = layout_builder
300
  .clone()
301
  .with_font_size(layout.font_size)
302
  .with_max_width(layout.width)
303
  .with_max_height(layout_box.height)
304
- .run(&normalized)?;
305
- expanded
306
  } else {
307
  layout
308
  };
@@ -588,7 +587,7 @@ fn core_align_to_renderer(a: koharu_core::TextAlign) -> RendererTextAlign {
588
  // Helpers: placement
589
  // ---------------------------------------------------------------------------
590
 
591
- fn find_input<'a>(blocks: &'a [RenderBlockInput], id: NodeId) -> &'a RenderBlockInput {
592
  blocks
593
  .iter()
594
  .find(|b| b.node_id == id)
 
117
  .first()
118
  .map(|(family, _)| family.clone())
119
  .unwrap_or_else(|| face.post_script_name.clone());
120
+ seen.insert(family_name.clone()).then_some(FontFaceInfo {
121
  family_name,
122
  post_script_name: face.post_script_name,
123
  source: FontSource::System,
 
146
  /// the full page + per-block sprites. Blocks with an empty translation
147
  /// are skipped (they appear as holes in the composite, falling through to
148
  /// the inpainted plane).
149
+ #[allow(clippy::too_many_arguments)]
150
  #[tracing::instrument(level = "info", skip_all, fields(blocks = blocks.len()))]
151
  pub fn render_page(
152
  &self,
 
187
  imageops::overlay(&mut canvas, &brush.to_rgba8(), 0, 0);
188
  }
189
  for out in &rendered_blocks {
190
+ let (x, y) = placement_origin(find_input(blocks, out.node_id), &out.expanded_transform);
 
191
  imageops::overlay(&mut canvas, &out.sprite.to_rgba8(), x as i64, y as i64);
192
  }
193
  Ok(RenderOutput {
 
237
 
238
  let font = self.select_font(&style)?;
239
  let block_effect = style.effect.unwrap_or(*effect);
240
+ let color = if style.effect.is_some() {
241
+ style.color
242
+ } else {
243
+ {
 
244
  if block.style.is_some() {
245
  style.color
246
  } else if let Some(pred) = &block.font_prediction {
 
253
  } else {
254
  [0, 0, 0, 255]
255
  }
256
+ }
257
+ };
258
 
259
  let writing_mode = writing_mode_for_block(&layout_source);
260
  // Translations default to centre alignment inside a bubble — each
 
296
  // Re-run the layout with `max_width = actual_content_width` so
297
  // every line is centred relative to the block's widest line.
298
  let layout = if layout.width > layout_box.width + 0.5 {
299
+ layout_builder
300
  .clone()
301
  .with_font_size(layout.font_size)
302
  .with_max_width(layout.width)
303
  .with_max_height(layout_box.height)
304
+ .run(&normalized)?
 
305
  } else {
306
  layout
307
  };
 
587
  // Helpers: placement
588
  // ---------------------------------------------------------------------------
589
 
590
+ fn find_input(blocks: &[RenderBlockInput], id: NodeId) -> &RenderBlockInput {
591
  blocks
592
  .iter()
593
  .find(|b| b.node_id == id)
koharu-core/src/op.rs CHANGED
@@ -10,6 +10,12 @@
10
  //! let undo = op.inverse(); // pure
11
  //! ```
12
 
 
 
 
 
 
 
13
  use chrono::{DateTime, Utc};
14
  use schemars::JsonSchema;
15
  use serde::{Deserialize, Serialize};
 
10
  //! let undo = op.inverse(); // pure
11
  //! ```
12
 
13
+ // `Op` / `NodeDataPatch` are wire-format data types; their variant-size
14
+ // asymmetry is inherent (Text patches carry many optional fields), and
15
+ // boxing would change the serialised representation. Silence clippy here
16
+ // rather than smear `#[allow]` across every producer.
17
+ #![allow(clippy::large_enum_variant)]
18
+
19
  use chrono::{DateTime, Utc};
20
  use schemars::JsonSchema;
21
  use serde::{Deserialize, Serialize};
koharu-core/src/scene.rs CHANGED
@@ -3,6 +3,10 @@
3
  //! Three primitives: `Node`, `Blob` (via `BlobRef`), `Op` (in `op.rs`).
4
  //! Everything visual on a page is a `Node`; scene mutations flow through `Op`s.
5
 
 
 
 
 
6
  use chrono::{DateTime, Utc};
7
  use indexmap::IndexMap;
8
  use schemars::JsonSchema;
 
3
  //! Three primitives: `Node`, `Blob` (via `BlobRef`), `Op` (in `op.rs`).
4
  //! Everything visual on a page is a `Node`; scene mutations flow through `Op`s.
5
 
6
+ // `NodeKind::Text` naturally carries more data than `Image`/`Mask`, and
7
+ // boxing would change the wire format. Same reasoning as in `op.rs`.
8
+ #![allow(clippy::large_enum_variant)]
9
+
10
  use chrono::{DateTime, Utc};
11
  use indexmap::IndexMap;
12
  use schemars::JsonSchema;
koharu-renderer/src/layout.rs CHANGED
@@ -636,10 +636,11 @@ mod tests {
636
  }
637
 
638
  fn assert_approx_eq(actual: f32, expected: f32) {
639
- if actual.is_infinite() && expected.is_infinite() {
640
- if actual.is_sign_positive() == expected.is_sign_positive() {
641
- return;
642
- }
 
643
  }
644
  let eps = 1e-4;
645
  assert!(
 
636
  }
637
 
638
  fn assert_approx_eq(actual: f32, expected: f32) {
639
+ if actual.is_infinite()
640
+ && expected.is_infinite()
641
+ && actual.is_sign_positive() == expected.is_sign_positive()
642
+ {
643
+ return;
644
  }
645
  let eps = 1e-4;
646
  assert!(
koharu-rpc/src/binary.rs CHANGED
@@ -66,7 +66,7 @@ async fn get_scene_bin(State(app): State<AppState>) -> ApiResult<Response> {
66
  let epoch = session.epoch();
67
  let bytes = postcard::to_allocvec(&WireSnapshot {
68
  epoch,
69
- scene: &*scene,
70
  })
71
  .map_err(|e| ApiError::internal(anyhow::Error::new(e)))?;
72
  (epoch, bytes)
 
66
  let epoch = session.epoch();
67
  let bytes = postcard::to_allocvec(&WireSnapshot {
68
  epoch,
69
+ scene: &scene,
70
  })
71
  .map_err(|e| ApiError::internal(anyhow::Error::new(e)))?;
72
  (epoch, bytes)
koharu-rpc/src/events.rs CHANGED
@@ -101,7 +101,7 @@ async fn events(
101
  fn snapshot_frame(app: &AppState) -> Result<Event, axum::Error> {
102
  let snap = snapshot_from(app);
103
  Event::default()
104
- .json_data(&AppEvent::Snapshot(snap))
105
  .map_err(axum::Error::new)
106
  }
107
 
 
101
  fn snapshot_frame(app: &AppState) -> Result<Event, axum::Error> {
102
  let snap = snapshot_from(app);
103
  Event::default()
104
+ .json_data(AppEvent::Snapshot(snap))
105
  .map_err(axum::Error::new)
106
  }
107
 
koharu-rpc/src/psd_export.rs CHANGED
@@ -19,6 +19,17 @@ use koharu_psd::{
19
  write_document,
20
  };
21
 
 
 
 
 
 
 
 
 
 
 
 
22
  /// Encode a single page as PSD bytes.
23
  pub fn psd_bytes_for_page(session: &Arc<ProjectSession>, page_id: PageId) -> Result<Vec<u8>> {
24
  let scene: Scene = session.scene_snapshot();
@@ -26,8 +37,15 @@ pub fn psd_bytes_for_page(session: &Arc<ProjectSession>, page_id: PageId) -> Res
26
  .pages
27
  .get(&page_id)
28
  .ok_or_else(|| anyhow::anyhow!("page {page_id} not found"))?;
29
- let (doc, source, segment, inpainted, rendered, brush, block_images) =
30
- resolve_page_blobs(session, page).with_context(|| format!("page {page_id}"))?;
 
 
 
 
 
 
 
31
  let resolved = ResolvedDocument {
32
  document: &doc,
33
  source: &source,
@@ -43,18 +61,7 @@ pub fn psd_bytes_for_page(session: &Arc<ProjectSession>, page_id: PageId) -> Res
43
  Ok(buf)
44
  }
45
 
46
- fn resolve_page_blobs(
47
- session: &ProjectSession,
48
- page: &koharu_core::Page,
49
- ) -> Result<(
50
- PsdDocument,
51
- DynamicImage,
52
- Option<DynamicImage>,
53
- Option<DynamicImage>,
54
- Option<DynamicImage>,
55
- Option<DynamicImage>,
56
- HashMap<PsdBlobRef, DynamicImage>,
57
- )> {
58
  let mut source: Option<DynamicImage> = None;
59
  let mut segment: Option<DynamicImage> = None;
60
  let mut inpainted: Option<DynamicImage> = None;
@@ -105,7 +112,7 @@ fn resolve_page_blobs(
105
  height: page.height,
106
  text_blocks,
107
  };
108
- Ok((
109
  doc,
110
  source,
111
  segment,
@@ -113,7 +120,7 @@ fn resolve_page_blobs(
113
  rendered,
114
  brush,
115
  block_images,
116
- ))
117
  }
118
 
119
  /// Encode a single page's image for `role` as PNG bytes. Returns `None` if
 
19
  write_document,
20
  };
21
 
22
+ /// Resolved page artifacts ready to hand to `koharu-psd`.
23
+ struct ResolvedPage {
24
+ doc: PsdDocument,
25
+ source: DynamicImage,
26
+ segment: Option<DynamicImage>,
27
+ inpainted: Option<DynamicImage>,
28
+ rendered: Option<DynamicImage>,
29
+ brush: Option<DynamicImage>,
30
+ block_images: HashMap<PsdBlobRef, DynamicImage>,
31
+ }
32
+
33
  /// Encode a single page as PSD bytes.
34
  pub fn psd_bytes_for_page(session: &Arc<ProjectSession>, page_id: PageId) -> Result<Vec<u8>> {
35
  let scene: Scene = session.scene_snapshot();
 
37
  .pages
38
  .get(&page_id)
39
  .ok_or_else(|| anyhow::anyhow!("page {page_id} not found"))?;
40
+ let ResolvedPage {
41
+ doc,
42
+ source,
43
+ segment,
44
+ inpainted,
45
+ rendered,
46
+ brush,
47
+ block_images,
48
+ } = resolve_page_blobs(session, page).with_context(|| format!("page {page_id}"))?;
49
  let resolved = ResolvedDocument {
50
  document: &doc,
51
  source: &source,
 
61
  Ok(buf)
62
  }
63
 
64
+ fn resolve_page_blobs(session: &ProjectSession, page: &koharu_core::Page) -> Result<ResolvedPage> {
 
 
 
 
 
 
 
 
 
 
 
65
  let mut source: Option<DynamicImage> = None;
66
  let mut segment: Option<DynamicImage> = None;
67
  let mut inpainted: Option<DynamicImage> = None;
 
112
  height: page.height,
113
  text_blocks,
114
  };
115
+ Ok(ResolvedPage {
116
  doc,
117
  source,
118
  segment,
 
120
  rendered,
121
  brush,
122
  block_images,
123
+ })
124
  }
125
 
126
  /// Encode a single page's image for `role` as PNG bytes. Returns `None` if
koharu-rpc/src/routes/pages.rs CHANGED
@@ -346,24 +346,22 @@ async fn add_image_layer(
346
  .len()
347
  };
348
 
349
- let mut filename = String::from("layer.png");
350
- let mut bytes: Option<Vec<u8>> = None;
351
- while let Some(field) = multipart
352
  .next_field()
353
  .await
354
  .map_err(|e| ApiError::bad_request(format!("multipart: {e}")))?
355
- {
356
- if let Some(name) = field.file_name() {
357
- filename = name.to_string();
358
- }
359
- let data = field
360
- .bytes()
361
- .await
362
- .map_err(|e| ApiError::bad_request(format!("read file: {e}")))?;
363
- bytes = Some(data.to_vec());
364
- break;
365
- }
366
- let bytes = bytes.ok_or_else(|| ApiError::bad_request("no file uploaded"))?;
367
 
368
  let decoded = image::load_from_memory(&bytes)
369
  .map_err(|e| ApiError::bad_request(format!("decode: {e}")))?;
 
346
  .len()
347
  };
348
 
349
+ // The handler only accepts a single image layer per request, so we
350
+ // pull the first multipart field and ignore the rest.
351
+ let field = multipart
352
  .next_field()
353
  .await
354
  .map_err(|e| ApiError::bad_request(format!("multipart: {e}")))?
355
+ .ok_or_else(|| ApiError::bad_request("no file uploaded"))?;
356
+ let filename = field
357
+ .file_name()
358
+ .map(|s| s.to_string())
359
+ .unwrap_or_else(|| String::from("layer.png"));
360
+ let bytes = field
361
+ .bytes()
362
+ .await
363
+ .map_err(|e| ApiError::bad_request(format!("read file: {e}")))?
364
+ .to_vec();
 
 
365
 
366
  let decoded = image::load_from_memory(&bytes)
367
  .map_err(|e| ApiError::bad_request(format!("decode: {e}")))?;
koharu-rpc/src/routes/projects.rs CHANGED
@@ -2,14 +2,12 @@
2
  //! `{data.path}/projects/` directory; clients never supply filesystem
3
  //! paths. A project's `id` is the `.khrproj/` directory basename.
4
  //!
5
- //! - `GET /projects` — list managed projects
6
- //! - `POST /projects` — create a new project (`{name}`),
7
- //! server allocates the path
8
- //! - `POST /projects/import` extract a `.khr` archive into a
9
- //! fresh allocated directory + open
10
- //! - `PUT /projects/current` open a managed project by `id`
11
- //! - `DELETE /projects/current` — close current session
12
- //! - `POST /projects/current/export` — export current; returns bytes
13
 
14
  use axum::Json;
15
  use axum::body::{Body, Bytes};
@@ -311,7 +309,7 @@ async fn export_image_role(
311
  "no pages have the requested layer populated",
312
  ));
313
  }
314
- Ok(files_to_response(files, project_name, role_ext(role))?)
315
  }
316
 
317
  fn resolve_page_ids(
 
2
  //! `{data.path}/projects/` directory; clients never supply filesystem
3
  //! paths. A project's `id` is the `.khrproj/` directory basename.
4
  //!
5
+ //! - `GET /projects` — list managed projects
6
+ //! - `POST /projects` — create a new project (`{name}`), server allocates path
7
+ //! - `POST /projects/import` — extract a `.khr` archive into a fresh dir + open
8
+ //! - `PUT /projects/current` open a managed project by `id`
9
+ //! - `DELETE /projects/current` — close current session
10
+ //! - `POST /projects/current/export` export current; returns bytes
 
 
11
 
12
  use axum::Json;
13
  use axum::body::{Body, Bytes};
 
309
  "no pages have the requested layer populated",
310
  ));
311
  }
312
+ files_to_response(files, project_name, role_ext(role))
313
  }
314
 
315
  fn resolve_page_ids(
koharu-rpc/tests/snapshots/openapi__openapi_paths_snapshot.snap ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ source: koharu-rpc/tests/openapi.rs
3
+ assertion_line: 34
4
+ expression: paths
5
+ ---
6
+ [
7
+ (
8
+ "/blobs/{hash}",
9
+ [
10
+ "get",
11
+ ],
12
+ ),
13
+ (
14
+ "/config",
15
+ [
16
+ "get",
17
+ "patch",
18
+ ],
19
+ ),
20
+ (
21
+ "/config/providers/{id}/secret",
22
+ [
23
+ "delete",
24
+ "put",
25
+ ],
26
+ ),
27
+ (
28
+ "/downloads",
29
+ [
30
+ "get",
31
+ "post",
32
+ ],
33
+ ),
34
+ (
35
+ "/engines",
36
+ [
37
+ "get",
38
+ ],
39
+ ),
40
+ (
41
+ "/events",
42
+ [
43
+ "get",
44
+ ],
45
+ ),
46
+ (
47
+ "/fonts",
48
+ [
49
+ "get",
50
+ ],
51
+ ),
52
+ (
53
+ "/google-fonts",
54
+ [
55
+ "get",
56
+ ],
57
+ ),
58
+ (
59
+ "/google-fonts/{family}/fetch",
60
+ [
61
+ "post",
62
+ ],
63
+ ),
64
+ (
65
+ "/google-fonts/{family}/{file}",
66
+ [
67
+ "get",
68
+ ],
69
+ ),
70
+ (
71
+ "/history/apply",
72
+ [
73
+ "post",
74
+ ],
75
+ ),
76
+ (
77
+ "/history/redo",
78
+ [
79
+ "post",
80
+ ],
81
+ ),
82
+ (
83
+ "/history/undo",
84
+ [
85
+ "post",
86
+ ],
87
+ ),
88
+ (
89
+ "/llm/catalog",
90
+ [
91
+ "get",
92
+ ],
93
+ ),
94
+ (
95
+ "/llm/current",
96
+ [
97
+ "delete",
98
+ "get",
99
+ "put",
100
+ ],
101
+ ),
102
+ (
103
+ "/meta",
104
+ [
105
+ "get",
106
+ ],
107
+ ),
108
+ (
109
+ "/operations",
110
+ [
111
+ "get",
112
+ ],
113
+ ),
114
+ (
115
+ "/operations/{id}",
116
+ [
117
+ "delete",
118
+ ],
119
+ ),
120
+ (
121
+ "/pages",
122
+ [
123
+ "post",
124
+ ],
125
+ ),
126
+ (
127
+ "/pages/from-paths",
128
+ [
129
+ "post",
130
+ ],
131
+ ),
132
+ (
133
+ "/pages/{id}/image-layers",
134
+ [
135
+ "post",
136
+ ],
137
+ ),
138
+ (
139
+ "/pages/{id}/masks/{role}",
140
+ [
141
+ "put",
142
+ ],
143
+ ),
144
+ (
145
+ "/pages/{id}/thumbnail",
146
+ [
147
+ "get",
148
+ ],
149
+ ),
150
+ (
151
+ "/pipelines",
152
+ [
153
+ "post",
154
+ ],
155
+ ),
156
+ (
157
+ "/projects",
158
+ [
159
+ "get",
160
+ "post",
161
+ ],
162
+ ),
163
+ (
164
+ "/projects/current",
165
+ [
166
+ "delete",
167
+ "put",
168
+ ],
169
+ ),
170
+ (
171
+ "/projects/current/export",
172
+ [
173
+ "post",
174
+ ],
175
+ ),
176
+ (
177
+ "/projects/import",
178
+ [
179
+ "post",
180
+ ],
181
+ ),
182
+ (
183
+ "/scene.bin",
184
+ [
185
+ "get",
186
+ ],
187
+ ),
188
+ (
189
+ "/scene.json",
190
+ [
191
+ "get",
192
+ ],
193
+ ),
194
+ ]
tests/integration-tests/client/src/lib.rs CHANGED
@@ -1,5 +1,8 @@
 
 
1
  #![allow(unused_imports)]
2
- #![allow(clippy::too_many_arguments)]
 
3
 
4
  extern crate reqwest;
5
  extern crate serde;
 
1
+ // Auto-generated by openapi-generator — silence clippy across the whole
2
+ // crate so CI stays green without hand-patching generator output.
3
  #![allow(unused_imports)]
4
+ #![allow(clippy::all)]
5
+ #![allow(clippy::pedantic)]
6
 
7
  extern crate reqwest;
8
  extern crate serde;
ui/package.json CHANGED
@@ -38,6 +38,7 @@
38
  "@tanstack/react-query": "^5.99.0",
39
  "@tanstack/react-virtual": "^3.13.24",
40
  "@tauri-apps/api": "^2.10.1",
 
41
  "@tauri-apps/plugin-opener": "^2.4.0",
42
  "@tauri-apps/plugin-process": "^2.3.1",
43
  "@tauri-apps/plugin-updater": "^2.10.1",
 
38
  "@tanstack/react-query": "^5.99.0",
39
  "@tanstack/react-virtual": "^3.13.24",
40
  "@tauri-apps/api": "^2.10.1",
41
+ "@tauri-apps/plugin-dialog": "^2.7.0",
42
  "@tauri-apps/plugin-opener": "^2.4.0",
43
  "@tauri-apps/plugin-process": "^2.3.1",
44
  "@tauri-apps/plugin-updater": "^2.10.1",