1use std::path::{Path, PathBuf};
20
21use rusqlite::Connection;
22use rusqlite::types::Value;
23
24use crate::clip::ClipFile;
25use crate::cmc::NODE_PAGE;
26use crate::create::{NewCanvas, comic_story, fill_registry, insert};
27use crate::database;
28use crate::error::{Result, bail};
29
30const SCHEMA: &str = include_str!("data/cmc_schema.sql");
31const PARAM_SCHEME: &str = include_str!("data/cmc_param_scheme.tsv");
32const ELEM_SCHEME: &str = include_str!("data/cmc_elem_scheme.tsv");
33
34const ROOT_NODE: i64 = 1;
35const UNIT_MM: i64 = 2;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Binding {
41 Right,
43 Left,
45}
46
47impl Binding {
48 pub fn code(self) -> i64 {
50 match self {
51 Binding::Right => 1,
52 Binding::Left => 0,
53 }
54 }
55
56 pub fn from_code(code: i64) -> Self {
58 if code == 1 { Binding::Right } else { Binding::Left }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum StartSide {
65 Left,
67 Right,
69}
70
71impl StartSide {
72 pub fn code(self) -> i64 {
74 match self {
75 StartSide::Left => 0,
76 StartSide::Right => 1,
77 }
78 }
79
80 pub fn from_code(code: i64) -> Self {
82 if code == 1 { StartSide::Right } else { StartSide::Left }
83 }
84}
85
86pub fn is_left_page(position: usize, start: StartSide) -> bool {
88 position.is_multiple_of(2) != (start == StartSide::Right)
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct PageSlot {
94 pub position: usize,
96 pub spread: bool,
98 pub left: bool,
100}
101
102#[derive(Debug, Clone, PartialEq)]
104pub struct NewWork {
105 pub trim_width: f64,
107 pub trim_height: f64,
109 pub bleed: f64,
111 pub resolution: f64,
113 pub pages: usize,
115 pub spreads: bool,
117 pub binding: Binding,
119 pub start: StartSide,
121 pub title: String,
123 pub author: String,
125}
126
127impl Default for NewWork {
128 fn default() -> Self {
129 NewWork {
130 trim_width: 182.0,
131 trim_height: 257.0,
132 bleed: 5.0,
133 resolution: 600.0,
134 pages: 4,
135 spreads: false,
136 binding: Binding::Right,
137 start: StartSide::Left,
138 title: String::new(),
139 author: String::new(),
140 }
141 }
142}
143
144pub struct WorkFiles {
147 pub cmc: Vec<u8>,
149 pub pages: Vec<(String, Vec<u8>)>,
151}
152
153impl NewWork {
154 pub fn layout(&self) -> Vec<PageSlot> {
156 let mut slots = Vec::new();
157 let mut position = 0;
158 while position < self.pages {
159 let left = is_left_page(position, self.start);
160 let first_side_left = self.binding == Binding::Left;
162 let spread = self.spreads && position + 1 < self.pages && left == first_side_left;
163 slots.push(PageSlot { position, spread, left });
164 position += if spread { 2 } else { 1 };
165 }
166 slots
167 }
168
169 fn canvas_mm(&self, spread: bool) -> (f64, f64) {
171 let width = self.trim_width + 2.0 * self.bleed;
172 (if spread { width * 2.0 } else { width }, self.trim_height + 2.0 * self.bleed)
173 }
174
175 pub fn build(&self) -> Result<WorkFiles> {
177 if self.pages == 0 {
178 bail!("A work needs at least one page");
179 }
180 if !(self.trim_width > 0.0 && self.trim_height > 0.0 && self.bleed >= 0.0 && self.resolution > 0.0) {
181 bail!("Page size, bleed and resolution must be positive");
182 }
183 let slots = self.layout();
184 let pages = slots
185 .iter()
186 .enumerate()
187 .map(|(i, slot)| Ok((format!("page{:04}.clip", i + 1), self.page(slot)?.to_bytes()?)))
188 .collect::<Result<Vec<_>>>()?;
189 Ok(WorkFiles { cmc: self.cmc(&slots)?, pages })
190 }
191
192 pub fn write(&self, folder: &Path, name: &str) -> Result<PathBuf> {
195 let files = self.build()?;
196 let cmc = folder.join(format!("{name}.cmc"));
197 let paths: Vec<PathBuf> =
198 std::iter::once(cmc.clone()).chain(files.pages.iter().map(|(page, _)| folder.join(page))).collect();
199 if let Some(existing) = paths.iter().find(|path| path.exists()) {
200 bail!("{} already exists", existing.display());
201 }
202 std::fs::create_dir_all(folder)?;
203 for (page, bytes) in &files.pages {
204 std::fs::write(folder.join(page), bytes)?;
205 }
206 std::fs::write(&cmc, files.cmc)?;
207 Ok(cmc)
208 }
209
210 fn story(&self) -> Vec<(&'static str, Value)> {
212 let text = |s: &str| Value::Text(s.to_owned());
213 let mut story = comic_story();
214 for (name, value) in story.iter_mut() {
215 *value = match *name {
216 "ComicStoryStoryName" => text(&self.title),
217 "ComicStoryAuthorName" => text(&self.author),
218 "ComicStoryStoryIndex" => Value::Integer(0),
220 "ComicStoryNombreFont" => text("I-OTFアンチックStd B"),
221 "ComicStoryNombreFontPostScriptName" => text("IwaAntStd-Bd"),
222 "ComicStoryNombreEdgeWidth" => Value::Real(0.3),
223 _ => value.clone(),
224 };
225 }
226 story
227 }
228
229 fn cmc(&self, slots: &[PageSlot]) -> Result<Vec<u8>> {
230 let (int, text, real) = (Value::Integer, |s: &str| Value::Text(s.to_owned()), Value::Real);
231 let connection = Connection::open_in_memory()?;
232 connection.execute_batch(SCHEMA)?;
233 fill_registry(&connection, PARAM_SCHEME, ELEM_SCHEME)?;
234 let (width, height) = self.canvas_mm(false);
235 let mut project = vec![
236 ("ProjectInternalVersion", text("1.1.0")),
237 ("ProjectRootCanvasNode", int(ROOT_NODE)),
238 ("ComicBindPosition", int(self.binding.code())),
239 ("ComicStartPage", int(self.start.code())),
240 ("ComicBindAndStartPageIndeterminate", int(0)),
241 ("ComicShowStyle", int(0)),
242 ("ComicWebtoonWrapDirection", int(1)),
243 ("ComicCoverExist", int(0)),
244 ];
245 project.extend(self.story());
246 project.extend([
247 ("DefaultPageUnit", int(UNIT_MM)),
248 ("DefaultPageWidth", real(width)),
249 ("DefaultPageHeight", real(height)),
250 ("DefaultPageResolution", real(self.resolution)),
251 ("DefaultPageChannelBytes", int(1)),
252 ("DefaultPageChannelOrder", int(33)),
253 ("DefaultPageColorType", int(0)),
254 ("DefaultPageBlackChecked", int(1)),
255 ("DefaultPageWhiteChecked", int(1)),
256 ("DefaultPageToneLine", real(60.0)),
257 ("DefaultPageUsePaper", int(1)),
258 ("DefaultPagePaperRed", int(0xFFFF_FFFF)),
259 ("DefaultPagePaperGreen", int(0xFFFF_FFFF)),
260 ("DefaultPagePaperBlue", int(0xFFFF_FFFF)),
261 ("DefaultPageUseTemplate", int(0)),
262 ("DefaultPageTemplatePath", text("")),
263 ("DefaultPageTemplatePath2", text("")),
264 ("DefaultPageTemplateName", text("")),
265 ("DefaultPageIsUserTemplate", int(0)),
266 ("DefaultPageDoublePage", int(self.spreads as i64)),
267 ("DefaultPagePresetID", text("")),
268 ("DefaultPageCheckBookBinding", int(0)),
269 ("DefaultSrcPageColorProfile", Value::Blob(Vec::new())),
270 ("DefaultSimulatePageColorProfile", Value::Blob(Vec::new())),
271 ("DefaultPageCelUseTemplate", int(0)),
272 ("DefaultPageCelTemplatePath", text("")),
273 ("DefaultPageCelTemplatePath2", text("")),
274 ("DefaultPageCelTemplateName", text("")),
275 ("DefaultPageCelTemplateUUID", text("")),
276 ("DefaultPageCelIsUserTemplate", int(0)),
277 ]);
278 project.extend(self.crop_frame("DefaultPage"));
279 project.extend([
280 ("DefaultPageUseCropFrame", int(1)),
281 ("DefaultPageTemplateUUID", text("")),
282 ("DefaultPageUseDefaultCoverInfo", int(0)),
283 ("DefaultPageSettingType", text("Comic")),
284 ("DefaultPageRecordTimeLapse", int(0)),
285 ("DefaultPageTimeLapseCodec", int(0)),
286 ("MaxCanvasFileIndex", int(slots.len() as i64)),
287 ]);
288 insert(&connection, "Project", &project)?;
289 let page_id = |i: usize| ROOT_NODE + 1 + i as i64;
290 let node = |main_id: i64, kind: i64, next: i64, child: i64| {
291 vec![
292 ("MainId", int(main_id)),
293 ("Type", int(kind)),
294 ("NextIndex", int(next)),
295 ("FirstChildIndex", int(child)),
296 ("SelectedIndex", int(child)),
297 ("CanvasIndex", int(0)),
298 ("PageFlag", int(0)),
299 ("Memo", text("")),
300 ("BookBindMemo", text("")),
301 ("BookBindWarning", int(0)),
303 ("BookBindWarningDef", int((kind == NODE_PAGE) as i64)),
304 ]
305 };
306 insert(&connection, "CanvasNode", &node(ROOT_NODE, crate::cmc::NODE_ROOT, 0, page_id(0)))?;
307 for (i, slot) in slots.iter().enumerate() {
308 let next = if i + 1 < slots.len() { page_id(i + 1) } else { 0 };
309 let mut row = node(page_id(i), NODE_PAGE, next, 0);
310 row[6].1 = int(slot.spread as i64);
311 row.push(("LinkPath", text(&format!(".:page{:04}.clip", i + 1))));
312 insert(&connection, "CanvasNode", &row)?;
313 }
314 for (table, max) in [("CanvasNode", page_id(slots.len() - 1)), ("Project", 1)] {
315 connection.execute("UPDATE ElemScheme SET MaxIndex = ?1 WHERE TableName = ?2", (max, table))?;
316 }
317 database::serialize(&connection)
318 }
319
320 fn crop_frame(&self, prefix: &str) -> Vec<(&'static str, Value)> {
323 let real = Value::Real;
324 let page = prefix == "CropFrame";
325 let pick = |page_name: &'static str, cmc_name: &'static str| if page { page_name } else { cmc_name };
326 vec![
327 (pick("CropFrameWidth", "DefaultPageCropWidth"), real(self.trim_width)),
328 (pick("CropFrameHeight", "DefaultPageCropHeight"), real(self.trim_height)),
329 (pick("CropFrameDitch", "DefaultPageCropDitch"), real(self.bleed)),
330 (pick("CropFrameCropOffsetX", "DefaultPageCropOffsetX"), real(0.0)),
331 (pick("CropFrameCropOffsetY", "DefaultPageCropOffsetY"), real(0.0)),
332 (pick("CropFrameInnerWidth", "DefaultPageCropInnerWidth"), real(self.trim_width)),
333 (pick("CropFrameInnerHeight", "DefaultPageCropInnerHeight"), real(self.trim_height)),
334 (pick("CropFrameInnerOffsetX", "DefaultPageCropInnerOffsetX"), real(0.0)),
335 (pick("CropFrameInnerOffsetY", "DefaultPageCropInnerOffsetY"), real(0.0)),
336 (pick("CropFrameSafeMarginLeft", "DefaultPageSafeMarginLeft"), real(0.0)),
337 (pick("CropFrameSafeMarginTop", "DefaultPageSafeMarginTop"), real(0.0)),
338 (pick("CropFrameSafeMarginRight", "DefaultPageSafeMarginRight"), real(0.0)),
339 (pick("CropFrameSafeMarginBottom", "DefaultPageSafeMarginBottom"), real(0.0)),
340 (pick("CropFrameMergeDitch", "DefaultPageCropMergeDitch"), real(0.0)),
341 ]
342 .into_iter()
343 .chain((!page).then_some(("DefaultPageCropOptionFlag", Value::Integer(0))))
344 .collect()
345 }
346
347 fn page(&self, slot: &PageSlot) -> Result<ClipFile> {
348 let (width, height) = self.canvas_mm(slot.spread);
349 let pixels = |mm: f64| (mm * self.resolution / 25.4).round().max(1.0) as u32;
350 let canvas = NewCanvas {
351 width: pixels(width),
352 height: pixels(height),
353 resolution: self.resolution,
354 ..Default::default()
355 };
356 let mut file = ClipFile::create(&canvas)?;
357 let connection = file.database();
358 let added = [
360 ("CropFrameOptionFlag", "INTEGER"),
361 ("CropFrameUnitKind", "INTEGER"),
362 ("CropFrameWidth", "REAL"),
363 ("CropFrameHeight", "REAL"),
364 ("CropFrameDitch", "REAL"),
365 ("CropFrameInnerWidth", "REAL"),
366 ("CropFrameInnerHeight", "REAL"),
367 ("CropFrameInnerOffsetX", "REAL"),
368 ("CropFrameInnerOffsetY", "REAL"),
369 ("CropFrameMergeDitch", "REAL"),
370 ("CropFrameCropOffsetX", "REAL"),
371 ("CropFrameCropOffsetY", "REAL"),
372 ("CropFrameSafeMarginLeft", "REAL"),
373 ("CropFrameSafeMarginTop", "REAL"),
374 ("CropFrameSafeMarginRight", "REAL"),
375 ("CropFrameSafeMarginBottom", "REAL"),
376 ("CropFrameInnerOffsetBasePosition", "INTEGER"),
377 ("CropFrameDesignationType", "INTEGER"),
378 ("CropFrameShow", "INTEGER"),
379 ("SafeFrameShow", "INTEGER"),
380 ];
381 for (column, kind) in added {
382 connection.execute_batch(&format!("ALTER TABLE Canvas ADD COLUMN {column} {kind} DEFAULT NULL"))?;
383 }
384 let option = if slot.spread {
386 3
387 } else if slot.left {
388 2
389 } else {
390 4
391 };
392 let mut values = vec![
393 ("CanvasUnit", Value::Integer(UNIT_MM)),
394 ("CanvasWidth", Value::Real(width)),
395 ("CanvasHeight", Value::Real(height)),
396 ("CanvasDoublePage", Value::Integer(slot.spread as i64)),
397 ("ComicBindPosition", Value::Integer(self.binding.code())),
398 ("ComicPageIndex", Value::Integer(slot.position as i64)),
399 ("ComicIsLeftPage", Value::Integer(slot.left as i64)),
400 ("CropFrameOptionFlag", Value::Integer(option)),
401 ("CropFrameUnitKind", Value::Integer(UNIT_MM)),
402 ("CropFrameInnerOffsetBasePosition", Value::Integer(0)),
403 ("CropFrameDesignationType", Value::Integer(1)),
404 ("CropFrameShow", Value::Integer(1)),
405 ("SafeFrameShow", Value::Integer(0)),
406 ];
407 values.extend(self.crop_frame("CropFrame"));
408 values.extend(self.story());
409 let assignments: Vec<String> =
410 values.iter().enumerate().map(|(i, (name, _))| format!("{} = ?{}", database::quote(name), i + 1)).collect();
411 connection.execute(
412 &format!("UPDATE Canvas SET {}", assignments.join(", ")),
413 rusqlite::params_from_iter(values.into_iter().map(|(_, value)| value)),
414 )?;
415 file.reload()?;
416 Ok(file)
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::cmc::CmcFile;
424
425 fn slots(work: &NewWork) -> Vec<(usize, bool, bool)> {
426 work.layout().iter().map(|slot| (slot.position, slot.spread, slot.left)).collect()
427 }
428
429 #[test]
431 fn layouts_match_csp() {
432 let work = |binding, start, pages, spreads| NewWork { binding, start, pages, spreads, ..Default::default() };
433 use {Binding::*, StartSide as S};
434 assert_eq!(slots(&work(Right, S::Left, 4, true)), [(0, false, true), (1, true, false), (3, false, false)]);
435 assert_eq!(slots(&work(Left, S::Left, 4, true)), [(0, true, true), (2, true, true)]);
436 assert_eq!(slots(&work(Right, S::Right, 4, true)), [(0, true, false), (2, true, false)]);
437 assert_eq!(
438 slots(&work(Right, S::Left, 5, false)),
439 [(0, false, true), (1, false, false), (2, false, true), (3, false, false), (4, false, true)]
440 );
441 }
442
443 #[test]
444 fn new_works_read_back() {
445 let work = NewWork {
446 trim_width: 60.0,
447 trim_height: 80.0,
448 resolution: 150.0,
449 spreads: true,
450 title: "T".into(),
451 ..Default::default()
452 };
453 let files = work.build().unwrap();
454 let cmc = CmcFile::from_bytes(files.cmc).unwrap();
455 assert!(cmc.findings(None).is_empty(), "{:?}", cmc.findings(None));
456 assert_eq!(cmc.positions().unwrap(), work.layout());
457 let pages = cmc.pages().unwrap();
458 assert_eq!(pages.iter().map(|page| page.spread).collect::<Vec<_>>(), [false, true, false]);
459 assert_eq!(cmc.project.bind_position, 1);
460 assert_eq!(files.pages.len(), 3);
461 let spread = ClipFile::from_bytes(files.pages[1].1.clone()).unwrap();
462 assert_eq!((spread.canvas.width_px, spread.canvas.height_px), (827, 531));
464 let row = spread.database().query_row(
465 "SELECT ComicPageIndex, ComicIsLeftPage, CropFrameOptionFlag, ComicStoryStoryName FROM Canvas",
466 [],
467 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, i64>(2)?, row.get::<_, String>(3)?)),
468 );
469 assert_eq!(row.unwrap(), (1, 0, 3, "T".to_owned()));
470 let report = crate::verify::check_clip(files.pages[1].1.clone(), crate::verify::CheckOptions::default());
471 assert!(report.ok(), "{:?}", serde_json::to_string(&report).unwrap());
472 }
473}