Skip to main content

csp_format/
create.rs

1//! New .clip files without a template (R13).
2//!
3//! The database gets the tables CSP creates for a raster-only canvas
4//! (`data/new_file_schema.sql`) and CSP's own schema registry
5//! (`ParamScheme`, `ElemScheme`, `ExternalTableAndColumnName`, from
6//! `data/`), all taken from a file CSP wrote; they hold structure only.
7//! The rows are written here: the project and canvas settings CSP uses for
8//! a new illustration, the root folder, the paper and one raster layer
9//! (built by [`ClipFile::add_layer`]), and a preview rendered from them.
10
11use rusqlite::Connection;
12use rusqlite::types::Value;
13
14use crate::clip::ClipFile;
15use crate::container::ContainerWriter;
16use crate::database;
17use crate::edit::{NewLayer, new_uuid};
18use crate::error::Result;
19use crate::image::RgbaImage;
20
21const SCHEMA: &str = include_str!("data/new_file_schema.sql");
22const PARAM_SCHEME: &str = include_str!("data/param_scheme.tsv");
23const ELEM_SCHEME: &str = include_str!("data/elem_scheme.tsv");
24
25/// Tables whose BLOB columns live in external chunks.
26const EXTERNAL_COLUMNS: [(&str, &str); 10] = [
27    ("Offscreen", "BlockData"),
28    ("VectorObjectList", "VectorData"),
29    ("Canvas3DModelBank", "BankData"),
30    ("Canvas3DModelLoader", "ModelData"),
31    ("Track", "TrackActionMixer"),
32    ("Track", "TrackActionMixer2"),
33    ("CanvasItemBinary", "ItemBinaryData"),
34    ("Manager3DOd", "SceneData"),
35    ("ModelData3D", "Layer3DModelData"),
36    ("TimeLapseBlob", "BlobData"),
37];
38
39/// Settings of a new canvas (pixels; the unit is set to pixels).
40#[derive(Debug, Clone, PartialEq)]
41pub struct NewCanvas {
42    /// Width in pixels.
43    pub width: u32,
44    /// Height in pixels.
45    pub height: u32,
46    /// Dots per inch.
47    pub resolution: f64,
48    /// Paper colour; `None` for no paper layer.
49    pub paper: Option<[u8; 3]>,
50    /// Name of the first raster layer.
51    pub layer_name: String,
52}
53
54impl Default for NewCanvas {
55    fn default() -> Self {
56        NewCanvas {
57            width: 1024,
58            height: 1024,
59            resolution: 350.0,
60            paper: Some([255; 3]),
61            layer_name: "レイヤー 1".to_owned(),
62        }
63    }
64}
65
66/// Ids CSP gives the singletons of a new file.
67const CANVAS_ID: i64 = 1;
68const ITEM_BANK_ID: i64 = 1;
69const CUT_BANK_ID: i64 = 2;
70const LAYER_COMP_MANAGER_ID: i64 = 3;
71const ROOT_ID: i64 = 2;
72
73/// Size of the stored preview: the canvas halved until it fits in 2048
74/// pixels (as in CSP's files).
75pub fn preview_size(width: u32, height: u32) -> (u32, u32) {
76    let (mut w, mut h) = (width.max(1), height.max(1));
77    while w.max(h) > 2048 {
78        (w, h) = ((w / 2).max(1), (h / 2).max(1));
79    }
80    (w, h)
81}
82
83impl ClipFile {
84    /// A new file: root folder, paper (unless `paper` is `None`) and one
85    /// empty raster layer, ready for [`ClipFile::set_layer_pixels`] and
86    /// [`ClipFile::add_raster_layer`]. Call [`ClipFile::update_preview`]
87    /// after drawing.
88    pub fn create(canvas: &NewCanvas) -> Result<ClipFile> {
89        let connection = Connection::open_in_memory()?;
90        connection.execute_batch(SCHEMA)?;
91        fill_registry(&connection, PARAM_SCHEME, ELEM_SCHEME)?;
92        for (table, column) in EXTERNAL_COLUMNS {
93            connection.execute("INSERT INTO ExternalTableAndColumnName VALUES (?1, ?2)", (table, column))?;
94        }
95        fill_singletons(&connection, canvas)?;
96        let bytes = ContainerWriter::new(new_uuid()).assemble(&database::serialize(&connection)?)?;
97        // The database holds a bare root folder so that it opens; it is
98        // replaced by a complete one now that layers can be built.
99        let mut file = ClipFile::from_bytes(bytes)?;
100        let root = ROOT_ID;
101        file.database().execute("DELETE FROM Layer WHERE MainId = ?1", [root])?;
102        file.insert_layer_row(root, NewLayer::Root, "")?;
103        file.reload()?;
104        if let Some(color) = canvas.paper {
105            file.add_layer(root, NewLayer::Paper(color), "用紙")?;
106        }
107        file.add_layer(root, NewLayer::Raster, &canvas.layer_name)?;
108        file.update_preview()?;
109        Ok(file)
110    }
111}
112
113/// Fill `ParamScheme` and `ElemScheme` from the tab-separated copies in `data/`.
114pub(crate) fn fill_registry(connection: &Connection, param_scheme: &str, elem_scheme: &str) -> Result<()> {
115    let mut insert = connection.prepare(
116        "INSERT INTO ParamScheme (TableName, LabelName, DataType, Flag, OwnerType, LockType, LockSpecified, \
117         AlternativeLockType, AlternativeLockSpecified, LinkTable) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
118    )?;
119    for line in param_scheme.lines().filter(|line| !line.is_empty()) {
120        let cells: Vec<Value> = line
121            .split('\t')
122            .enumerate()
123            .map(|(i, cell)| match (i, cell) {
124                (_, "\\N") => Value::Null,
125                (0 | 1 | 9, text) => Value::Text(text.to_owned()),
126                (_, number) => number.parse().map(Value::Integer).unwrap_or(Value::Null),
127            })
128            .collect();
129        insert.execute(rusqlite::params_from_iter(cells))?;
130    }
131    for line in elem_scheme.lines().filter(|line| !line.is_empty()) {
132        let (table, kind) = line.split_once('\t').unwrap_or((line, "0"));
133        connection.execute(
134            "INSERT INTO ElemScheme (TableName, ElemType, MaxIndex) VALUES (?1, ?2, 0)",
135            (table, kind.parse::<i64>().unwrap_or(0)),
136        )?;
137    }
138    Ok(())
139}
140
141/// The comic settings CSP writes for a new illustration (Project and Canvas
142/// share them).
143pub(crate) fn comic_story() -> Vec<(&'static str, Value)> {
144    let (int, text, real) = (Value::Integer, |s: &str| Value::Text(s.to_owned()), Value::Real);
145    vec![
146        ("ComicStoryStoryName", text("")),
147        ("ComicStoryUseStoryIndex", int(0)),
148        ("ComicStoryStoryIndex", int(1)),
149        ("ComicStorySubTitle", text("")),
150        ("ComicStoryStoryPosition", int(1)),
151        ("ComicStoryAuthorName", text("")),
152        ("ComicStoryAuthorPosition", int(2)),
153        ("ComicStoryUsePageNumber", int(0)),
154        ("ComicStoryPageNumberStart", int(1)),
155        ("ComicStoryPageNumberPosition", int(3)),
156        ("ComicStoryNombreStart", int(1)),
157        ("ComicStoryNombreFont", text("")),
158        ("ComicStoryNombreFontSize", real(9.0)),
159        ("ComicStoryNombreFontPostScriptName", text("")),
160        ("ComicStoryUseShownNombre", int(0)),
161        ("ComicStoryNombrePosition", int(5)),
162        ("ComicStoryNombrePrefix", text("")),
163        ("ComicStoryNombreSuffix", text("")),
164        ("ComicStoryUseHiddenNombre", int(0)),
165        ("ComicStoryNombreColor", int(0)),
166        ("ComicStoryNombreUseEdge", int(0)),
167        ("ComicStoryNombreEdgeWidth", real(0.5)),
168        ("ComicStoryNombreEdgeUnit", int(2)),
169        ("ComicStoryYPageNombreOffset", real(5.0)),
170        ("ComicStoryYPageNombreOffsetUnit", int(2)),
171        ("ComicStoryXPageNombreOffset", real(0.0)),
172        ("ComicStoryXPageNombreOffsetUnit", int(2)),
173    ]
174}
175
176pub(crate) fn insert(connection: &Connection, table: &str, values: &[(&str, Value)]) -> Result<()> {
177    let names: Vec<String> = values.iter().map(|(name, _)| database::quote(name)).collect();
178    let marks: Vec<String> = (1..=values.len()).map(|i| format!("?{i}")).collect();
179    let sql = format!("INSERT INTO {} ({}) VALUES ({})", database::quote(table), names.join(", "), marks.join(", "));
180    connection.execute(&sql, rusqlite::params_from_iter(values.iter().map(|(_, value)| value.clone())))?;
181    Ok(())
182}
183
184fn fill_singletons(connection: &Connection, canvas: &NewCanvas) -> Result<()> {
185    let (int, text, real) = (Value::Integer, |s: &str| Value::Text(s.to_owned()), Value::Real);
186    let (width, height) = (canvas.width.max(1) as f64, canvas.height.max(1) as f64);
187    let paper = canvas.paper.unwrap_or([255; 3]).map(|c| int(c as i64 * 0x0101_0101));
188    let mut project = vec![
189        ("ProjectInternalVersion", text("1.1.0")),
190        ("ProjectName", text("")),
191        ("ProjectCanvas", int(CANVAS_ID)),
192        ("ProjectItemBank", int(ITEM_BANK_ID)),
193        ("ProjectCutBank", int(CUT_BANK_ID)),
194        ("ProjectLayerCompManager", int(LAYER_COMP_MANAGER_ID)),
195        ("ProjectRootCanvasNode", int(0)),
196    ];
197    project.extend(comic_story());
198    project.extend([
199        ("DefaultPageUnit", int(0)),
200        ("DefaultPageWidth", real(width)),
201        ("DefaultPageHeight", real(height)),
202        ("DefaultPageResolution", real(canvas.resolution)),
203        ("DefaultPageChannelBytes", int(1)),
204        ("DefaultPageChannelOrder", int(33)),
205        ("DefaultPageColorType", int(0)),
206        ("DefaultPageBlackChecked", int(1)),
207        ("DefaultPageWhiteChecked", int(1)),
208        ("DefaultPageToneLine", real(60.0)),
209        ("DefaultPageUsePaper", int(canvas.paper.is_some() as i64)),
210        ("DefaultPagePaperRed", paper[0].clone()),
211        ("DefaultPagePaperGreen", paper[1].clone()),
212        ("DefaultPagePaperBlue", paper[2].clone()),
213        ("DefaultPageUseTemplate", int(0)),
214        ("DefaultPageTemplatePath", text("")),
215        ("DefaultPageTemplatePath2", text("")),
216        ("DefaultPageTemplateName", text("")),
217        ("DefaultPageIsUserTemplate", int(0)),
218        ("DefaultPageDoublePage", int(0)),
219        ("DefaultPagePresetID", text("")),
220        ("DefaultPageCheckBookBinding", int(0)),
221        ("DefaultPageCelUseTemplate", int(0)),
222        ("DefaultPageCelTemplatePath", text("")),
223        ("DefaultPageCelTemplatePath2", text("")),
224        ("DefaultPageCelTemplateName", text("")),
225        ("DefaultPageCelTemplateUUID", text("")),
226        ("DefaultPageCelIsUserTemplate", int(0)),
227        ("DefaultPageUseCropFrame", int(0)),
228        ("DefaultPageTemplateUUID", text("")),
229        ("DefaultPageUseDefaultCoverInfo", int(0)),
230        ("DefaultPageSettingType", text("Illust")),
231        ("DefaultPageRecordTimeLapse", int(0)),
232        ("DefaultPageTimeLapseCodec", int(0)),
233    ]);
234    insert(connection, "Project", &project)?;
235    let mut canvas_row = vec![
236        ("MainId", int(CANVAS_ID)),
237        ("CanvasUnit", int(0)),
238        ("CanvasWidth", real(width)),
239        ("CanvasHeight", real(height)),
240        ("CanvasResolution", real(canvas.resolution)),
241        ("CanvasChannelBytes", int(1)),
242        ("CanvasDefaultChannelOrder", int(33)),
243        ("CanvasRootFolder", int(ROOT_ID)),
244        ("CanvasCurrentLayer", int(0)),
245        ("CanvasDoSimulateColor", int(0)),
246        ("CanvasSimulateRenderingIntent", int(1)),
247        ("CanvasSimulateUseLibraryType", int(2)),
248        ("CanvasUseColorAdjustment", int(0)),
249        ("CanvasDefaultColorTypeIndex", int(0)),
250        ("CanvasDefaultColorBlackChecked", int(1)),
251        ("CanvasDefaultColorWhiteChecked", int(1)),
252        ("CanvasDefaultToneLine", real(60.0)),
253        ("CanvasDoublePage", int(0)),
254        ("CanvasRenderMipmapForceSaved", int(0)),
255        ("CanvasWorkTime", int(0)),
256        ("ComicBindPosition", int(1)),
257        ("ComicHasComicCover", int(0)),
258        ("ComicHasDoubleCover", int(0)),
259        ("ComicPageIndex", int(0)),
260        ("ComicIsLeftPage", int(1)),
261    ];
262    canvas_row.extend(comic_story());
263    canvas_row.push(("TimeLapseCodec", int(0)));
264    insert(connection, "Canvas", &canvas_row)?;
265    insert(
266        connection,
267        "CanvasItemBank",
268        &[("MainId", int(ITEM_BANK_ID)), ("ModelBankMainIndex", int(0)), ("BankRootItemMainIndex", int(0))],
269    )?;
270    insert(
271        connection,
272        "AnimationCutBank",
273        &[
274            ("MainId", int(CUT_BANK_ID)),
275            ("FirstTimeLine", int(0)),
276            ("FirstScenario", int(0)),
277            ("CurrentIndex", int(0)),
278            ("Enable", int(0)),
279        ],
280    )?;
281    insert(
282        connection,
283        "LayerCompManager",
284        &[
285            ("MainId", int(LAYER_COMP_MANAGER_ID)),
286            ("FirstLayerCompIndex", int(0)),
287            ("LastStateLayerCompIndex", int(0)),
288            ("AppliedLayerCompIndex", int(-1)),
289        ],
290    )?;
291    let (preview_width, preview_height) = preview_size(canvas.width, canvas.height);
292    let preview = RgbaImage::new(preview_width, preview_height).to_png()?;
293    insert(
294        connection,
295        "CanvasPreview",
296        &[
297            ("MainId", int(1)),
298            ("CanvasId", int(CANVAS_ID)),
299            ("ImageType", int(1)),
300            ("ImageWidth", int(preview_width as i64)),
301            ("ImageHeight", int(preview_height as i64)),
302            ("ImageData", Value::Blob(preview)),
303        ],
304    )?;
305    insert(
306        connection,
307        "Layer",
308        &[
309            ("MainId", int(ROOT_ID)),
310            ("CanvasId", int(CANVAS_ID)),
311            ("LayerType", int(crate::model::LAYER_TYPE_ROOT)),
312            ("LayerFolder", int(1)),
313            ("LayerFirstChildIndex", int(0)),
314            ("LayerNextIndex", int(0)),
315        ],
316    )?;
317    for (table, max) in [("Canvas", CANVAS_ID), ("Project", 1), ("Layer", ROOT_ID)] {
318        connection.execute("UPDATE ElemScheme SET MaxIndex = ?1 WHERE TableName = ?2", (max, table))?;
319    }
320    Ok(())
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::model::LayerKind;
327    use crate::verify::{CheckOptions, check_clip};
328
329    #[test]
330    fn new_files_pass_the_self_check_and_read_back() {
331        let canvas = NewCanvas { width: 300, height: 200, ..Default::default() };
332        let mut file = ClipFile::create(&canvas).unwrap();
333        let mut image = RgbaImage::new(40, 30);
334        image.fill([200, 10, 20, 255]);
335        let raster = file.layers.flatten().iter().find(|(l, _)| l.kind == LayerKind::Raster).unwrap().0.main_id;
336        file.set_layer_pixels(raster, &image, (10, 20)).unwrap();
337        file.update_preview().unwrap();
338        let bytes = file.to_bytes().unwrap();
339        let report = check_clip(bytes.clone(), CheckOptions::default());
340        assert!(report.ok(), "{:?}", serde_json::to_string(&report).unwrap());
341        let again = ClipFile::from_bytes(bytes).unwrap();
342        assert_eq!((again.canvas.width_px, again.canvas.height_px), (300, 200));
343        let kinds: Vec<LayerKind> = again.layers.flatten().iter().map(|(l, _)| l.kind).collect();
344        assert_eq!(kinds, [LayerKind::Root, LayerKind::Paper, LayerKind::Raster]);
345        let (pixels, origin) = again.layer_pixels(again.layers.get(raster).unwrap()).unwrap().unwrap();
346        assert_eq!(pixels.pixel(15 - origin.0 as u32, 25 - origin.1 as u32), [200, 10, 20, 255]);
347        let preview = again.preview().unwrap().unwrap();
348        assert_eq!(preview.pixel(15, 25), [200, 10, 20, 255]);
349        assert_eq!(preview.pixel(100, 100), [255, 255, 255, 255]);
350        // New thumbnails are blank (CSP redraws them).
351        let thumbnail = again.layer_thumbnail(again.layers.get(raster).unwrap()).unwrap().unwrap();
352        assert_eq!((thumbnail.width, thumbnail.height, thumbnail.pixel(5, 5)[3]), (512, 512, 0));
353    }
354
355    #[test]
356    fn empty_raster_layers_store_empty_blocks_like_csp() {
357        // CSP reports a raster layer whose base offscreen has no block data as
358        // damaged; its own empty layers store every block as an empty record
359        // and leave the smaller levels without (V8).
360        let file = ClipFile::create(&NewCanvas { width: 600, height: 400, ..Default::default() }).unwrap();
361        let file = ClipFile::from_bytes(file.to_bytes().unwrap()).unwrap();
362        for (layer, _) in file.layers.flatten() {
363            let mipmap = file.mipmap(layer.render_mipmap).unwrap().unwrap();
364            for (level, info) in mipmap.levels.iter().enumerate() {
365                let offscreen = file.offscreen(info.offscreen).unwrap().unwrap();
366                let stored = layer.kind == LayerKind::Raster && level == 0;
367                assert_eq!(offscreen.data.is_some(), stored, "{:?} level {level}", layer.kind);
368                if let Some(data) = &offscreen.data {
369                    assert_eq!(data.blocks.len(), 6);
370                    assert!(data.record_sizes().iter().all(|&size| size == crate::offscreen::EMPTY_RECORD_SIZE));
371                    assert_eq!(offscreen.attribute.block_sizes().unwrap(), &data.record_sizes()[..]);
372                }
373            }
374        }
375    }
376
377    #[test]
378    fn layer_thumbnails_are_read_through_their_offscreen() {
379        let mut file = ClipFile::create(&NewCanvas { width: 64, height: 48, ..Default::default() }).unwrap();
380        let mut image = RgbaImage::new(8, 6);
381        image.fill([0, 90, 200, 255]);
382        let raster = file.layers.flatten().iter().find(|(l, _)| l.kind == LayerKind::Raster).unwrap().0.main_id;
383        file.set_layer_pixels(raster, &image, (0, 0)).unwrap();
384        // Point the layer's thumbnail at an offscreen that has pixels.
385        let layer = file.layers.get(raster).unwrap().clone();
386        let base = file.mipmap(layer.render_mipmap).unwrap().unwrap().levels[0].offscreen;
387        file.database()
388            .execute(
389                "UPDATE LayerThumbnail SET ThumbnailOffscreen = ?1 WHERE MainId = ?2",
390                [base, layer.row.int("LayerRenderThumbnail")],
391            )
392            .unwrap();
393        let thumbnail = file.layer_thumbnail(&layer).unwrap().unwrap();
394        assert_eq!(thumbnail.pixel(1, 1), [0, 90, 200, 255]);
395    }
396}