1use rusqlite::Connection;
15use rusqlite::types::Value;
16
17use crate::ClipFile;
18use crate::cmc::CmcFile;
19use crate::database::{has_column, has_table, quote, select, value_as_text};
20use crate::error::{Error, Result, bail};
21use crate::image::{RgbaImage, decode_png};
22use crate::offscreen::{Attribute, BLOCK_SIZE, BlockData, Offscreen, PixelKind, encode};
23use crate::story::{
24 ATTR_COLOR, ATTR_FONT_LABEL, ATTR_ORIGIN, ATTR_STORY_PART, Align, LEFT_PAGE_FLAG, NOMBRE_INFO, RIGHT_PAGE_FLAG,
25 STORY_INFO, StoryPart, array_parts, canvas_value, drop_render_cache, edit_story_blocks, em_px, i32_bytes, i32s,
26 map_records, mirror_nombre, page_number_text, resize_block, resolution, round_half_even, set_run_length,
27 set_string_array, story_layers, string_array, text_width, update_nombre,
28};
29use crate::text::{Attributes, ids, join_array, split_array};
30
31pub const BLANK_PAGE: &[u8] = include_bytes!("../assets/blank-page.clip");
33pub const BLANK_PARTS: &[u8] = include_bytes!("../assets/blank-parts.clip");
36pub const BLANK_WORK: &[u8] = include_bytes!("../assets/blank-work.cmc");
38pub const NOMBRE_EDGE: &[u8] = include_bytes!("../assets/nombre-edge.bin");
41
42#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44pub struct StoryFont {
45 pub name: String,
47 pub postscript: String,
49}
50
51fn first_number() -> i64 {
52 1
53}
54
55#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct PageSetup {
59 pub canvas_mm: [f64; 2],
61 pub resolution: f64,
63 pub trim_mm: [f64; 2],
65 pub bleed_mm: f64,
67 pub basic_mm: [f64; 2],
69 #[serde(default)]
71 pub basic_offset_mm: [f64; 2],
72 pub right_binding: bool,
74 pub first_page_left: bool,
76 pub title: String,
78 #[serde(default)]
80 pub story_index: Option<i64>,
81 #[serde(default)]
83 pub author: String,
84 #[serde(default = "first_number")]
86 pub nombre_start: i64,
87 #[serde(default)]
89 pub font: Option<StoryFont>,
90 #[serde(default)]
92 pub story: StoryLayout,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
97#[serde(rename_all = "lowercase")]
98pub enum StoryPosition {
99 #[default]
101 Left,
102 Center,
104 Right,
106}
107
108impl StoryPosition {
109 pub fn column(self) -> i64 {
111 match self {
112 StoryPosition::Left => 1,
113 StoryPosition::Center => 2,
114 StoryPosition::Right => 3,
115 }
116 }
117
118 fn align(self) -> Align {
119 Align::of_position(self.column())
120 }
121}
122
123fn center() -> StoryPosition {
124 StoryPosition::Center
125}
126
127fn right() -> StoryPosition {
128 StoryPosition::Right
129}
130
131#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct StoryLayout {
137 #[serde(default)]
139 pub subtitle: String,
140 #[serde(default)]
142 pub title_position: StoryPosition,
143 #[serde(default = "center")]
145 pub author_position: StoryPosition,
146 #[serde(default)]
148 pub page_number: Option<PageNumber>,
149 #[serde(default)]
151 pub nombre: Option<ShownNombre>,
152}
153
154impl Default for StoryLayout {
155 fn default() -> Self {
156 StoryLayout {
157 subtitle: String::new(),
158 title_position: StoryPosition::Left,
159 author_position: StoryPosition::Center,
160 page_number: None,
161 nombre: None,
162 }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct PageNumber {
170 pub start: i64,
172 #[serde(default = "right")]
174 pub position: StoryPosition,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
180#[serde(rename_all = "kebab-case")]
181pub enum NombrePlace {
182 TopInner,
184 Top,
186 TopOuter,
188 BottomInner,
190 Bottom,
192 BottomOuter,
194}
195
196impl NombrePlace {
197 pub fn column(self) -> i64 {
199 match self {
200 NombrePlace::TopInner => 1,
201 NombrePlace::Top => 2,
202 NombrePlace::TopOuter => 3,
203 NombrePlace::BottomInner => 4,
204 NombrePlace::Bottom => 5,
205 NombrePlace::BottomOuter => 6,
206 }
207 }
208
209 fn top(self) -> bool {
210 self.column() <= 3
211 }
212
213 fn align(self) -> Align {
214 match self.column() {
215 1 | 4 => Align::End,
216 2 | 5 => Align::Middle,
217 _ => Align::Start,
218 }
219 }
220}
221
222fn nombre_offset() -> [f64; 2] {
223 [0.0, 5.0]
224}
225
226fn nombre_size() -> f64 {
227 9.0
228}
229
230#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct ShownNombre {
234 pub place: NombrePlace,
236 #[serde(default)]
238 pub white: bool,
239 #[serde(default)]
241 pub edge_mm: Option<f64>,
242 #[serde(default)]
244 pub prefix: String,
245 #[serde(default)]
247 pub suffix: String,
248 #[serde(default = "nombre_offset")]
250 pub offset_mm: [f64; 2],
251 #[serde(default = "nombre_size")]
253 pub size_pt: f64,
254}
255
256impl ShownNombre {
257 fn text(&self, number: i64) -> String {
258 format!("{}{number}{}", self.prefix, self.suffix)
259 }
260}
261
262pub const MAX_SIDE_PX: i64 = 30_000;
264
265fn mm_to_px(mm: f64, resolution: f64) -> i64 {
266 round_half_even(mm * resolution / 25.4)
267}
268
269impl PageSetup {
270 pub fn check(&self) -> Result<()> {
274 let positive = |values: &[f64]| values.iter().all(|value| value.is_finite() && *value > 0.0);
275 if !positive(&[self.canvas_mm[0], self.canvas_mm[1], self.trim_mm[0], self.trim_mm[1]])
276 || !positive(&[self.basic_mm[0], self.basic_mm[1]])
277 {
278 bail!("Canvas, trim and basic frame sizes must be positive");
279 }
280 if !(self.resolution.is_finite() && (72.0..=1200.0).contains(&self.resolution)) {
281 bail!("Resolution {} is outside 72–1200 dpi", self.resolution);
282 }
283 if self.trim_mm[0] > self.canvas_mm[0] + 1e-9 || self.trim_mm[1] > self.canvas_mm[1] + 1e-9 {
284 bail!("The trim frame is larger than the canvas");
285 }
286 if !(self.bleed_mm.is_finite() && self.bleed_mm >= 0.0) {
287 bail!("The bleed must be 0 or more");
288 }
289 let [ox, oy] = self.basic_offset_mm;
290 if !(ox.is_finite() && oy.is_finite())
291 || self.basic_mm[0] / 2.0 + ox.abs() > self.canvas_mm[0] / 2.0 + 1e-9
292 || self.basic_mm[1] / 2.0 + oy.abs() > self.canvas_mm[1] / 2.0 + 1e-9
293 {
294 bail!("The basic frame goes past the canvas");
295 }
296 let (width, height) = self.canvas_px();
297 if width > MAX_SIDE_PX || height > MAX_SIDE_PX {
298 bail!("The canvas is {width}×{height} px; CSP takes up to {MAX_SIDE_PX}");
299 }
300 if self.story_index.is_some_and(|index| index < 0)
301 || self.nombre_start < 0
302 || self.story.page_number.as_ref().is_some_and(|number| number.start < 0)
303 {
304 bail!("Story and page numbers must be 0 or more");
305 }
306 if let Some(nombre) = &self.story.nombre
307 && !(nombre.size_pt.is_finite()
308 && nombre.size_pt > 0.0
309 && nombre.edge_mm.is_none_or(|edge| edge.is_finite() && edge >= 0.0)
310 && nombre.offset_mm.iter().all(|offset| offset.is_finite()))
311 {
312 bail!("The shown nombre needs a positive size and finite offsets");
313 }
314 Ok(())
315 }
316
317 fn px(&self, mm: f64) -> i64 {
318 mm_to_px(mm, self.resolution)
319 }
320
321 pub fn canvas_px(&self) -> (i64, i64) {
323 (self.px(self.canvas_mm[0]), self.px(self.canvas_mm[1]))
324 }
325
326 pub fn story_line(&self) -> String {
328 let mut line = self.title.trim().to_owned();
329 if let Some(index) = self.story_index {
330 line.push_str(&format!(" {index}話"));
331 }
332 let subtitle = self.story.subtitle.trim();
333 if !subtitle.is_empty() {
334 line.push(' ');
335 line.push_str(subtitle);
336 }
337 line
338 }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq)]
343struct Frames {
344 width: i64,
345 height: i64,
346 resolution: f64,
347 trim: [i64; 4],
348 basic: [i64; 4],
349}
350
351impl Frames {
352 fn new(width: i64, height: i64, resolution: f64, trim: (i64, i64), basic: (i64, i64), offset: (i64, i64)) -> Self {
353 let centred = |(w, h): (i64, i64), (dx, dy): (i64, i64)| {
354 let x0 = (width - w).div_euclid(2) + dx;
355 let y0 = (height - h).div_euclid(2) + dy;
356 [x0, y0, x0 + w, y0 + h]
357 };
358 Frames { width, height, resolution, trim: centred(trim, (0, 0)), basic: centred(basic, offset) }
359 }
360
361 fn of_setup(setup: &PageSetup) -> Self {
362 let (width, height) = setup.canvas_px();
363 Frames::new(
364 width,
365 height,
366 setup.resolution,
367 (setup.px(setup.trim_mm[0]), setup.px(setup.trim_mm[1])),
368 (setup.px(setup.basic_mm[0]), setup.px(setup.basic_mm[1])),
369 (setup.px(setup.basic_offset_mm[0]), setup.px(setup.basic_offset_mm[1])),
370 )
371 }
372
373 fn of_page(db: &Connection) -> Result<Self> {
375 let real = |column: &str| -> Result<f64> {
376 Ok(match canvas_value(db, column)? {
377 Some(Value::Real(number)) => number,
378 Some(Value::Integer(number)) => number as f64,
379 _ => 0.0,
380 })
381 };
382 let resolution = real("CanvasResolution")?;
383 let px = |column: &str| -> Result<i64> { Ok(mm_to_px(real(column)?, resolution)) };
384 Ok(Frames::new(
385 px("CanvasWidth")?,
386 px("CanvasHeight")?,
387 resolution,
388 (px("CropFrameWidth")?, px("CropFrameHeight")?),
389 (px("CropFrameInnerWidth")?, px("CropFrameInnerHeight")?),
390 (px("CropFrameInnerOffsetX")?, px("CropFrameInnerOffsetY")?),
391 ))
392 }
393}
394
395pub fn blank_page(setup: &PageSetup) -> Result<ClipFile> {
397 setup.check()?;
398 let mut page = ClipFile::from_bytes(BLANK_PAGE.to_vec())?;
399 page.renew_file_id();
400 renew_layer_uuids(&page)?;
401 apply_setup(&mut page, setup)?;
402 Ok(page)
403}
404
405pub fn blank_parts() -> Result<ClipFile> {
408 let mut parts = ClipFile::from_bytes(BLANK_PARTS.to_vec())?;
409 parts.renew_file_id();
410 Ok(parts)
411}
412
413fn renew_layer_uuids(page: &ClipFile) -> Result<()> {
414 if !has_column(page.database(), "Layer", "LayerUuid")? {
415 return Ok(());
416 }
417 for row in select(page.database(), "SELECT MainId FROM Layer WHERE LayerUuid IS NOT NULL", [])? {
418 page.database().execute(
419 "UPDATE Layer SET LayerUuid = ?1 WHERE MainId = ?2",
420 (crate::edit::layer_uuid(), row.int("MainId")),
421 )?;
422 }
423 Ok(())
424}
425
426pub fn apply_setup(page: &mut ClipFile, setup: &PageSetup) -> Result<()> {
429 let db = page.database();
430 if matches!(canvas_value(db, "CanvasDoublePage")?, Some(Value::Integer(flag)) if flag != 0) {
431 bail!("A spread cannot be made into a single page");
432 }
433 if !matches!(canvas_value(db, "ComicIsLeftPage")?, Some(Value::Integer(1))) {
434 bail!("The empty page must be a left page");
435 }
436 let old = Frames::of_page(db)?;
437 let new = Frames::of_setup(setup);
438 if setup.story.page_number.is_some() {
439 add_page_number(db)?;
440 }
441 if let Some(nombre) = &setup.story.nombre {
442 show_nombre(db, nombre, setup.nombre_start, setup.resolution)?;
443 }
444 set_story_texts(db, setup)?;
445 place_story_texts(db, &old, &new, setup)?;
446 drop_text_images(db)?;
447 set_canvas(db, setup)?;
448 if let Some(font) = &setup.font {
449 set_story_font(db, font)?;
450 }
451 if (old.width, old.height) != (new.width, new.height) {
452 resize_offscreens(page, (old.width, old.height), (new.width, new.height))?;
455 set_thumbnail_canvas(page.database(), new.width, new.height)?;
456 blank_preview(page.database(), new.width, new.height)?;
457 }
458 let db = page.database();
459 if !setup.first_page_left {
460 db.execute("UPDATE Canvas SET ComicIsLeftPage = 0", [])?;
461 if let Some(Value::Integer(flag)) = canvas_value(db, "CropFrameOptionFlag")? {
462 let flag = (flag & !(LEFT_PAGE_FLAG | RIGHT_PAGE_FLAG)) | RIGHT_PAGE_FLAG;
463 db.execute("UPDATE Canvas SET CropFrameOptionFlag = ?1", [flag])?;
464 }
465 mirror_nombre(db)?;
466 }
467 update_nombre(db, !setup.first_page_left)?;
469 page.reload()
470}
471
472fn set_columns(db: &Connection, table: &str, values: &[(&str, Value)]) -> Result<()> {
473 for (column, value) in values {
474 if has_column(db, table, column)? {
475 db.execute(&format!("UPDATE {} SET {} = ?1", quote(table), quote(column)), [value])?;
476 }
477 }
478 Ok(())
479}
480
481fn story_values(setup: &PageSetup) -> Vec<(&'static str, Value)> {
484 let mut values = vec![
485 ("ComicStoryStoryName", Value::Text(setup.title.trim().to_owned())),
486 ("ComicStoryUseStoryIndex", Value::Integer(setup.story_index.is_some() as i64)),
487 ("ComicStoryAuthorName", Value::Text(setup.author.trim().to_owned())),
488 ("ComicStoryNombreStart", Value::Integer(setup.nombre_start)),
489 ("ComicBindPosition", Value::Integer(setup.right_binding as i64)),
491 ];
492 if let Some(index) = setup.story_index {
493 values.push(("ComicStoryStoryIndex", Value::Integer(index)));
494 }
495 let story = &setup.story;
496 values.extend([
497 ("ComicStorySubTitle", Value::Text(story.subtitle.trim().to_owned())),
498 ("ComicStoryStoryPosition", Value::Integer(story.title_position.column())),
499 ("ComicStoryAuthorPosition", Value::Integer(story.author_position.column())),
500 ("ComicStoryUsePageNumber", Value::Integer(story.page_number.is_some() as i64)),
501 ("ComicStoryUseShownNombre", Value::Integer(story.nombre.is_some() as i64)),
502 ("ComicStoryUseHiddenNombre", Value::Integer(story.nombre.is_none() as i64)),
503 ]);
504 if let Some(number) = &story.page_number {
505 values.push(("ComicStoryPageNumberStart", Value::Integer(number.start)));
506 values.push(("ComicStoryPageNumberPosition", Value::Integer(number.position.column())));
507 }
508 if let Some(nombre) = &story.nombre {
509 values.extend([
510 ("ComicStoryNombrePosition", Value::Integer(nombre.place.column())),
511 ("ComicStoryNombrePrefix", Value::Text(nombre.prefix.clone())),
512 ("ComicStoryNombreSuffix", Value::Text(nombre.suffix.clone())),
513 ("ComicStoryNombreColor", Value::Integer(nombre.white as i64)),
514 ("ComicStoryNombreUseEdge", Value::Integer(nombre.edge_mm.is_some() as i64)),
515 ("ComicStoryNombreFontSize", Value::Real(nombre.size_pt)),
516 ("ComicStoryXPageNombreOffset", Value::Real(nombre.offset_mm[0])),
517 ("ComicStoryXPageNombreOffsetUnit", Value::Integer(2)),
518 ("ComicStoryYPageNombreOffset", Value::Real(nombre.offset_mm[1])),
519 ("ComicStoryYPageNombreOffsetUnit", Value::Integer(2)),
520 ]);
521 if let Some(edge) = nombre.edge_mm {
522 values.push(("ComicStoryNombreEdgeWidth", Value::Real(edge)));
523 values.push(("ComicStoryNombreEdgeUnit", Value::Integer(2)));
524 }
525 }
526 if let Some(font) = &setup.font {
527 values.push(("ComicStoryNombreFont", Value::Text(font.name.clone())));
528 values.push(("ComicStoryNombreFontPostScriptName", Value::Text(font.postscript.clone())));
529 }
530 values
531}
532
533fn set_canvas(db: &Connection, setup: &PageSetup) -> Result<()> {
534 let real = Value::Real;
535 let mut values = vec![
536 ("CanvasUnit", Value::Integer(2)),
537 ("CanvasWidth", real(setup.canvas_mm[0])),
538 ("CanvasHeight", real(setup.canvas_mm[1])),
539 ("CanvasResolution", real(setup.resolution)),
540 ("CropFrameUnitKind", Value::Integer(2)),
541 ("CropFrameWidth", real(setup.trim_mm[0])),
542 ("CropFrameHeight", real(setup.trim_mm[1])),
543 ("CropFrameDitch", real(setup.bleed_mm)),
544 ("CropFrameInnerWidth", real(setup.basic_mm[0])),
545 ("CropFrameInnerHeight", real(setup.basic_mm[1])),
546 ("CropFrameInnerOffsetX", real(setup.basic_offset_mm[0])),
547 ("CropFrameInnerOffsetY", real(setup.basic_offset_mm[1])),
548 ("CropFrameShow", Value::Integer(1)),
549 ("ComicPageIndex", Value::Integer(0)),
550 ];
551 values.extend(story_values(setup));
552 set_columns(db, "Canvas", &values)
553}
554
555pub fn blank_work(setup: &PageSetup) -> Result<CmcFile> {
558 setup.check()?;
559 let mut work = CmcFile::from_bytes(BLANK_WORK.to_vec())?;
560 let real = Value::Real;
561 let mut values = vec![
562 ("ComicStartPage", Value::Integer((!setup.first_page_left) as i64)),
564 ("DefaultPageUnit", Value::Integer(2)),
565 ("DefaultPageWidth", real(setup.canvas_mm[0])),
566 ("DefaultPageHeight", real(setup.canvas_mm[1])),
567 ("DefaultPageResolution", real(setup.resolution)),
568 ("DefaultPageUseCropFrame", Value::Integer(1)),
569 ("DefaultPageCropWidth", real(setup.trim_mm[0])),
570 ("DefaultPageCropHeight", real(setup.trim_mm[1])),
571 ("DefaultPageCropDitch", real(setup.bleed_mm)),
572 ("DefaultPageCropInnerWidth", real(setup.basic_mm[0])),
573 ("DefaultPageCropInnerHeight", real(setup.basic_mm[1])),
574 ("DefaultPageCropInnerOffsetX", real(setup.basic_offset_mm[0])),
575 ("DefaultPageCropInnerOffsetY", real(setup.basic_offset_mm[1])),
576 ];
577 values.extend(story_values(setup));
578 set_columns(work.database(), "Project", &values)?;
579 work.database().execute(
580 "UPDATE CanvasNode SET LinkUnit = 2, LinkWidth = ?1, LinkHeight = ?2, LinkResolution = ?3 WHERE Type = 2",
581 (setup.canvas_mm[0], setup.canvas_mm[1], setup.resolution),
582 )?;
583 work.modified = true;
584 work.reload()?;
585 Ok(work)
586}
587
588fn edit_array(db: &Connection, layer_id: i64, column: &str, edit: &dyn Fn(&mut Vec<Attributes>)) -> Result<()> {
590 if !has_column(db, "Layer", column)? {
591 return Ok(());
592 }
593 let blob: Value =
594 db.query_row(&format!("SELECT {} FROM Layer WHERE MainId = ?1", quote(column)), [layer_id], |row| row.get(0))?;
595 let Value::Blob(blob) = blob else { return Ok(()) };
596 let mut blocks = split_array(&blob)?.into_iter().map(Attributes::parse).collect::<Result<Vec<_>>>()?;
597 edit(&mut blocks);
598 let joined = join_array(&blocks.iter().map(Attributes::to_bytes).collect::<Vec<_>>());
599 db.execute(&format!("UPDATE Layer SET {} = ?1 WHERE MainId = ?2", quote(column)), (joined, layer_id))?;
600 Ok(())
601}
602
603fn with_part(attributes: &Attributes, part: StoryPart) -> Attributes {
604 map_records(attributes.clone(), |id, data| if id == ATTR_STORY_PART { i32_bytes(&[part.code()]) } else { data })
605}
606
607fn add_page_number(db: &Connection) -> Result<()> {
610 for (layer_id, _) in story_layers(db, STORY_INFO)? {
611 if array_parts(db, layer_id)?.contains(&StoryPart::PageNumber) {
612 continue;
613 }
614 for column in ["TextLayerAttributesArray", "TextLayerAddAttributesV01"] {
615 edit_array(db, layer_id, column, &|blocks| {
616 let author = blocks.iter().rposition(|attributes| {
617 attributes.get(ATTR_STORY_PART).map(i32s).as_deref() == Some(&[StoryPart::Author.code()])
618 });
619 if let Some(author) = author.or(blocks.len().checked_sub(1)) {
620 let block = with_part(&blocks[author], StoryPart::PageNumber);
621 blocks.push(block);
622 }
623 })?;
624 }
625 if let Some(mut strings) = string_array(db, layer_id)? {
626 strings.push(String::new());
627 set_string_array(db, layer_id, &strings)?;
628 }
629 }
630 Ok(())
631}
632
633fn show_nombre(db: &Connection, nombre: &ShownNombre, start: i64, page_resolution: f64) -> Result<()> {
636 let resolution = resolution(db)?;
637 let text = nombre.text(start);
638 let units = text.encode_utf16().count();
639 let size = round_half_even(nombre.size_pt * 100.0);
640 for (layer_id, _) in story_layers(db, NOMBRE_INFO)? {
641 db.execute("UPDATE Layer SET TextLayerString = ?1 WHERE MainId = ?2", (text.as_bytes(), layer_id))?;
642 edit_story_blocks(db, layer_id, NOMBRE_INFO, &|_, counted, attributes| {
643 let old = attributes
644 .get(ids::FONT_SIZE)
645 .map(i32s)
646 .and_then(|values| values.first().copied())
647 .filter(|old| *old > 0)
648 .unwrap_or(600);
649 let ratio = size as f64 / old as f64;
650 let scale = |values: Vec<i64>| -> Vec<u8> {
651 i32_bytes(&values.into_iter().map(|value| round_half_even(value as f64 * ratio)).collect::<Vec<_>>())
652 };
653 let attributes = map_records(attributes, |id, data| match id {
654 ATTR_STORY_PART => i32_bytes(&[StoryPart::Nombre.code()]),
655 ids::FONT_SIZE if data.len() == 4 => i32_bytes(&[size]),
656 ATTR_COLOR if data.len() == 12 => vec![if nombre.white { 0xff } else { 0 }; 12],
657 ids::BLOCK_SIZE | ids::QUAD | ATTR_ORIGIN if data.len() % 4 == 0 => scale(i32s(&data)),
658 ids::BBOX if data.len() == 16 => {
659 let [x0, y0, x1, y1] = i32s(&data)[..] else { unreachable!() };
660 i32_bytes(&[x0, y0, x1, y0 + round_half_even((y1 - y0) as f64 * ratio)])
661 }
662 _ => data,
663 });
664 let width = text_width(&text, em_px(&attributes, resolution));
665 Ok(resize_block(set_run_length(attributes, units, counted), width, Align::Start))
666 })?;
667 drop_render_cache(db, layer_id)?;
668 if let Some(edge) = nombre.edge_mm {
669 set_edge(db, layer_id, edge / 25.4 * page_resolution, nombre.white)?;
671 }
672 }
673 Ok(())
674}
675
676fn set_edge(db: &Connection, layer_id: i64, width: f64, white_text: bool) -> Result<()> {
679 let mut info = NOMBRE_EDGE.to_vec();
680 let at = 4 + 4 + 4 + "EffectEdge".len() * 2;
682 if info.get(at..at + 4) != Some(&[0, 0, 0, 1]) {
683 bail!("Unexpected layer effect template");
684 }
685 info[at + 4..at + 12].copy_from_slice(&width.to_be_bytes());
686 let channel: u32 = if white_text { 0 } else { 255 };
687 for index in 0..3 {
688 let start = at + 12 + index * 4;
689 info[start..start + 4].copy_from_slice(&channel.to_be_bytes());
690 }
691 let mut values: Vec<(&str, Value)> = [
692 "EffectLayerRenderType",
693 "EffectRenderType",
694 "EffectReferAreaType",
695 "EffectSetUpdateRectType",
696 "EffectOffscreenFixType",
697 "EffectOffscreenMoveType",
698 ]
699 .into_iter()
700 .map(|column| (column, Value::Integer(10)))
701 .collect();
702 values.push(("LayerEffectAttached", Value::Integer(1)));
703 values.push(("LayerEffectEdgeNewRender", Value::Integer(1)));
704 values.push(("LayerEffectInfo", Value::Blob(info)));
705 for (column, value) in values {
706 if !has_column(db, "Layer", column)? {
707 db.execute(&format!("ALTER TABLE Layer ADD COLUMN {}", quote(column)), [])?;
708 }
709 db.execute(&format!("UPDATE Layer SET {} = ?1 WHERE MainId = ?2", quote(column)), (value, layer_id))?;
710 }
711 Ok(())
712}
713
714fn set_story_texts(db: &Connection, setup: &PageSetup) -> Result<()> {
718 let resolution = resolution(db)?;
719 let title = setup.story_line();
720 let author = setup.author.trim().to_owned();
721 let page = setup.story.page_number.as_ref().map(|number| page_number_text(number.start));
723 for (layer_id, _) in story_layers(db, STORY_INFO)? {
724 db.execute("UPDATE Layer SET TextLayerString = ?1 WHERE MainId = ?2", (title.as_bytes(), layer_id))?;
725 if let Some(mut strings) = string_array(db, layer_id)? {
726 let parts = array_parts(db, layer_id)?;
727 strings.resize(parts.len().max(strings.len()), String::new());
728 for (index, part) in parts.iter().enumerate() {
729 match (part, &page) {
730 (StoryPart::Author, _) => strings[index] = author.clone(),
731 (StoryPart::PageNumber, Some(page)) => strings[index] = page.clone(),
732 _ => {}
733 }
734 }
735 set_string_array(db, layer_id, &strings)?;
736 }
737 edit_story_blocks(db, layer_id, STORY_INFO, &|part, counted, attributes| {
738 let text = match (part, &page) {
739 (StoryPart::Title, _) => &title,
740 (StoryPart::Author, _) => &author,
741 (StoryPart::PageNumber, Some(page)) => page,
742 _ => return Ok(attributes),
743 };
744 let width = text_width(text, em_px(&attributes, resolution));
745 Ok(resize_block(set_run_length(attributes, text.encode_utf16().count(), counted), width, Align::Start))
746 })?;
747 drop_render_cache(db, layer_id)?;
748 }
749 Ok(())
750}
751
752fn layer_offset(db: &Connection, layer_id: i64) -> Result<(i64, i64)> {
753 let (x, y): (Option<i64>, Option<i64>) =
754 db.query_row("SELECT LayerOffsetX, LayerOffsetY FROM Layer WHERE MainId = ?1", [layer_id], |row| {
755 Ok((row.get(0)?, row.get(1)?))
756 })?;
757 Ok((x.unwrap_or(0), y.unwrap_or(0)))
758}
759
760fn story_rows(story: &StoryLayout) -> (impl Fn(StoryPart) -> Option<(StoryPosition, i64)>, i64) {
764 let order = [
765 (StoryPart::PageNumber, story.page_number.as_ref().map(|number| number.position)),
766 (StoryPart::Title, Some(story.title_position)),
767 (StoryPart::Author, Some(story.author_position)),
768 ];
769 let row = move |part: StoryPart| {
770 let at = order.iter().position(|(one, _)| *one == part)?;
771 let position = order[at].1?;
772 let above = order[..at].iter().filter(|(_, other)| *other == Some(position)).count();
773 Some((position, above as i64))
774 };
775 let rows = order
776 .iter()
777 .filter_map(|(_, position)| *position)
778 .map(|position| order.iter().filter(|(_, other)| *other == Some(position)).count())
779 .max()
780 .unwrap_or(1) as i64;
781 (row, rows)
782}
783
784const STORY_MARGIN_MM: f64 = 0.5;
786
787fn place_story_texts(db: &Connection, old: &Frames, new: &Frames, setup: &PageSetup) -> Result<()> {
793 let scale = new.resolution / old.resolution;
794 let at = |anchor_old: i64, anchor_new: i64, value: i64| {
795 anchor_new + round_half_even((value - anchor_old) as f64 * scale)
796 };
797 let px = |mm: f64| mm_to_px(mm, new.resolution);
798 let margin = round_half_even(STORY_MARGIN_MM / 25.4 * new.resolution);
803 let (row_of, rows) = story_rows(&setup.story);
804 let below = |y0: i64, height: i64, row: i64| {
805 let first = at(old.trim[3], new.trim[3], y0);
806 let lift = (first + rows * height - (new.height - margin)).max(0);
807 first + row * height - lift
808 };
809 for info in [STORY_INFO, NOMBRE_INFO] {
810 for (layer_id, _) in story_layers(db, info)? {
811 let (dx, dy) = layer_offset(db, layer_id)?;
812 edit_story_blocks(db, layer_id, info, &|part, _, attributes| {
813 Ok(map_records(attributes, |id, data| match id {
814 ids::BBOX if data.len() == 16 => {
815 let [x0, y0, x1, y1] = i32s(&data)[..] else { unreachable!() };
816 let (x0, y0, x1, y1) = (x0 + dx, y0 + dy, x1 + dx, y1 + dy);
817 let width = round_half_even((x1 - x0) as f64 * scale);
818 let height = round_half_even((y1 - y0) as f64 * scale);
819 let along = |align: Align, from: i64, to: i64| match align {
820 Align::Start => from,
821 Align::Middle => (from + to) / 2 - width / 2,
822 Align::End => to - width,
823 };
824 let (x0, y0) = match (part, &setup.story.nombre) {
825 (StoryPart::Nombre, None) => (
826 at(old.trim[2], new.trim[2], x1) - width,
828 at(old.basic[3], new.basic[3], y1) - height,
829 ),
830 (StoryPart::Nombre, Some(nombre)) => {
831 let [across, apart] = [px(nombre.offset_mm[0]), px(nombre.offset_mm[1])];
833 let x = match nombre.place.align() {
834 Align::Start => new.basic[0] + across,
835 Align::Middle => along(Align::Middle, new.basic[0], new.basic[2]),
836 Align::End => new.basic[2] - across - width,
837 };
838 let y = if nombre.place.top() {
839 new.basic[1] - apart - height
840 } else {
841 new.basic[3] + apart
842 };
843 (x, y)
844 }
845 (part, _) => match row_of(part) {
846 Some((position, row)) => {
847 (along(position.align(), new.trim[0], new.trim[2]), below(y0, height, row))
848 }
849 None => (at(old.trim[0], new.trim[0], x0), below(y0, height, 0)),
850 },
851 };
852 i32_bytes(&[x0 - dx, y0 - dy, x0 + width - dx, y0 + height - dy])
853 }
854 ids::BLOCK_SIZE | ids::QUAD | ATTR_ORIGIN if data.len() % 4 == 0 && scale != 1.0 => {
855 let scaled: Vec<i64> =
856 i32s(&data).into_iter().map(|number| round_half_even(number as f64 * scale)).collect();
857 i32_bytes(&scaled)
858 }
859 _ => data,
860 }))
861 })?;
862 drop_render_cache(db, layer_id)?;
863 }
864 }
865 Ok(())
866}
867
868fn drop_text_images(db: &Connection) -> Result<()> {
872 let mut layers = story_layers(db, STORY_INFO)?;
873 layers.extend(story_layers(db, NOMBRE_INFO)?);
874 for (layer_id, _) in layers {
875 db.execute(
876 "DELETE FROM Offscreen WHERE LayerId = ?1 \
877 AND MainId NOT IN (SELECT Offscreen FROM MipmapInfo WHERE Offscreen IS NOT NULL) \
878 AND MainId NOT IN (SELECT ThumbnailOffscreen FROM LayerThumbnail WHERE ThumbnailOffscreen IS NOT NULL)",
879 [layer_id],
880 )?;
881 }
882 Ok(())
883}
884
885fn set_story_font(db: &Connection, font: &StoryFont) -> Result<()> {
887 let mut attribute = 1u16.to_le_bytes().to_vec();
889 for text in [&font.name, &font.postscript] {
890 attribute.extend((text.len() as u16).to_le_bytes());
891 attribute.extend(text.as_bytes());
892 }
893 for info in [STORY_INFO, NOMBRE_INFO] {
894 for (layer_id, _) in story_layers(db, info)? {
895 edit_story_blocks(db, layer_id, info, &|_, _, attributes| {
896 Ok(map_records(attributes, |id, data| match id {
897 ATTR_FONT_LABEL if !data.is_empty() => font.name.as_bytes().to_vec(),
898 ids::FONT if !data.is_empty() => attribute.clone(),
899 _ => data,
900 }))
901 })?;
902 }
903 }
904 Ok(())
905}
906
907fn levels(base: i64) -> Vec<i64> {
910 let mut result = vec![base];
911 while result.len() < 16 && *result.last().unwrap() > 1 {
912 result.push(result.last().unwrap() / 2);
913 }
914 result
915}
916
917fn padded(size: i64) -> i64 {
918 let block = BLOCK_SIZE as i64;
919 (size + block - 1) / block * block
920}
921
922fn resize_offscreens(
928 page: &mut ClipFile,
929 (width, height): (i64, i64),
930 (new_width, new_height): (i64, i64),
931) -> Result<()> {
932 let families = [
933 (levels(width), levels(height), levels(new_width), levels(new_height)),
934 (levels(padded(width)), levels(padded(height)), levels(padded(new_width)), levels(padded(new_height))),
935 ];
936 for row in select(page.database(), "SELECT MainId, Attribute, BlockData FROM Offscreen", [])? {
937 let Some(attribute) = row.blob("Attribute") else { continue };
938 let Ok(mut attribute) = Attribute::parse(attribute) else { continue };
939 let Ok(parameter) = attribute.parameter() else { continue };
940 let size = (parameter.width as i64, parameter.height as i64);
941 let Some((target_width, target_height)) = families.iter().find_map(|(widths, heights, new_w, new_h)| {
942 let count = widths.len().min(heights.len()).min(new_w.len()).min(new_h.len());
943 (0..count).find(|level| (widths[*level], heights[*level]) == size).map(|level| (new_w[level], new_h[level]))
944 }) else {
945 continue;
946 };
947 let external = row.get("BlockData").and_then(value_as_text).unwrap_or_default();
948 let stored = page.external(&external).map(<[u8]>::to_vec);
949 let old = match &stored {
950 Some(payload) => Some(Offscreen::parse(&attribute.to_bytes(), Some(payload))?.to_rgba()?),
951 None => None,
952 };
953 let resized = crate::offscreen::Parameter {
954 width: target_width as u32,
955 height: target_height as u32,
956 blocks_x: (target_width as u32).div_ceil(BLOCK_SIZE),
957 blocks_y: (target_height as u32).div_ceil(BLOCK_SIZE),
958 ..parameter
959 };
960 let image =
961 old.filter(|old| old.alpha_bbox().is_some()).map(|old| old.resize_area(resized.width, resized.height));
962 let data = match (&image, resized.kind()) {
963 (Some(image), PixelKind::Color | PixelKind::Gray) => encode(&resized, Some(image), None)?,
964 (Some(_), kind) => bail!("Resizing {kind:?} offscreens with pixels is not supported"),
965 (None, _) => BlockData::from_raw_blocks(&vec![None; resized.block_count()], resized.block_bytes())?,
966 };
967 let values = &mut attribute
968 .sections
969 .iter_mut()
970 .find(|section| section.name == "Parameter")
971 .ok_or_else(|| Error::format("Offscreen attribute has no Parameter section"))?
972 .values;
973 values[..4].copy_from_slice(&[resized.width, resized.height, resized.blocks_x, resized.blocks_y]);
974 attribute.set_block_sizes(&data.record_sizes());
975 page.database().execute(
976 "UPDATE Offscreen SET Attribute = ?1 WHERE MainId = ?2",
977 (attribute.to_bytes(), row.int("MainId")),
978 )?;
979 if stored.is_some() {
980 page.set_external(&external, data.to_bytes());
981 }
982 }
983 Ok(())
984}
985
986fn set_thumbnail_canvas(db: &Connection, width: i64, height: i64) -> Result<()> {
988 for (column, value) in [("ThumbnailCanvasWidth", width), ("ThumbnailCanvasHeight", height)] {
989 if has_column(db, "LayerThumbnail", column)? {
990 db.execute(&format!("UPDATE LayerThumbnail SET {} = ?1", quote(column)), [value])?;
991 }
992 }
993 Ok(())
994}
995
996const PREVIEW_SCALE: f64 = 8.0;
998
999fn blank_preview(db: &Connection, width: i64, height: i64) -> Result<()> {
1002 if !has_table(db, "CanvasPreview")? {
1003 return Ok(());
1004 }
1005 let Some(row) = select(db, "SELECT rowid AS id, ImageData FROM CanvasPreview ORDER BY rowid LIMIT 1", [])?.pop()
1006 else {
1007 return Ok(());
1008 };
1009 if row.blob("ImageData").is_none_or(|png| decode_png(png).is_err()) {
1010 return Ok(());
1011 }
1012 let size = |pixels: i64| ((pixels as f64 / PREVIEW_SCALE).round() as u32).max(1);
1013 let mut preview = RgbaImage::new(size(width), size(height));
1014 preview.fill([255, 255, 255, 255]);
1015 let id = row.int("id");
1016 db.execute("UPDATE CanvasPreview SET ImageData = ?1 WHERE rowid = ?2", (preview.to_png()?, id))?;
1017 for (column, value) in [("ImageWidth", preview.width), ("ImageHeight", preview.height)] {
1018 if has_column(db, "CanvasPreview", column)? {
1019 db.execute(&format!("UPDATE CanvasPreview SET {} = ?1 WHERE rowid = ?2", quote(column)), (value, id))?;
1020 }
1021 }
1022 Ok(())
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028 use crate::model::LayerKind;
1029
1030 fn commercial() -> PageSetup {
1033 PageSetup {
1034 canvas_mm: [297.0, 420.0],
1035 resolution: 600.0,
1036 trim_mm: [257.0, 364.0],
1037 bleed_mm: 5.0,
1038 basic_mm: [212.0, 300.0],
1039 basic_offset_mm: [0.0, 0.0],
1040 right_binding: true,
1041 first_page_left: true,
1042 title: "ユメの話".into(),
1043 story_index: Some(4),
1044 author: "だれか".into(),
1045 nombre_start: 1,
1046 font: None,
1047 story: StoryLayout::default(),
1048 }
1049 }
1050
1051 fn shown(place: NombrePlace, position: StoryPosition) -> PageSetup {
1055 PageSetup {
1056 canvas_mm: [70.0, 90.0],
1057 trim_mm: [60.0, 80.0],
1058 basic_mm: [60.0, 80.0],
1059 title: "G1".into(),
1060 story_index: None,
1061 author: "作者".into(),
1062 story: StoryLayout {
1063 subtitle: "サブ".into(),
1064 title_position: position,
1065 author_position: position,
1066 page_number: Some(PageNumber { start: 7, position }),
1067 nombre: Some(ShownNombre {
1068 place,
1069 white: true,
1070 edge_mm: Some(0.5),
1071 prefix: "p.".into(),
1072 suffix: "-".into(),
1073 offset_mm: [0.0, 3.0],
1074 size_pt: 9.0,
1075 }),
1076 },
1077 ..commercial()
1078 }
1079 }
1080
1081 fn story_parts(page: &ClipFile, info: i64) -> Vec<(i64, String, [i32; 4])> {
1083 let mut parts = Vec::new();
1084 for (id, _) in story_layers(page.database(), info).unwrap() {
1085 let layer = page.layers.get(id).unwrap();
1086 let (dx, dy) = (layer.offset.0 as i32, layer.offset.1 as i32);
1087 for block in crate::text::read_blocks(&layer.row).unwrap() {
1088 let part = block.attributes.get(ATTR_STORY_PART).map(i32s).map_or(0, |values| values[0]);
1089 let [x0, y0, x1, y1] = block.attributes.bbox().unwrap();
1090 parts.push((part, block.text, [x0 + dx, y0 + dy, x1 + dx, y1 + dy]));
1091 }
1092 }
1093 parts
1094 }
1095
1096 fn canvas_ints(page: &ClipFile, columns: &[&str]) -> Vec<i64> {
1097 columns.iter().map(|column| page.canvas.row.int(column)).collect()
1098 }
1099
1100 fn reopened(page: ClipFile) -> ClipFile {
1101 ClipFile::from_bytes(page.to_bytes().unwrap()).unwrap()
1102 }
1103
1104 #[test]
1105 fn keeps_csp_layout_for_the_settings_it_was_saved_with() {
1106 let page = reopened(blank_page(&commercial()).unwrap());
1107 assert_eq!((page.canvas.width_px, page.canvas.height_px), (7016, 9921));
1108 let story = story_parts(&page, STORY_INFO);
1111 assert_eq!(story[0].1, "ユメの話 4話");
1112 assert_eq!(story[0].2[..2], [472, 9402]);
1113 assert_eq!(story[1].1, "だれか");
1114 assert!(((story[1].2[0] + story[1].2[2]) / 2 - 3507).abs() <= 1, "{story:?}");
1115 let nombre = story_parts(&page, NOMBRE_INFO);
1116 assert_eq!((nombre[0].1.as_str(), nombre[0].2), ("1", [6512, 8454, 6543, 8504]));
1117 assert_eq!(canvas_ints(&page, &["ComicIsLeftPage", "CropFrameOptionFlag", "ComicBindPosition"]), [1, 2, 1]);
1118 assert!(crate::verify::check_clip(page.to_bytes().unwrap(), Default::default()).ok());
1119 let work = blank_work(&commercial()).unwrap();
1120 assert_eq!(work.project.bind_position, 1);
1121 assert_eq!(work.project.start_page, 0);
1122 assert_eq!(work.pages().unwrap().len(), 1);
1123 }
1124
1125 #[test]
1126 fn makes_pages_of_another_size_and_binding() {
1127 let setup = PageSetup {
1128 canvas_mm: [158.0, 220.0],
1129 resolution: 350.0,
1130 trim_mm: [148.0, 210.0],
1131 bleed_mm: 3.0,
1132 basic_mm: [120.0, 180.0],
1133 basic_offset_mm: [2.0, -3.0],
1134 right_binding: false,
1135 first_page_left: false,
1136 title: "Short".into(),
1137 story_index: None,
1138 author: String::new(),
1139 nombre_start: 3,
1140 font: Some(StoryFont { name: "BIZ UDゴシック".into(), postscript: "BIZ-UDGothic".into() }),
1141 story: StoryLayout::default(),
1142 };
1143 let page = reopened(blank_page(&setup).unwrap());
1144 let (width, height) = (page.canvas.width_px as i64, page.canvas.height_px as i64);
1145 assert_eq!((width, height), (2177, 3031));
1146 for row in select(page.database(), "SELECT Attribute FROM Offscreen", []).unwrap() {
1148 let parameter = Attribute::parse(row.blob("Attribute").unwrap()).unwrap().parameter().unwrap();
1149 let size = (parameter.width as i64, parameter.height as i64);
1150 let fits = (0..8).any(|level| {
1151 size == (width >> level, height >> level) || size == (padded(width) >> level, padded(height) >> level)
1152 }) || size == (512, 512);
1153 assert!(fits, "offscreen {size:?} is not sized by the {width}x{height} canvas");
1154 }
1155 assert_eq!(canvas_ints(&page, &["ComicIsLeftPage", "CropFrameOptionFlag", "ComicBindPosition"]), [0, 4, 0]);
1157 let nombre = story_parts(&page, NOMBRE_INFO);
1158 assert!((nombre[0].2[0] as i64 - (width - 2039) / 2).abs() <= 1, "{nombre:?}");
1159 assert_eq!(nombre[0].2[3] as i64, (height - 2480) / 2 - 41 + 2480);
1160 assert_eq!(nombre[0].1, "3");
1161 let story = story_parts(&page, STORY_INFO);
1162 assert_eq!((story[0].1.as_str(), story[1].1.as_str()), ("Short", ""));
1163 let fonts = page
1164 .database()
1165 .query_row("SELECT TextLayerAttributes FROM Layer WHERE ComicStoryInfoType = 2", [], |row| {
1166 row.get::<_, Vec<u8>>(0)
1167 })
1168 .unwrap();
1169 assert!(fonts.windows(12).any(|window| window == b"BIZ-UDGothic"));
1170 let work = blank_work(&setup).unwrap();
1171 assert_eq!((work.project.bind_position, work.project.start_page), (0, 1));
1172 assert!(crate::verify::check_clip(page.to_bytes().unwrap(), Default::default()).ok());
1173 }
1174
1175 #[test]
1176 fn lays_out_a_shown_nombre_and_page_numbers_as_csp_does() {
1177 let close = |a: i32, b: i32| (a - b).abs() <= 1;
1178 let setup = PageSetup { canvas_mm: [70.0, 110.0], ..shown(NombrePlace::TopInner, StoryPosition::Left) };
1180 let page = reopened(blank_page(&setup).unwrap());
1181 assert_eq!((page.canvas.width_px, page.canvas.height_px), (1654, 2598));
1182 let lower = (2598 - 2126) / 2;
1183 let story = story_parts(&page, STORY_INFO);
1184 let texts: Vec<(i64, &str)> = story.iter().map(|(part, text, _)| (*part, text.as_str())).collect();
1185 assert_eq!(texts, [(1, "G1 サブ"), (2, "作者"), (4, "7ページ")]);
1186 let bbox = |part: i64| story.iter().find(|block| block.0 == part).unwrap().2;
1187 for (part, y) in [(4, 2150), (1, 2200), (2, 2250)] {
1188 let [x0, y0, _, y1] = bbox(part);
1189 assert!(x0 == 118 && close(y0, y + lower) && y1 - y0 == 50, "{part}: {:?}", bbox(part));
1190 }
1191 let nombre = story_parts(&page, NOMBRE_INFO);
1192 let (part, text, [_, y0, x1, y1]) = &nombre[0];
1193 assert_eq!((*part, text.as_str()), (16, "p.1-"));
1194 assert!(*x1 == 1535 && close(*y0, -29 + lower) && *y1 == 47 + lower, "{nombre:?}");
1195 let (attached, info): (i64, Vec<u8>) = page
1196 .database()
1197 .query_row(
1198 "SELECT LayerEffectAttached, LayerEffectInfo FROM Layer WHERE ComicStoryInfoType = 2",
1199 [],
1200 |row| Ok((row.get(0)?, row.get(1)?)),
1201 )
1202 .unwrap();
1203 assert_eq!((attached, info.as_slice()), (1, NOMBRE_EDGE));
1204 let mut next = page;
1206 next.database().execute("UPDATE Canvas SET ComicPageIndex = 1, ComicIsLeftPage = 0", []).unwrap();
1207 next.update_story_numbers(false).unwrap();
1208 let next = reopened(next);
1209 let numbers: Vec<String> = story_parts(&next, STORY_INFO)
1210 .into_iter()
1211 .chain(story_parts(&next, NOMBRE_INFO))
1212 .filter(|(part, ..)| *part == 4 || *part == 16)
1213 .map(|(_, text, _)| text)
1214 .collect();
1215 assert_eq!(numbers, ["8ページ", "p.2-"]);
1216 assert!(crate::verify::check_clip(next.to_bytes().unwrap(), Default::default()).ok());
1217 assert!(next.layers.flatten().iter().any(|(layer, _)| layer.kind == LayerKind::StoryInfo));
1218 }
1219
1220 #[test]
1221 fn refuses_settings_csp_cannot_make() {
1222 let check = |change: &dyn Fn(&mut PageSetup)| {
1223 let mut setup = commercial();
1224 change(&mut setup);
1225 setup.check().is_err()
1226 };
1227 assert!(check(&|setup| setup.trim_mm = [300.0, 364.0]));
1228 assert!(check(&|setup| setup.resolution = 2400.0));
1229 assert!(check(&|setup| setup.basic_offset_mm = [60.0, 0.0]));
1230 assert!(check(&|setup| setup.canvas_mm = [2000.0, 2000.0]));
1231 assert!(commercial().check().is_ok());
1232 }
1233}