Mayo commited on
Commit
3c20efb
·
unverified ·
1 Parent(s): 3cd559b

fix: render on textless page

Browse files
Cargo.lock CHANGED
@@ -4468,6 +4468,7 @@ dependencies = [
4468
  "tempfile",
4469
  "tokio",
4470
  "uuid",
 
4471
  ]
4472
 
4473
  [[package]]
 
4468
  "tempfile",
4469
  "tokio",
4470
  "uuid",
4471
+ "zip 8.6.0",
4472
  ]
4473
 
4474
  [[package]]
koharu-app/src/pipeline/engines/renderer.rs CHANGED
@@ -69,10 +69,6 @@ impl Engine for Model {
69
  })
70
  .collect();
71
 
72
- if inputs.is_empty() {
73
- return Ok(Vec::new());
74
- }
75
-
76
  let page_opts = PageRenderOptions {
77
  shader_effect: Default::default(),
78
  shader_stroke: None,
 
69
  })
70
  .collect();
71
 
 
 
 
 
72
  let page_opts = PageRenderOptions {
73
  shader_effect: Default::default(),
74
  shader_stroke: None,
tests/integration-tests/Cargo.toml CHANGED
@@ -21,6 +21,7 @@ serde_json = { workspace = true }
21
  tempfile = { workspace = true }
22
  tokio = { workspace = true }
23
  uuid = { workspace = true }
 
24
 
25
  [dev-dependencies]
26
  tokio = { workspace = true }
 
21
  tempfile = { workspace = true }
22
  tokio = { workspace = true }
23
  uuid = { workspace = true }
24
+ zip = { workspace = true }
25
 
26
  [dev-dependencies]
27
  tokio = { workspace = true }
tests/integration-tests/tests/pipelines.rs CHANGED
@@ -1,6 +1,8 @@
1
  //! Pipelines + operations + downloads. We don't run real engines (would require
2
  //! loading multi-gigabyte models); we verify the routes surface.
3
 
 
 
4
  use koharu_client::apis::default_api as api;
5
  use koharu_client::models;
6
  use koharu_core::{ImageRole, JobStatus, NodeKind, PageId};
@@ -123,9 +125,10 @@ async fn cancel_operation_via_download_id_is_204() -> anyhow::Result<()> {
123
  }
124
 
125
  #[tokio::test]
126
- async fn renderer_pipeline_noops_on_pages_without_text_blocks() -> anyhow::Result<()> {
 
127
  let app = TestApp::spawn().await?;
128
- app.open_fresh_project("render-noop").await?;
129
 
130
  let png = TestApp::tiny_png(24, 24, [255, 255, 255, 255]);
131
  let page_ids = import_pages(&app, vec![("a.png", png.clone()), ("b.png", png)]).await?;
@@ -151,18 +154,54 @@ async fn renderer_pipeline_noops_on_pages_without_text_blocks() -> anyhow::Resul
151
  assert_eq!(job.status, JobStatus::Completed);
152
  assert_eq!(job.error, None);
153
 
154
- let session = app.app.current_session().expect("session");
155
- let scene = session.scene.read();
156
- for page_id in page_ids {
157
- let page_id = page_id.parse::<uuid::Uuid>().map(PageId)?;
158
- let page = scene.page(page_id).expect("page exists");
159
- let rendered = page
160
- .nodes
161
- .values()
162
- .filter(|node| matches!(&node.kind, NodeKind::Image(img) if img.role == ImageRole::Rendered))
163
- .count();
164
- assert_eq!(rendered, 0, "renderer should no-op on textless page");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  }
 
 
 
 
 
166
 
167
  Ok(())
168
  }
 
1
  //! Pipelines + operations + downloads. We don't run real engines (would require
2
  //! loading multi-gigabyte models); we verify the routes surface.
3
 
4
+ use std::io::Cursor;
5
+
6
  use koharu_client::apis::default_api as api;
7
  use koharu_client::models;
8
  use koharu_core::{ImageRole, JobStatus, NodeKind, PageId};
 
125
  }
126
 
127
  #[tokio::test]
128
+ async fn renderer_pipeline_creates_final_render_for_pages_without_text_blocks() -> anyhow::Result<()>
129
+ {
130
  let app = TestApp::spawn().await?;
131
+ app.open_fresh_project("render-textless").await?;
132
 
133
  let png = TestApp::tiny_png(24, 24, [255, 255, 255, 255]);
134
  let page_ids = import_pages(&app, vec![("a.png", png.clone()), ("b.png", png)]).await?;
 
154
  assert_eq!(job.status, JobStatus::Completed);
155
  assert_eq!(job.error, None);
156
 
157
+ {
158
+ let session = app.app.current_session().expect("session");
159
+ let scene = session.scene.read();
160
+ for page_id in &page_ids {
161
+ let page_id = page_id.parse::<uuid::Uuid>().map(PageId)?;
162
+ let page = scene.page(page_id).expect("page exists");
163
+ let rendered = page
164
+ .nodes
165
+ .values()
166
+ .filter(|node| {
167
+ matches!(&node.kind, NodeKind::Image(img) if img.role == ImageRole::Rendered)
168
+ })
169
+ .count();
170
+ assert_eq!(
171
+ rendered, 1,
172
+ "renderer should create a final render for textless page"
173
+ );
174
+ }
175
+ }
176
+
177
+ let res = app
178
+ .client_config
179
+ .client
180
+ .post(format!("{}/projects/current/export", app.base_url))
181
+ .json(&models::ExportProjectRequest {
182
+ format: models::ExportFormat::Rendered,
183
+ pages: None,
184
+ })
185
+ .send()
186
+ .await?
187
+ .error_for_status()?;
188
+ let body = res.bytes().await?;
189
+ let mut archive = zip::ZipArchive::new(Cursor::new(body.to_vec()))?;
190
+ assert_eq!(
191
+ archive.len(),
192
+ 2,
193
+ "rendered export should include every page"
194
+ );
195
+
196
+ let mut names = Vec::new();
197
+ for i in 0..archive.len() {
198
+ names.push(archive.by_index(i)?.name().to_string());
199
  }
200
+ names.sort();
201
+ assert!(
202
+ names.iter().all(|name| name.ends_with(".png")),
203
+ "rendered export entries should be PNGs: {names:?}",
204
+ );
205
 
206
  Ok(())
207
  }