Skip to main content

csp_format/
model.rs

1//! Typed view of the `Project`, `Canvas` and `Layer` tables of a .clip.
2//!
3//! The layer tree is stored as first-child / next-sibling links:
4//! `Canvas.CanvasRootFolder` is the root folder's `Layer.MainId`, and
5//! `LayerFirstChildIndex` / `LayerNextIndex` hold `MainId`s (0 = none).
6//! Siblings are listed bottom to top (the paper first). See
7//! `docs/spec/03-canvas-and-layers.md`.
8
9use std::collections::{HashMap, HashSet};
10
11use rusqlite::Connection;
12
13use crate::database::{Row, select, select_all, table_exists};
14use crate::error::{Result, bail};
15
16/// `LayerOpacity` of a fully opaque layer (100 %).
17pub const OPACITY_FULL: i64 = 256;
18const MAX_DEPTH: usize = 256;
19
20/// `CanvasUnit` / `CropFrameUnitKind` and friends.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum Unit {
25    /// `CanvasUnit` 0.
26    Pixel,
27    /// `CanvasUnit` 1.
28    Centimeter,
29    /// `CanvasUnit` 2.
30    Millimeter,
31    /// `CanvasUnit` 3.
32    Inch,
33    /// `CanvasUnit` 5.
34    Point,
35    /// A code this crate does not know; sizes are taken as pixels.
36    Unknown(i64),
37}
38
39impl Unit {
40    /// The unit of a stored unit code.
41    pub fn from_code(code: i64) -> Self {
42        match code {
43            0 => Unit::Pixel,
44            1 => Unit::Centimeter,
45            2 => Unit::Millimeter,
46            3 => Unit::Inch,
47            5 => Unit::Point,
48            other => Unit::Unknown(other),
49        }
50    }
51
52    /// Pixels of `value` at `dpi`, rounded half to even (as the Python reference).
53    pub fn to_pixels(&self, value: f64, dpi: f64) -> f64 {
54        match self {
55            Unit::Pixel | Unit::Unknown(_) => value,
56            Unit::Centimeter => value * dpi / 2.54,
57            Unit::Millimeter => value * dpi / 25.4,
58            Unit::Inch => value * dpi,
59            Unit::Point => value * dpi / 72.0,
60        }
61    }
62}
63
64/// Settings of the canvas (one row of `Canvas`).
65#[derive(Debug, Clone, serde::Serialize)]
66pub struct Canvas {
67    /// `MainId` of the row.
68    pub main_id: i64,
69    /// Unit of `width` and `height` (`CanvasUnit`).
70    pub unit: Unit,
71    /// Width in `unit` (`CanvasWidth`).
72    pub width: f64,
73    /// Height in `unit` (`CanvasHeight`).
74    pub height: f64,
75    /// Resolution in dots per inch (`CanvasResolution`).
76    pub resolution: f64,
77    /// Size in pixels as CSP computes it from `width`/`height`.
78    pub width_px: u32,
79    /// Height in pixels, as `width_px`.
80    pub height_px: u32,
81    /// `MainId` of the root folder layer (`CanvasRootFolder`).
82    pub root_folder: i64,
83    /// Bytes per colour channel (`CanvasChannelBytes`, 1 in CSP files).
84    pub channel_bytes: i64,
85    /// The canvas is a two-page spread (`CanvasDoublePage`).
86    pub double_page: bool,
87    /// The whole row, for the columns not typed here.
88    #[serde(skip)]
89    pub row: Row,
90}
91
92impl Canvas {
93    /// Types the columns of a `Canvas` row.
94    pub fn from_row(row: Row) -> Self {
95        let unit = Unit::from_code(row.int("CanvasUnit"));
96        let width = row.real("CanvasWidth");
97        let height = row.real("CanvasHeight");
98        let resolution = row.real("CanvasResolution");
99        Canvas {
100            main_id: row.int("MainId"),
101            width_px: unit.to_pixels(width, resolution).round_ties_even().max(0.0) as u32,
102            height_px: unit.to_pixels(height, resolution).round_ties_even().max(0.0) as u32,
103            unit,
104            width,
105            height,
106            resolution,
107            root_folder: row.int("CanvasRootFolder"),
108            channel_bytes: row.int("CanvasChannelBytes"),
109            double_page: row.truthy("CanvasDoublePage"),
110            row,
111        }
112    }
113}
114
115/// What a layer is. Derived from `LayerType`, `LayerFolder` and which
116/// optional columns are filled (no single column holds it).
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
118#[serde(rename_all = "snake_case")]
119#[non_exhaustive]
120pub enum LayerKind {
121    /// The root folder (`LayerType` 256), not shown in CSP.
122    Root,
123    /// 用紙 (`LayerType` 1584).
124    Paper,
125    /// Comic story info / nombre text (`LayerType` 800).
126    StoryInfo,
127    /// A folder (`LayerFolder` bit 0).
128    Folder,
129    /// コマ枠フォルダー: a folder with `ComicFrameLineMipmap`.
130    FrameFolder,
131    /// Pixel layer (`LayerType` bit 0).
132    Raster,
133    /// Vector layer: has a `VectorObjectList` row (or the vector index columns when nothing is drawn).
134    Vector,
135    /// Text layer (`TextLayerString`).
136    Text,
137    /// Gradient or fill layer (`GradationFillInfo`).
138    Gradient,
139    /// Tone-correction layer (`FilterLayerInfo`, `LayerType` bit 12).
140    Correction,
141    /// Image material (`ResizableImageInfo`).
142    ImageMaterial,
143    /// 3D layer (`FirstLayerObject`).
144    ThreeD,
145    /// Something else (e.g. a ruler-only layer).
146    Other,
147}
148
149impl LayerKind {
150    /// Root, folder or frame folder: a layer that holds other layers.
151    pub fn is_folder(self) -> bool {
152        matches!(self, LayerKind::Root | LayerKind::Folder | LayerKind::FrameFolder)
153    }
154
155    /// A short snake_case name (`"raster"`, `"frame_folder"`, …), the same as the serde name.
156    pub fn name(self) -> &'static str {
157        match self {
158            LayerKind::Root => "root",
159            LayerKind::Paper => "paper",
160            LayerKind::StoryInfo => "story_info",
161            LayerKind::Folder => "folder",
162            LayerKind::FrameFolder => "frame_folder",
163            LayerKind::Raster => "raster",
164            LayerKind::Vector => "vector",
165            LayerKind::Text => "text",
166            LayerKind::Gradient => "gradient",
167            LayerKind::Correction => "correction",
168            LayerKind::ImageMaterial => "image_material",
169            LayerKind::ThreeD => "3d",
170            LayerKind::Other => "other",
171        }
172    }
173}
174
175/// `LayerType` bit of a pixel layer.
176pub const LAYER_TYPE_RASTER: i64 = 1;
177/// `LayerType` bit of a layer with a mask.
178pub const LAYER_TYPE_MASKED: i64 = 2;
179/// `LayerType` of the root folder.
180pub const LAYER_TYPE_ROOT: i64 = 256;
181/// `LayerType` of a story info (作品情報) layer.
182pub const LAYER_TYPE_STORY_INFO: i64 = 800;
183/// `LayerType` of the paper (用紙) layer.
184pub const LAYER_TYPE_PAPER: i64 = 1584;
185/// `LayerType` bit of a tone-correction layer.
186pub const LAYER_TYPE_CORRECTION: i64 = 4096;
187
188/// One row of `Layer`, with the commonly used columns typed.
189#[derive(Debug, Clone, serde::Serialize)]
190pub struct Layer {
191    /// `MainId` of the row; other rows refer to the layer by it.
192    pub main_id: i64,
193    /// Name shown in the layer palette (`LayerName`).
194    pub name: String,
195    /// What the layer is (derived, see [`LayerKind`]).
196    pub kind: LayerKind,
197    /// `LayerType` as stored.
198    pub layer_type: i64,
199    /// `LayerVisibility` bit 0.
200    pub visible: bool,
201    /// `LayerVisibility` bit 1: the layer mask is applied.
202    pub mask_enabled: bool,
203    /// 0–256 (`LayerOpacity`, 256 = 100 %).
204    pub opacity: i64,
205    /// `LayerComposite`.
206    pub blend: i64,
207    /// Clipped to the layer below (`LayerClip`).
208    pub clipping: bool,
209    /// `LayerLock` is set.
210    pub locked: bool,
211    /// `LayerFolder` as stored (bit 0 folder, bit 4 open in the palette).
212    pub folder_flags: i64,
213    /// Layer position on the canvas (`LayerOffsetX/Y`); pixel, vector and text positions are relative to it.
214    pub offset: (i64, i64),
215    /// Offset of the render offscreen from `offset` (`LayerRenderOffscrOffsetX/Y`).
216    pub render_offset: (i64, i64),
217    /// Offset of the mask from `offset` (`LayerMaskOffsetX/Y`).
218    pub mask_offset: (i64, i64),
219    /// Offset of the mask offscreen from the mask offset (`LayerMaskOffscrOffsetX/Y`).
220    pub mask_render_offset: (i64, i64),
221    /// `MainId` of the `Mipmap` row of the layer's pixels (`LayerRenderMipmap`, 0 = none).
222    pub render_mipmap: i64,
223    /// `MainId` of the `Mipmap` row of the mask (`LayerLayerMaskMipmap`, 0 = none).
224    pub mask_mipmap: i64,
225    /// `MainId` of the bottom child (`LayerFirstChildIndex`, 0 = none).
226    pub first_child: i64,
227    /// `MainId` of the sibling above (`LayerNextIndex`, 0 = none).
228    pub next: i64,
229    /// `LayerUuid`, when set.
230    pub uuid: Option<String>,
231    /// The whole row, for the columns not typed here.
232    #[serde(skip)]
233    pub row: Row,
234}
235
236impl Layer {
237    /// Types the columns of a `Layer` row. `vector_layers` are the `LayerId`s of `VectorObjectList`, used to tell vector layers.
238    pub fn from_row(row: Row, vector_layers: &HashSet<i64>) -> Self {
239        let main_id = row.int("MainId");
240        let layer_type = row.int("LayerType");
241        let folder_flags = row.int("LayerFolder");
242        let kind = classify(&row, main_id, layer_type, folder_flags, vector_layers);
243        let visibility = row.int("LayerVisibility");
244        let pair = |x: &str, y: &str| (row.int(x), row.int(y));
245        Layer {
246            main_id,
247            name: row.text("LayerName").unwrap_or_default(),
248            kind,
249            layer_type,
250            visible: visibility & 1 != 0,
251            mask_enabled: visibility & 2 != 0,
252            opacity: row.opt_int("LayerOpacity").unwrap_or(OPACITY_FULL),
253            blend: row.int("LayerComposite"),
254            clipping: row.int("LayerClip") != 0,
255            locked: row.int("LayerLock") != 0,
256            folder_flags,
257            offset: pair("LayerOffsetX", "LayerOffsetY"),
258            render_offset: pair("LayerRenderOffscrOffsetX", "LayerRenderOffscrOffsetY"),
259            mask_offset: pair("LayerMaskOffsetX", "LayerMaskOffsetY"),
260            mask_render_offset: pair("LayerMaskOffscrOffsetX", "LayerMaskOffscrOffsetY"),
261            render_mipmap: row.int("LayerRenderMipmap"),
262            mask_mipmap: row.int("LayerLayerMaskMipmap"),
263            first_child: row.int("LayerFirstChildIndex"),
264            next: row.int("LayerNextIndex"),
265            uuid: row.text("LayerUuid"),
266            row,
267        }
268    }
269
270    /// Canvas position of the render offscreen's top-left pixel.
271    pub fn pixel_origin(&self) -> (i64, i64) {
272        (self.offset.0 + self.render_offset.0, self.offset.1 + self.render_offset.1)
273    }
274
275    /// Canvas position of the mask offscreen's top-left pixel. The mask
276    /// offsets are relative to the layer's offset (a moved layer keeps its
277    /// mask in place: offset (7018, 1) with mask offset (−7018, −1) puts a
278    /// canvas-sized mask at (0, 0)).
279    pub fn mask_origin(&self) -> (i64, i64) {
280        (
281            self.offset.0 + self.mask_offset.0 + self.mask_render_offset.0,
282            self.offset.1 + self.mask_offset.1 + self.mask_render_offset.1,
283        )
284    }
285
286    /// The layer has a mask.
287    pub fn has_mask(&self) -> bool {
288        self.mask_mipmap != 0
289    }
290
291    /// Opacity as 0.0–1.0.
292    pub fn opacity_fraction(&self) -> f64 {
293        self.opacity.clamp(0, OPACITY_FULL) as f64 / OPACITY_FULL as f64
294    }
295
296    /// Folder shown expanded in the layer palette (`LayerFolder` bit 4).
297    pub fn is_open(&self) -> bool {
298        self.folder_flags & 16 != 0
299    }
300
301    /// A colour column pair as 8-bit RGB (`DrawColorMainRed` …): CSP stores
302    /// each channel as a 32-bit value whose top byte is the 8-bit value.
303    pub fn color(&self, prefix: &str) -> Option<[u8; 3]> {
304        let channel =
305            |name: &str| self.row.opt_int(&format!("{prefix}{name}")).map(|v| ((v as u64 >> 24) & 0xff) as u8);
306        Some([channel("Red")?, channel("Green")?, channel("Blue")?])
307    }
308}
309
310fn classify(row: &Row, main_id: i64, layer_type: i64, folder: i64, vector_layers: &HashSet<i64>) -> LayerKind {
311    if layer_type == LAYER_TYPE_ROOT {
312        return LayerKind::Root;
313    }
314    if layer_type == LAYER_TYPE_PAPER {
315        return LayerKind::Paper;
316    }
317    if layer_type == LAYER_TYPE_STORY_INFO {
318        return LayerKind::StoryInfo;
319    }
320    if folder & 1 != 0 {
321        return if row.truthy("ComicFrameLineMipmap") { LayerKind::FrameFolder } else { LayerKind::Folder };
322    }
323    if row.is_set("TextLayerString") {
324        return LayerKind::Text;
325    }
326    if row.truthy("GradationFillInfo") {
327        return LayerKind::Gradient;
328    }
329    if row.truthy("FilterLayerInfo") || layer_type & LAYER_TYPE_CORRECTION != 0 {
330        return LayerKind::Correction;
331    }
332    if row.truthy("FirstLayerObject") {
333        return LayerKind::ThreeD;
334    }
335    if row.truthy("ResizableImageInfo") {
336        return LayerKind::ImageMaterial;
337    }
338    if vector_layers.contains(&main_id) {
339        return LayerKind::Vector;
340    }
341    if layer_type & LAYER_TYPE_RASTER != 0 {
342        return LayerKind::Raster;
343    }
344    // A vector layer nothing was drawn on: no `VectorObjectList` row, but
345    // the vector index columns are there (all 101 such layers in the corpus
346    // had `LayerType` 0 and blank pixels).
347    if layer_type == 0 && row.is_set("VectorNormalStrokeIndex") {
348        return LayerKind::Vector;
349    }
350    LayerKind::Other
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use rusqlite::types::Value;
357
358    #[test]
359    fn empty_vector_layers_are_vector() {
360        let row = |columns: &[(&str, Value)]| {
361            Row::new(
362                columns.iter().map(|(c, _)| c.to_string()).collect(),
363                columns.iter().map(|(_, v)| v.clone()).collect(),
364            )
365        };
366        let none = HashSet::new();
367        let empty = row(&[("VectorNormalStrokeIndex", Value::Integer(0))]);
368        assert_eq!(classify(&empty, 5, 0, 0, &none), LayerKind::Vector);
369        let raster = row(&[("VectorNormalStrokeIndex", Value::Null)]);
370        assert_eq!(classify(&raster, 5, 1, 0, &none), LayerKind::Raster);
371        assert_eq!(classify(&raster, 5, 0, 0, &none), LayerKind::Other);
372    }
373}
374
375/// A layer and its children (bottom to top).
376#[derive(Debug, Clone)]
377pub struct LayerNode {
378    /// The layer.
379    pub layer: Layer,
380    /// Its children, bottom to top.
381    pub children: Vec<LayerNode>,
382}
383
384impl LayerNode {
385    /// Depth-first, parents before children, siblings bottom to top.
386    pub fn walk<'a>(&'a self, depth: usize, visit: &mut impl FnMut(&'a LayerNode, usize)) {
387        visit(self, depth);
388        for child in &self.children {
389            child.walk(depth + 1, visit);
390        }
391    }
392
393    /// The node of a layer in this subtree.
394    pub fn find(&self, main_id: i64) -> Option<&LayerNode> {
395        if self.layer.main_id == main_id {
396            return Some(self);
397        }
398        self.children.iter().find_map(|child| child.find(main_id))
399    }
400}
401
402/// All layers and the tree they form.
403#[derive(Debug, Clone)]
404pub struct LayerTree {
405    /// Every row of `Layer`, in table order.
406    pub layers: Vec<Layer>,
407    /// The root folder and everything reachable from it.
408    pub root: LayerNode,
409    /// Layers not reachable from the root (CSP files have none).
410    pub unreachable: Vec<i64>,
411}
412
413impl LayerTree {
414    /// Reads `Layer` and links the tree from the root folder `root_id` (`Canvas.CanvasRootFolder`). Fails on a cycle, a missing link or a tree deeper than 256.
415    pub fn read(connection: &Connection, root_id: i64) -> Result<Self> {
416        let vector_layers: HashSet<i64> = if table_exists(connection, "VectorObjectList")? {
417            select(connection, "SELECT LayerId FROM VectorObjectList", [])?
418                .iter()
419                .map(|row| row.int("LayerId"))
420                .collect()
421        } else {
422            HashSet::new()
423        };
424        let layers: Vec<Layer> =
425            select_all(connection, "Layer")?.into_iter().map(|row| Layer::from_row(row, &vector_layers)).collect();
426        let by_id: HashMap<i64, &Layer> = layers.iter().map(|layer| (layer.main_id, layer)).collect();
427        if by_id.len() != layers.len() {
428            bail!("Layer MainIds are not unique");
429        }
430        let mut seen = HashSet::new();
431        let root = build(&by_id, root_id, &mut seen, 0)?;
432        let unreachable = layers.iter().map(|layer| layer.main_id).filter(|id| !seen.contains(id)).collect();
433        Ok(LayerTree { layers: layers.clone(), root, unreachable })
434    }
435
436    /// The layer with this `MainId`.
437    pub fn get(&self, main_id: i64) -> Option<&Layer> {
438        self.layers.iter().find(|layer| layer.main_id == main_id)
439    }
440
441    /// (layer, depth) in palette order: parents first, siblings bottom to top.
442    pub fn flatten(&self) -> Vec<(&Layer, usize)> {
443        let mut result = Vec::new();
444        self.root.walk(0, &mut |node, depth| result.push((&node.layer, depth)));
445        result
446    }
447}
448
449fn build(by_id: &HashMap<i64, &Layer>, id: i64, seen: &mut HashSet<i64>, depth: usize) -> Result<LayerNode> {
450    if depth > MAX_DEPTH {
451        bail!("Layer tree deeper than {MAX_DEPTH}");
452    }
453    let layer = *by_id.get(&id).ok_or_else(|| crate::Error::format(format!("Layer {id} does not exist")))?;
454    if !seen.insert(id) {
455        bail!("Layer {id} is linked twice (the tree has a cycle)");
456    }
457    let mut children = Vec::new();
458    let mut child = layer.first_child;
459    while child != 0 {
460        let node = build(by_id, child, seen, depth + 1)?;
461        child = node.layer.next;
462        children.push(node);
463    }
464    Ok(LayerNode { layer: layer.clone(), children })
465}
466
467/// One row of `Project` (in a .clip: name and links to the canvas).
468#[derive(Debug, Clone, serde::Serialize)]
469pub struct Project {
470    /// `ProjectInternalVersion`.
471    pub internal_version: Option<String>,
472    /// `ProjectName`.
473    pub name: Option<String>,
474    /// `MainId` of the canvas (`ProjectCanvas`).
475    pub canvas: i64,
476    /// The whole row, for the columns not typed here.
477    #[serde(skip)]
478    pub row: Row,
479}
480
481impl Project {
482    /// Types the columns of a `Project` row.
483    pub fn from_row(row: Row) -> Self {
484        Project {
485            internal_version: row.text("ProjectInternalVersion"),
486            name: row.text("ProjectName"),
487            canvas: row.int("ProjectCanvas"),
488            row,
489        }
490    }
491}