1use rusqlite::types::Value;
10use rusqlite::{Connection, OptionalExtension};
11
12use crate::database::{has_column, quote};
13use crate::error::{Error, Result};
14use crate::text::{Attributes, Record, ids, join_array, split_array};
15
16pub const STORY_INFO: i64 = 1;
18pub const NOMBRE_INFO: i64 = 2;
20pub const FOR_RIGHT_PAGE: &str = "ComicStoryInfoForRightPage";
22pub const LEFT_PAGE_FLAG: i64 = 2;
24pub const RIGHT_PAGE_FLAG: i64 = 4;
26
27pub const ATTR_STORY_PART: u32 = 0x2e;
29const ATTR_RUNS: u32 = ids::RUN_FONT_SIZE;
31pub const ATTR_ORIGIN: u32 = 0x48;
33pub const ATTR_COLOR: u32 = 0x22;
35pub const ATTR_FONT_LABEL: u32 = 0x1f;
37
38const TEXT_BLOBS: [&str; 1] = ["TextLayerAttributes"];
39const TEXT_ARRAYS: [&str; 2] = ["TextLayerAttributesArray", "TextLayerAddAttributesV01"];
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum StoryPart {
44 Title,
46 Author,
48 PageNumber,
50 Nombre,
52}
53
54impl StoryPart {
55 pub fn code(self) -> i64 {
57 match self {
58 StoryPart::Title => 1,
59 StoryPart::Author => 2,
60 StoryPart::PageNumber => 4,
61 StoryPart::Nombre => 16,
62 }
63 }
64
65 pub fn of(attributes: &Attributes) -> Option<StoryPart> {
67 match attributes.get(ATTR_STORY_PART).map(i32s).as_deref() {
68 Some([1]) => Some(StoryPart::Title),
69 Some([2]) => Some(StoryPart::Author),
70 Some([4]) => Some(StoryPart::PageNumber),
71 Some([16] | [32]) => Some(StoryPart::Nombre),
72 _ => None,
73 }
74 }
75
76 fn by_place(info: i64, column: &str, block: Option<usize>) -> StoryPart {
78 if info != STORY_INFO {
79 return StoryPart::Nombre;
80 }
81 match (column, block) {
82 ("TextLayerAttributesArray", Some(0)) | ("TextLayerAddAttributesV01", Some(1)) => StoryPart::Author,
83 ("TextLayerAttributesArray", Some(_)) | ("TextLayerAddAttributesV01", Some(_)) => StoryPart::PageNumber,
84 _ => StoryPart::Title,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum Align {
92 Start,
94 Middle,
96 End,
98}
99
100impl Align {
101 pub fn of_position(position: i64) -> Align {
103 match position {
104 2 => Align::Middle,
105 3 => Align::End,
106 _ => Align::Start,
107 }
108 }
109}
110
111pub fn i32s(value: &[u8]) -> Vec<i64> {
113 value.as_chunks::<4>().0.iter().map(|bytes| i32::from_le_bytes(*bytes) as i64).collect()
114}
115
116pub fn i32_bytes(values: &[i64]) -> Vec<u8> {
118 values.iter().flat_map(|value| (*value as i32).to_le_bytes()).collect()
119}
120
121pub fn round_half_even(value: f64) -> i64 {
123 value.round_ties_even() as i64
124}
125
126pub fn map_records(attributes: Attributes, change: impl Fn(u32, Vec<u8>) -> Vec<u8>) -> Attributes {
128 Attributes {
129 records: attributes
130 .records
131 .into_iter()
132 .map(|Record { id, data }| Record { id, data: change(id, data) })
133 .collect(),
134 }
135}
136
137pub fn canvas_value(db: &Connection, column: &str) -> Result<Option<Value>> {
139 if !has_column(db, "Canvas", column)? {
140 return Ok(None);
141 }
142 Ok(db.query_row(&format!("SELECT {} FROM Canvas", quote(column)), [], |row| row.get(0)).optional()?)
143}
144
145fn canvas_int(db: &Connection, column: &str) -> Result<Option<i64>> {
146 Ok(match canvas_value(db, column)? {
147 Some(Value::Integer(number)) => Some(number),
148 Some(Value::Real(number)) => Some(number as i64),
149 _ => None,
150 })
151}
152
153fn canvas_text(db: &Connection, column: &str) -> Result<String> {
154 Ok(match canvas_value(db, column)? {
155 Some(Value::Text(text)) => text,
156 Some(Value::Blob(bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
157 _ => String::new(),
158 })
159}
160
161pub fn resolution(db: &Connection) -> Result<f64> {
163 match canvas_value(db, "CanvasResolution")? {
164 Some(Value::Real(number)) => Ok(number),
165 Some(Value::Integer(number)) => Ok(number as f64),
166 _ => Err(Error::format("Canvas has no resolution")),
167 }
168}
169
170pub fn is_spread(db: &Connection) -> Result<bool> {
172 Ok(matches!(canvas_value(db, "CanvasDoublePage")?, Some(Value::Integer(flag)) if flag != 0))
173}
174
175pub fn canvas_width_px(db: &Connection) -> Result<i64> {
177 let row = crate::database::select(db, "SELECT CanvasWidth, CanvasResolution, CanvasUnit FROM Canvas", [])?;
178 let row = row.first().ok_or_else(|| Error::format("No Canvas row"))?;
179 let unit = crate::model::Unit::from_code(row.int("CanvasUnit"));
180 Ok(round_half_even(unit.to_pixels(row.real("CanvasWidth"), row.real("CanvasResolution"))))
181}
182
183fn layer_blob(db: &Connection, layer_id: i64, column: &str) -> Result<Option<Vec<u8>>> {
184 let value: Value =
185 db.query_row(&format!("SELECT {} FROM Layer WHERE MainId = ?1", quote(column)), [layer_id], |row| row.get(0))?;
186 Ok(match value {
187 Value::Blob(blob) => Some(blob),
188 _ => None,
189 })
190}
191
192fn set_layer_column(db: &Connection, layer_id: i64, column: &str, value: impl rusqlite::ToSql) -> Result<()> {
193 db.execute(&format!("UPDATE Layer SET {} = ?1 WHERE MainId = ?2", quote(column)), (value, layer_id))?;
194 Ok(())
195}
196
197fn layer_offset_x(db: &Connection, layer_id: i64) -> Result<i64> {
198 let offset: Option<i64> =
199 db.query_row("SELECT LayerOffsetX FROM Layer WHERE MainId = ?1", [layer_id], |row| row.get(0))?;
200 Ok(offset.unwrap_or(0))
201}
202
203pub fn story_layers(db: &Connection, info: i64) -> Result<Vec<(i64, bool)>> {
205 if !has_column(db, "Layer", "ComicStoryInfoType")? {
206 return Ok(Vec::new());
207 }
208 let side = if has_column(db, "Layer", FOR_RIGHT_PAGE)? {
209 format!("IFNULL({}, 0)", quote(FOR_RIGHT_PAGE))
210 } else {
211 "0".to_owned()
212 };
213 let mut statement =
214 db.prepare(&format!("SELECT MainId, {side} FROM Layer WHERE ComicStoryInfoType = ?1 ORDER BY MainId"))?;
215 let ids = statement
216 .query_map([info], |row| Ok((row.get(0)?, row.get::<_, i64>(1)? != 0)))?
217 .collect::<std::result::Result<_, _>>()?;
218 Ok(ids)
219}
220
221fn nombre_layer(db: &Connection) -> Result<Option<i64>> {
224 let ids = story_layers(db, NOMBRE_INFO)?;
225 Ok(if ids.len() == 1 { Some(ids[0].0) } else { None })
226}
227
228pub fn edit_story_blocks(
231 db: &Connection,
232 layer_id: i64,
233 info: i64,
234 edit: &dyn Fn(StoryPart, bool, Attributes) -> Result<Attributes>,
235) -> Result<()> {
236 for column in TEXT_BLOBS.iter().chain(TEXT_ARRAYS.iter()) {
237 if !has_column(db, "Layer", column)? {
238 continue;
239 }
240 let Some(blob) = layer_blob(db, layer_id, column)? else { continue };
241 let counted = *column != "TextLayerAddAttributesV01";
243 let one = |bytes: &[u8], block: Option<usize>| -> Result<Vec<u8>> {
244 let attributes = Attributes::parse(bytes)?;
245 let part = StoryPart::of(&attributes).unwrap_or_else(|| StoryPart::by_place(info, column, block));
246 Ok(edit(part, counted, attributes)?.to_bytes())
247 };
248 let value = if TEXT_BLOBS.contains(column) {
249 one(&blob, None)?
250 } else {
251 let blocks = split_array(&blob)?
252 .into_iter()
253 .enumerate()
254 .map(|(index, block)| one(block, Some(index)))
255 .collect::<Result<Vec<_>>>()?;
256 join_array(&blocks)
257 };
258 set_layer_column(db, layer_id, column, value)?;
259 }
260 Ok(())
261}
262
263pub fn edit_text_blocks(db: &Connection, layer_id: i64, edit: &dyn Fn(Attributes) -> Attributes) -> Result<()> {
266 let one = |blob: &[u8]| -> Result<Vec<u8>> { Ok(edit(Attributes::parse(blob)?).to_bytes()) };
267 for column in TEXT_BLOBS.iter().chain(TEXT_ARRAYS.iter()) {
268 if !has_column(db, "Layer", column)? {
269 continue;
270 }
271 let Some(blob) = layer_blob(db, layer_id, column)? else { continue };
272 if blob.is_empty() {
273 continue;
274 }
275 let value = if TEXT_BLOBS.contains(column) {
276 one(&blob)?
277 } else {
278 join_array(&split_array(&blob)?.into_iter().map(one).collect::<Result<Vec<_>>>()?)
279 };
280 set_layer_column(db, layer_id, column, value)?;
281 }
282 Ok(())
283}
284
285pub fn drop_render_cache(db: &Connection, layer_id: i64) -> Result<()> {
288 edit_text_blocks(db, layer_id, &|attributes| {
289 map_records(
290 attributes,
291 |id, data| if id == ids::RENDER_OFFSCREEN && data.len() == 4 { vec![0; 4] } else { data },
292 )
293 })
294}
295
296pub fn em_px(attributes: &Attributes, resolution: f64) -> f64 {
298 let points = attributes
299 .get(ids::FONT_SIZE)
300 .filter(|value| value.len() == 4)
301 .map(|value| i32::from_le_bytes(value.try_into().unwrap()) as f64 / 100.0)
302 .unwrap_or(6.0);
303 points * resolution / 72.0
304}
305
306pub fn text_width(text: &str, em: f64) -> i64 {
312 let ems: f64 = text
313 .chars()
314 .map(|character| match character {
315 ' ' | '.' | ',' | '-' | ':' => 0.5,
316 character if character.is_ascii() => 0.62,
317 _ => 1.0,
318 })
319 .sum();
320 round_half_even(ems * em).max(1)
321}
322
323pub fn set_run_length(attributes: Attributes, units: usize, counted: bool) -> Attributes {
325 map_records(attributes, |id, mut data| {
326 if id == ATTR_RUNS && counted && data.len() >= 12 && data[0..4] == 1u32.to_le_bytes() {
327 data[8..12].copy_from_slice(&(units as u32).to_le_bytes());
328 }
329 data
330 })
331}
332
333pub fn resize_block(attributes: Attributes, width: i64, align: Align) -> Attributes {
336 map_records(attributes, |id, data| match id {
337 ids::BBOX if data.len() == 16 => {
338 let [x0, y0, x1, y1] = i32s(&data)[..] else { unreachable!() };
339 let x0 = match align {
340 Align::Start => x0,
341 Align::Middle => (x0 + x1 - width).div_euclid(2),
342 Align::End => x1 - width,
343 };
344 i32_bytes(&[x0, y0, x0 + width, y1])
345 }
346 ids::BLOCK_SIZE if data.len() == 8 => i32_bytes(&[width, i32s(&data)[1]]),
347 ids::QUAD if data.len() == 32 => {
348 let mut quad = i32s(&data);
349 for index in [2, 4] {
350 quad[index] = quad[0] + width * 100;
351 }
352 i32_bytes(&quad)
353 }
354 _ => data,
355 })
356}
357
358pub fn write_part(db: &Connection, layer_id: i64, info: i64, part: StoryPart, text: &str, align: Align) -> Result<()> {
360 let resolution = resolution(db)?;
361 let units = text.encode_utf16().count();
362 edit_story_blocks(db, layer_id, info, &|found, counted, attributes| {
363 if found != part {
364 return Ok(attributes);
365 }
366 let width = text_width(text, em_px(&attributes, resolution));
367 Ok(resize_block(set_run_length(attributes, units, counted), width, align))
368 })
369}
370
371pub fn string_array(db: &Connection, layer_id: i64) -> Result<Option<Vec<String>>> {
373 if !has_column(db, "Layer", "TextLayerStringArray")? {
374 return Ok(None);
375 }
376 let Some(blob) = layer_blob(db, layer_id, "TextLayerStringArray")? else { return Ok(None) };
377 Ok(Some(split_array(&blob)?.into_iter().map(|bytes| String::from_utf8_lossy(bytes).into_owned()).collect()))
378}
379
380pub fn set_string_array(db: &Connection, layer_id: i64, strings: &[String]) -> Result<()> {
382 set_layer_column(db, layer_id, "TextLayerStringArray", join_array(strings))
383}
384
385pub fn array_parts(db: &Connection, layer_id: i64) -> Result<Vec<StoryPart>> {
387 if !has_column(db, "Layer", "TextLayerAttributesArray")? {
388 return Ok(Vec::new());
389 }
390 let Some(blob) = layer_blob(db, layer_id, "TextLayerAttributesArray")? else { return Ok(Vec::new()) };
391 split_array(&blob)?
392 .into_iter()
393 .enumerate()
394 .map(|(index, block)| {
395 let attributes = Attributes::parse(block)?;
396 Ok(StoryPart::of(&attributes)
397 .unwrap_or_else(|| StoryPart::by_place(STORY_INFO, "TextLayerAttributesArray", Some(index))))
398 })
399 .collect()
400}
401
402pub fn page_number_text(number: i64) -> String {
404 format!("{number}ページ")
405}
406
407pub fn update_page_number(db: &Connection, first_right: bool) -> Result<()> {
412 if canvas_int(db, "ComicStoryUsePageNumber")? != Some(1) {
413 return Ok(());
414 }
415 let first =
416 canvas_int(db, "ComicStoryPageNumberStart")?.unwrap_or(1) + canvas_int(db, "ComicPageIndex")?.unwrap_or(0);
417 let align = Align::of_position(canvas_int(db, "ComicStoryPageNumberPosition")?.unwrap_or(3));
418 let spread = is_spread(db)?;
419 for (layer_id, right) in story_layers(db, STORY_INFO)? {
420 let number = if spread && right != first_right { first + 1 } else { first };
421 let text = page_number_text(number);
422 let Some(mut strings) = string_array(db, layer_id)? else { continue };
423 let Some(index) = array_parts(db, layer_id)?.iter().position(|part| *part == StoryPart::PageNumber) else {
424 continue;
425 };
426 if strings.get(index).is_none_or(|old| *old == text) {
427 continue;
428 }
429 strings[index] = text.clone();
430 set_string_array(db, layer_id, &strings)?;
431 write_part(db, layer_id, STORY_INFO, StoryPart::PageNumber, &text, align)?;
432 drop_render_cache(db, layer_id)?;
433 }
434 Ok(())
435}
436
437#[derive(Debug, Clone, PartialEq, Eq)]
440pub struct NombreStyle {
441 pub prefix: String,
443 pub suffix: String,
445 pub centred: bool,
447}
448
449pub fn nombre_style(db: &Connection) -> Result<Option<NombreStyle>> {
451 if canvas_int(db, "ComicStoryUseShownNombre")? != Some(1) {
452 return Ok(None);
453 }
454 let position = canvas_int(db, "ComicStoryNombrePosition")?.unwrap_or(5);
455 Ok(Some(NombreStyle {
456 prefix: canvas_text(db, "ComicStoryNombrePrefix")?,
457 suffix: canvas_text(db, "ComicStoryNombreSuffix")?,
458 centred: matches!(position, 2 | 5),
459 }))
460}
461
462pub fn map_bbox_x(attributes: Attributes, change: &dyn Fn(i64, i64) -> (i64, i64)) -> Attributes {
464 map_records(attributes, |id, data| {
465 if id != ids::BBOX || data.len() != 16 {
466 return data;
467 }
468 let [x0, y0, x1, y1] = i32s(&data)[..] else { unreachable!() };
469 let (x0, x1) = change(x0, x1);
470 i32_bytes(&[x0, y0, x1, y1])
471 })
472}
473
474pub fn mirror_nombre(db: &Connection) -> Result<()> {
481 let Some(layer_id) = nombre_layer(db)? else { return Ok(()) };
482 let width = canvas_width_px(db)?;
483 let offset = layer_offset_x(db, layer_id)?;
484 let mirror = |x0: i64, x1: i64| (width - (x1 + offset) - offset, width - (x0 + offset) - offset);
485 edit_text_blocks(db, layer_id, &|attributes| map_bbox_x(attributes, &mirror))
486}
487
488pub fn move_run_length(blob: &[u8], delta: i64) -> Result<Vec<u8>> {
490 if delta == 0 {
491 return Ok(blob.to_vec());
492 }
493 Ok(map_records(Attributes::parse(blob)?, |id, mut data| {
494 if id == ATTR_RUNS && data.len() >= 12 && u32::from_le_bytes(data[0..4].try_into().unwrap()) == 1 {
495 let length = u32::from_le_bytes(data[8..12].try_into().unwrap()) as i64;
496 data[8..12].copy_from_slice(&((length + delta).max(0) as u32).to_le_bytes());
497 }
498 data
499 })
500 .to_bytes())
501}
502
503pub fn update_nombre(db: &Connection, first_right: bool) -> Result<()> {
512 if !has_column(db, "Canvas", "ComicPageIndex")? || !has_column(db, "Canvas", "ComicStoryNombreStart")? {
513 return Ok(());
514 }
515 let first = canvas_int(db, "ComicStoryNombreStart")?.unwrap_or(1) + canvas_int(db, "ComicPageIndex")?.unwrap_or(0);
516 let style = nombre_style(db)?;
517 let text = |number: i64| match &style {
518 Some(style) => format!("{}{number}{}", style.prefix, style.suffix),
519 None => number.to_string(),
520 };
521 let centred = style.as_ref().is_some_and(|style| style.centred);
522 if is_spread(db)? {
523 for (layer_id, right) in story_layers(db, NOMBRE_INFO)? {
524 let number = if right == first_right { first } else { first + 1 };
525 set_nombre(db, layer_id, &text(number), centred)?;
526 }
527 return Ok(());
528 }
529 let Some(layer_id) = nombre_layer(db)? else { return Ok(()) };
530 set_nombre(db, layer_id, &text(first), centred)
531}
532
533fn set_nombre(db: &Connection, layer_id: i64, number: &str, centred: bool) -> Result<()> {
535 let (string, attributes): (Value, Value) =
536 db.query_row("SELECT TextLayerString, TextLayerAttributes FROM Layer WHERE MainId = ?1", [layer_id], |row| {
537 Ok((row.get(0)?, row.get(1)?))
538 })?;
539 let old = match string {
540 Value::Blob(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
541 Value::Text(text) => text,
542 _ => String::new(),
543 };
544 if old.is_empty() || old == number {
545 return Ok(());
546 }
547 let (old_length, new_length) = (old.chars().count() as i64, number.chars().count() as i64);
548 let width = canvas_width_px(db)?;
549 let offset = layer_offset_x(db, layer_id)?;
550 let resize = |attributes: Attributes| -> Attributes {
551 let Some(block) = attributes.get(ids::BLOCK_SIZE).filter(|value| value.len() == 8).map(i32s) else {
552 return attributes;
553 };
554 let (block_width, height) = (block[0], block[1]);
555 let new_width = round_half_even(block_width as f64 * new_length as f64 / old_length as f64).max(1);
556 let grow = new_width - block_width;
557 let keep_border_edge = |x0: i64, x1: i64| {
558 if centred {
559 let x0 = (x0 + x1 - new_width).div_euclid(2);
560 (x0, x0 + new_width)
561 } else if x0 + offset < width - (x1 + offset) {
562 (x0, x1 + grow)
563 } else {
564 (x0 - grow, x1)
565 }
566 };
567 map_records(map_bbox_x(attributes, &keep_border_edge), |id, mut data| {
568 if id == ids::BLOCK_SIZE {
569 data = i32_bytes(&[new_width, height]);
570 } else if id == ids::QUAD && data.len() == 32 {
571 for position in [2, 4] {
573 let at = position * 4;
574 let x = i32::from_le_bytes(data[at..at + 4].try_into().unwrap()) as i64 + grow * 100;
575 data[at..at + 4].copy_from_slice(&(x as i32).to_le_bytes());
576 }
577 }
578 data
579 })
580 };
581 set_layer_column(db, layer_id, "TextLayerString", number.as_bytes())?;
582 if let Value::Blob(attributes) = attributes {
583 set_layer_column(db, layer_id, "TextLayerAttributes", move_run_length(&attributes, new_length - old_length)?)?;
584 }
585 edit_text_blocks(db, layer_id, &resize)?;
586 drop_render_cache(db, layer_id)
587}
588
589impl crate::ClipFile {
590 pub fn update_story_numbers(&mut self, first_right: bool) -> Result<()> {
594 update_page_number(self.database(), first_right)?;
595 update_nombre(self.database(), first_right)?;
596 self.reload()
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn gives_a_text_room_enough_not_to_break() {
606 assert!(text_width("- 1 -", 75.0) >= 188);
608 assert_eq!(text_width("作品名", 50.0), 150);
610 }
611
612 #[test]
613 fn resizes_a_block_keeping_the_edge_asked_for() {
614 let mut attributes = Attributes::default();
615 attributes.set_bbox([100, 10, 200, 60]);
616 attributes.set(ids::BLOCK_SIZE, i32_bytes(&[100, 50]));
617 let resized = resize_block(attributes.clone(), 40, Align::End);
618 assert_eq!(resized.bbox(), Some([160, 10, 200, 60]));
619 assert_eq!(resized.block_size(), Some([40, 50]));
620 assert_eq!(resize_block(attributes, 40, Align::Middle).bbox(), Some([130, 10, 170, 60]));
621 }
622}