use rusqlite::types::Value;
use rusqlite::{Connection, OptionalExtension};
use crate::database::{has_column, quote};
use crate::error::{Error, Result};
use crate::text::{Attributes, Record, ids, join_array, split_array};
pub const STORY_INFO: i64 = 1;
pub const NOMBRE_INFO: i64 = 2;
pub const FOR_RIGHT_PAGE: &str = "ComicStoryInfoForRightPage";
pub const LEFT_PAGE_FLAG: i64 = 2;
pub const RIGHT_PAGE_FLAG: i64 = 4;
pub const ATTR_STORY_PART: u32 = 0x2e;
const ATTR_RUNS: u32 = ids::RUN_FONT_SIZE;
pub const ATTR_ORIGIN: u32 = 0x48;
pub const ATTR_COLOR: u32 = 0x22;
pub const ATTR_FONT_LABEL: u32 = 0x1f;
const TEXT_BLOBS: [&str; 1] = ["TextLayerAttributes"];
const TEXT_ARRAYS: [&str; 2] = ["TextLayerAttributesArray", "TextLayerAddAttributesV01"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoryPart {
Title,
Author,
PageNumber,
Nombre,
}
impl StoryPart {
pub fn code(self) -> i64 {
match self {
StoryPart::Title => 1,
StoryPart::Author => 2,
StoryPart::PageNumber => 4,
StoryPart::Nombre => 16,
}
}
pub fn of(attributes: &Attributes) -> Option<StoryPart> {
match attributes.get(ATTR_STORY_PART).map(i32s).as_deref() {
Some([1]) => Some(StoryPart::Title),
Some([2]) => Some(StoryPart::Author),
Some([4]) => Some(StoryPart::PageNumber),
Some([16] | [32]) => Some(StoryPart::Nombre),
_ => None,
}
}
fn by_place(info: i64, column: &str, block: Option<usize>) -> StoryPart {
if info != STORY_INFO {
return StoryPart::Nombre;
}
match (column, block) {
("TextLayerAttributesArray", Some(0)) | ("TextLayerAddAttributesV01", Some(1)) => StoryPart::Author,
("TextLayerAttributesArray", Some(_)) | ("TextLayerAddAttributesV01", Some(_)) => StoryPart::PageNumber,
_ => StoryPart::Title,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Align {
Start,
Middle,
End,
}
impl Align {
pub fn of_position(position: i64) -> Align {
match position {
2 => Align::Middle,
3 => Align::End,
_ => Align::Start,
}
}
}
pub fn i32s(value: &[u8]) -> Vec<i64> {
value.as_chunks::<4>().0.iter().map(|bytes| i32::from_le_bytes(*bytes) as i64).collect()
}
pub fn i32_bytes(values: &[i64]) -> Vec<u8> {
values.iter().flat_map(|value| (*value as i32).to_le_bytes()).collect()
}
pub fn round_half_even(value: f64) -> i64 {
value.round_ties_even() as i64
}
pub fn map_records(attributes: Attributes, change: impl Fn(u32, Vec<u8>) -> Vec<u8>) -> Attributes {
Attributes {
records: attributes
.records
.into_iter()
.map(|Record { id, data }| Record { id, data: change(id, data) })
.collect(),
}
}
pub fn canvas_value(db: &Connection, column: &str) -> Result<Option<Value>> {
if !has_column(db, "Canvas", column)? {
return Ok(None);
}
Ok(db.query_row(&format!("SELECT {} FROM Canvas", quote(column)), [], |row| row.get(0)).optional()?)
}
fn canvas_int(db: &Connection, column: &str) -> Result<Option<i64>> {
Ok(match canvas_value(db, column)? {
Some(Value::Integer(number)) => Some(number),
Some(Value::Real(number)) => Some(number as i64),
_ => None,
})
}
fn canvas_text(db: &Connection, column: &str) -> Result<String> {
Ok(match canvas_value(db, column)? {
Some(Value::Text(text)) => text,
Some(Value::Blob(bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
_ => String::new(),
})
}
pub fn resolution(db: &Connection) -> Result<f64> {
match canvas_value(db, "CanvasResolution")? {
Some(Value::Real(number)) => Ok(number),
Some(Value::Integer(number)) => Ok(number as f64),
_ => Err(Error::format("Canvas has no resolution")),
}
}
pub fn is_spread(db: &Connection) -> Result<bool> {
Ok(matches!(canvas_value(db, "CanvasDoublePage")?, Some(Value::Integer(flag)) if flag != 0))
}
pub fn canvas_width_px(db: &Connection) -> Result<i64> {
let row = crate::database::select(db, "SELECT CanvasWidth, CanvasResolution, CanvasUnit FROM Canvas", [])?;
let row = row.first().ok_or_else(|| Error::format("No Canvas row"))?;
let unit = crate::model::Unit::from_code(row.int("CanvasUnit"));
Ok(round_half_even(unit.to_pixels(row.real("CanvasWidth"), row.real("CanvasResolution"))))
}
fn layer_blob(db: &Connection, layer_id: i64, column: &str) -> Result<Option<Vec<u8>>> {
let value: Value =
db.query_row(&format!("SELECT {} FROM Layer WHERE MainId = ?1", quote(column)), [layer_id], |row| row.get(0))?;
Ok(match value {
Value::Blob(blob) => Some(blob),
_ => None,
})
}
fn set_layer_column(db: &Connection, layer_id: i64, column: &str, value: impl rusqlite::ToSql) -> Result<()> {
db.execute(&format!("UPDATE Layer SET {} = ?1 WHERE MainId = ?2", quote(column)), (value, layer_id))?;
Ok(())
}
fn layer_offset_x(db: &Connection, layer_id: i64) -> Result<i64> {
let offset: Option<i64> =
db.query_row("SELECT LayerOffsetX FROM Layer WHERE MainId = ?1", [layer_id], |row| row.get(0))?;
Ok(offset.unwrap_or(0))
}
pub fn story_layers(db: &Connection, info: i64) -> Result<Vec<(i64, bool)>> {
if !has_column(db, "Layer", "ComicStoryInfoType")? {
return Ok(Vec::new());
}
let side = if has_column(db, "Layer", FOR_RIGHT_PAGE)? {
format!("IFNULL({}, 0)", quote(FOR_RIGHT_PAGE))
} else {
"0".to_owned()
};
let mut statement =
db.prepare(&format!("SELECT MainId, {side} FROM Layer WHERE ComicStoryInfoType = ?1 ORDER BY MainId"))?;
let ids = statement
.query_map([info], |row| Ok((row.get(0)?, row.get::<_, i64>(1)? != 0)))?
.collect::<std::result::Result<_, _>>()?;
Ok(ids)
}
fn nombre_layer(db: &Connection) -> Result<Option<i64>> {
let ids = story_layers(db, NOMBRE_INFO)?;
Ok(if ids.len() == 1 { Some(ids[0].0) } else { None })
}
pub fn edit_story_blocks(
db: &Connection,
layer_id: i64,
info: i64,
edit: &dyn Fn(StoryPart, bool, Attributes) -> Result<Attributes>,
) -> Result<()> {
for column in TEXT_BLOBS.iter().chain(TEXT_ARRAYS.iter()) {
if !has_column(db, "Layer", column)? {
continue;
}
let Some(blob) = layer_blob(db, layer_id, column)? else { continue };
let counted = *column != "TextLayerAddAttributesV01";
let one = |bytes: &[u8], block: Option<usize>| -> Result<Vec<u8>> {
let attributes = Attributes::parse(bytes)?;
let part = StoryPart::of(&attributes).unwrap_or_else(|| StoryPart::by_place(info, column, block));
Ok(edit(part, counted, attributes)?.to_bytes())
};
let value = if TEXT_BLOBS.contains(column) {
one(&blob, None)?
} else {
let blocks = split_array(&blob)?
.into_iter()
.enumerate()
.map(|(index, block)| one(block, Some(index)))
.collect::<Result<Vec<_>>>()?;
join_array(&blocks)
};
set_layer_column(db, layer_id, column, value)?;
}
Ok(())
}
pub fn edit_text_blocks(db: &Connection, layer_id: i64, edit: &dyn Fn(Attributes) -> Attributes) -> Result<()> {
let one = |blob: &[u8]| -> Result<Vec<u8>> { Ok(edit(Attributes::parse(blob)?).to_bytes()) };
for column in TEXT_BLOBS.iter().chain(TEXT_ARRAYS.iter()) {
if !has_column(db, "Layer", column)? {
continue;
}
let Some(blob) = layer_blob(db, layer_id, column)? else { continue };
if blob.is_empty() {
continue;
}
let value = if TEXT_BLOBS.contains(column) {
one(&blob)?
} else {
join_array(&split_array(&blob)?.into_iter().map(one).collect::<Result<Vec<_>>>()?)
};
set_layer_column(db, layer_id, column, value)?;
}
Ok(())
}
pub fn drop_render_cache(db: &Connection, layer_id: i64) -> Result<()> {
edit_text_blocks(db, layer_id, &|attributes| {
map_records(
attributes,
|id, data| if id == ids::RENDER_OFFSCREEN && data.len() == 4 { vec![0; 4] } else { data },
)
})
}
pub fn em_px(attributes: &Attributes, resolution: f64) -> f64 {
let points = attributes
.get(ids::FONT_SIZE)
.filter(|value| value.len() == 4)
.map(|value| i32::from_le_bytes(value.try_into().unwrap()) as f64 / 100.0)
.unwrap_or(6.0);
points * resolution / 72.0
}
pub fn text_width(text: &str, em: f64) -> i64 {
let ems: f64 = text
.chars()
.map(|character| match character {
' ' | '.' | ',' | '-' | ':' => 0.5,
character if character.is_ascii() => 0.62,
_ => 1.0,
})
.sum();
round_half_even(ems * em).max(1)
}
pub fn set_run_length(attributes: Attributes, units: usize, counted: bool) -> Attributes {
map_records(attributes, |id, mut data| {
if id == ATTR_RUNS && counted && data.len() >= 12 && data[0..4] == 1u32.to_le_bytes() {
data[8..12].copy_from_slice(&(units as u32).to_le_bytes());
}
data
})
}
pub fn resize_block(attributes: Attributes, width: i64, align: Align) -> Attributes {
map_records(attributes, |id, data| match id {
ids::BBOX if data.len() == 16 => {
let [x0, y0, x1, y1] = i32s(&data)[..] else { unreachable!() };
let x0 = match align {
Align::Start => x0,
Align::Middle => (x0 + x1 - width).div_euclid(2),
Align::End => x1 - width,
};
i32_bytes(&[x0, y0, x0 + width, y1])
}
ids::BLOCK_SIZE if data.len() == 8 => i32_bytes(&[width, i32s(&data)[1]]),
ids::QUAD if data.len() == 32 => {
let mut quad = i32s(&data);
for index in [2, 4] {
quad[index] = quad[0] + width * 100;
}
i32_bytes(&quad)
}
_ => data,
})
}
pub fn write_part(db: &Connection, layer_id: i64, info: i64, part: StoryPart, text: &str, align: Align) -> Result<()> {
let resolution = resolution(db)?;
let units = text.encode_utf16().count();
edit_story_blocks(db, layer_id, info, &|found, counted, attributes| {
if found != part {
return Ok(attributes);
}
let width = text_width(text, em_px(&attributes, resolution));
Ok(resize_block(set_run_length(attributes, units, counted), width, align))
})
}
pub fn string_array(db: &Connection, layer_id: i64) -> Result<Option<Vec<String>>> {
if !has_column(db, "Layer", "TextLayerStringArray")? {
return Ok(None);
}
let Some(blob) = layer_blob(db, layer_id, "TextLayerStringArray")? else { return Ok(None) };
Ok(Some(split_array(&blob)?.into_iter().map(|bytes| String::from_utf8_lossy(bytes).into_owned()).collect()))
}
pub fn set_string_array(db: &Connection, layer_id: i64, strings: &[String]) -> Result<()> {
set_layer_column(db, layer_id, "TextLayerStringArray", join_array(strings))
}
pub fn array_parts(db: &Connection, layer_id: i64) -> Result<Vec<StoryPart>> {
if !has_column(db, "Layer", "TextLayerAttributesArray")? {
return Ok(Vec::new());
}
let Some(blob) = layer_blob(db, layer_id, "TextLayerAttributesArray")? else { return Ok(Vec::new()) };
split_array(&blob)?
.into_iter()
.enumerate()
.map(|(index, block)| {
let attributes = Attributes::parse(block)?;
Ok(StoryPart::of(&attributes)
.unwrap_or_else(|| StoryPart::by_place(STORY_INFO, "TextLayerAttributesArray", Some(index))))
})
.collect()
}
pub fn page_number_text(number: i64) -> String {
format!("{number}ページ")
}
pub fn update_page_number(db: &Connection, first_right: bool) -> Result<()> {
if canvas_int(db, "ComicStoryUsePageNumber")? != Some(1) {
return Ok(());
}
let first =
canvas_int(db, "ComicStoryPageNumberStart")?.unwrap_or(1) + canvas_int(db, "ComicPageIndex")?.unwrap_or(0);
let align = Align::of_position(canvas_int(db, "ComicStoryPageNumberPosition")?.unwrap_or(3));
let spread = is_spread(db)?;
for (layer_id, right) in story_layers(db, STORY_INFO)? {
let number = if spread && right != first_right { first + 1 } else { first };
let text = page_number_text(number);
let Some(mut strings) = string_array(db, layer_id)? else { continue };
let Some(index) = array_parts(db, layer_id)?.iter().position(|part| *part == StoryPart::PageNumber) else {
continue;
};
if strings.get(index).is_none_or(|old| *old == text) {
continue;
}
strings[index] = text.clone();
set_string_array(db, layer_id, &strings)?;
write_part(db, layer_id, STORY_INFO, StoryPart::PageNumber, &text, align)?;
drop_render_cache(db, layer_id)?;
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NombreStyle {
pub prefix: String,
pub suffix: String,
pub centred: bool,
}
pub fn nombre_style(db: &Connection) -> Result<Option<NombreStyle>> {
if canvas_int(db, "ComicStoryUseShownNombre")? != Some(1) {
return Ok(None);
}
let position = canvas_int(db, "ComicStoryNombrePosition")?.unwrap_or(5);
Ok(Some(NombreStyle {
prefix: canvas_text(db, "ComicStoryNombrePrefix")?,
suffix: canvas_text(db, "ComicStoryNombreSuffix")?,
centred: matches!(position, 2 | 5),
}))
}
pub fn map_bbox_x(attributes: Attributes, change: &dyn Fn(i64, i64) -> (i64, i64)) -> Attributes {
map_records(attributes, |id, data| {
if id != ids::BBOX || data.len() != 16 {
return data;
}
let [x0, y0, x1, y1] = i32s(&data)[..] else { unreachable!() };
let (x0, x1) = change(x0, x1);
i32_bytes(&[x0, y0, x1, y1])
})
}
pub fn mirror_nombre(db: &Connection) -> Result<()> {
let Some(layer_id) = nombre_layer(db)? else { return Ok(()) };
let width = canvas_width_px(db)?;
let offset = layer_offset_x(db, layer_id)?;
let mirror = |x0: i64, x1: i64| (width - (x1 + offset) - offset, width - (x0 + offset) - offset);
edit_text_blocks(db, layer_id, &|attributes| map_bbox_x(attributes, &mirror))
}
pub fn move_run_length(blob: &[u8], delta: i64) -> Result<Vec<u8>> {
if delta == 0 {
return Ok(blob.to_vec());
}
Ok(map_records(Attributes::parse(blob)?, |id, mut data| {
if id == ATTR_RUNS && data.len() >= 12 && u32::from_le_bytes(data[0..4].try_into().unwrap()) == 1 {
let length = u32::from_le_bytes(data[8..12].try_into().unwrap()) as i64;
data[8..12].copy_from_slice(&((length + delta).max(0) as u32).to_le_bytes());
}
data
})
.to_bytes())
}
pub fn update_nombre(db: &Connection, first_right: bool) -> Result<()> {
if !has_column(db, "Canvas", "ComicPageIndex")? || !has_column(db, "Canvas", "ComicStoryNombreStart")? {
return Ok(());
}
let first = canvas_int(db, "ComicStoryNombreStart")?.unwrap_or(1) + canvas_int(db, "ComicPageIndex")?.unwrap_or(0);
let style = nombre_style(db)?;
let text = |number: i64| match &style {
Some(style) => format!("{}{number}{}", style.prefix, style.suffix),
None => number.to_string(),
};
let centred = style.as_ref().is_some_and(|style| style.centred);
if is_spread(db)? {
for (layer_id, right) in story_layers(db, NOMBRE_INFO)? {
let number = if right == first_right { first } else { first + 1 };
set_nombre(db, layer_id, &text(number), centred)?;
}
return Ok(());
}
let Some(layer_id) = nombre_layer(db)? else { return Ok(()) };
set_nombre(db, layer_id, &text(first), centred)
}
fn set_nombre(db: &Connection, layer_id: i64, number: &str, centred: bool) -> Result<()> {
let (string, attributes): (Value, Value) =
db.query_row("SELECT TextLayerString, TextLayerAttributes FROM Layer WHERE MainId = ?1", [layer_id], |row| {
Ok((row.get(0)?, row.get(1)?))
})?;
let old = match string {
Value::Blob(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
Value::Text(text) => text,
_ => String::new(),
};
if old.is_empty() || old == number {
return Ok(());
}
let (old_length, new_length) = (old.chars().count() as i64, number.chars().count() as i64);
let width = canvas_width_px(db)?;
let offset = layer_offset_x(db, layer_id)?;
let resize = |attributes: Attributes| -> Attributes {
let Some(block) = attributes.get(ids::BLOCK_SIZE).filter(|value| value.len() == 8).map(i32s) else {
return attributes;
};
let (block_width, height) = (block[0], block[1]);
let new_width = round_half_even(block_width as f64 * new_length as f64 / old_length as f64).max(1);
let grow = new_width - block_width;
let keep_border_edge = |x0: i64, x1: i64| {
if centred {
let x0 = (x0 + x1 - new_width).div_euclid(2);
(x0, x0 + new_width)
} else if x0 + offset < width - (x1 + offset) {
(x0, x1 + grow)
} else {
(x0 - grow, x1)
}
};
map_records(map_bbox_x(attributes, &keep_border_edge), |id, mut data| {
if id == ids::BLOCK_SIZE {
data = i32_bytes(&[new_width, height]);
} else if id == ids::QUAD && data.len() == 32 {
for position in [2, 4] {
let at = position * 4;
let x = i32::from_le_bytes(data[at..at + 4].try_into().unwrap()) as i64 + grow * 100;
data[at..at + 4].copy_from_slice(&(x as i32).to_le_bytes());
}
}
data
})
};
set_layer_column(db, layer_id, "TextLayerString", number.as_bytes())?;
if let Value::Blob(attributes) = attributes {
set_layer_column(db, layer_id, "TextLayerAttributes", move_run_length(&attributes, new_length - old_length)?)?;
}
edit_text_blocks(db, layer_id, &resize)?;
drop_render_cache(db, layer_id)
}
impl crate::ClipFile {
pub fn update_story_numbers(&mut self, first_right: bool) -> Result<()> {
update_page_number(self.database(), first_right)?;
update_nombre(self.database(), first_right)?;
self.reload()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gives_a_text_room_enough_not_to_break() {
assert!(text_width("- 1 -", 75.0) >= 188);
assert_eq!(text_width("作品名", 50.0), 150);
}
#[test]
fn resizes_a_block_keeping_the_edge_asked_for() {
let mut attributes = Attributes::default();
attributes.set_bbox([100, 10, 200, 60]);
attributes.set(ids::BLOCK_SIZE, i32_bytes(&[100, 50]));
let resized = resize_block(attributes.clone(), 40, Align::End);
assert_eq!(resized.bbox(), Some([160, 10, 200, 60]));
assert_eq!(resized.block_size(), Some([40, 50]));
assert_eq!(resize_block(attributes, 40, Align::Middle).bbox(), Some([130, 10, 170, 60]));
}
}