Skip to main content

csp_format/
copy.rs

1//! Copying layers from another .clip (from name-studio's clip-core).
2//!
3//! A layer is copied with the rows its row links to, following the id links
4//! `ParamScheme` lists (`LinkTable`, for example Layer → VectorObjectList),
5//! as CSP itself links them. Every copied row gets a new `MainId` (see
6//! [`ClipFile::next_main_id`]), columns owned by the canvas or the layer
7//! (`OwnerType` 2 and 4) get the target's ids, and external chunks (vector
8//! data) are copied under new ids.
9//!
10//! The target file may lack the tables and columns the copied rows use (CSP
11//! creates `VectorObjectList` only when a file first has a vector layer):
12//! [`merge_schema`] and [`merge_columns`] add them with their `ParamScheme`
13//! and `ElemScheme` rows. Vector objects name their brush by
14//! `BrushStyle.MainId`, so brush styles are copied keeping their ids
15//! ([`share_rows`]) when the target has none, together with the rows brush
16//! styles link to (tip and texture images, spray shapes, effector graphs):
17//! CSP crashes on opening a file whose brush style names a texture that is
18//! not there.
19
20use std::collections::{HashMap, HashSet};
21
22use rusqlite::types::Value;
23use rusqlite::{Connection, OptionalExtension, params_from_iter};
24
25use crate::clip::ClipFile;
26use crate::database::{Row, has_column, has_table, quote, select, value_as_text};
27use crate::edit::new_external_id;
28use crate::error::{Error, Result};
29use crate::model::{LAYER_TYPE_MASKED, LayerKind};
30use crate::offscreen::{Attribute, Parameter, PixelKind};
31
32/// `ParamScheme.OwnerType` of a column holding the canvas id.
33pub const OWNER_CANVAS: i64 = 2;
34/// `ParamScheme.OwnerType` of a column holding the owning layer's id.
35pub const OWNER_LAYER: i64 = 4;
36/// Self-links (linked lists) that `ParamScheme` does not list as links.
37const CHAIN_COLUMNS: [(&str, &str); 2] = [("MipmapInfo", "NextIndex"), ("BrushStyle", "NextIndex")];
38/// Tables (and Canvas columns) a vector or text layer copied from another
39/// file needs: vector lines find their brush styles through
40/// `Canvas.BrushStyleManager`, and without it CSP opens them empty.
41/// `Canvas.BrushStyleReadProtect070` (1 in newer files) comes with the brush
42/// styles: it seems to tell CSP how to read them (not needed by any file
43/// tried so far, but the styles were saved with it).
44pub const LAYER_TABLES: [&str; 9] = [
45    "Layer",
46    "VectorObjectList",
47    "BrushStyleManager",
48    "BrushStyle",
49    "BrushEffectorGraphData",
50    "BrushPatternImage",
51    "BrushPatternStyle",
52    "BrushFixedSpray",
53    "FillStyle",
54];
55/// The `Canvas` columns copied with the tables of [`LAYER_TABLES`].
56pub const LAYER_CANVAS_COLUMNS: [&str; 2] = ["BrushStyleManager", "BrushStyleReadProtect070"];
57/// Rows vector objects refer to by id, copied keeping their ids: the brush
58/// styles and every table `BrushStyleManager` and `BrushStyle` link to.
59pub const SHARED_TABLES: [&str; 7] = [
60    "BrushStyleManager",
61    "BrushStyle",
62    "BrushEffectorGraphData",
63    "BrushPatternImage",
64    "BrushPatternStyle",
65    "BrushFixedSpray",
66    "FillStyle",
67];
68/// Layer columns that would tie a copy to data that is not copied: masks,
69/// frame images, 3D, rulers and the like. A copy gets NULL in them, as CSP's
70/// own layers without these have: CSP crashes drawing a vector layer whose
71/// `ComicFrameLineMipmap` is 0 rather than NULL.
72pub const DROPPED_LAYER_LINKS: [&str; 13] = [
73    "LayerLayerMaskMipmap",
74    "LayerLayerMaskThumbnail",
75    "ResizableOriginalMipmap",
76    "ComicFrameLineMipmap",
77    "FirstLayerObject",
78    "Manager3D",
79    "Manager3DOd",
80    "SpecialRulerManager",
81    "StreamLineIndex",
82    "VoiceSpeakParam",
83    "RulerVectorIndex",
84    "TimeLineOriginalMipmap",
85    "TimeLineOriginalMaskMipmap",
86];
87/// Values after the size and block grid in a layer mask's `Parameter`
88/// section (one 8-bit plane), as CSP writes them for a frame folder.
89const MASK_PARAMETERS: [u32; 12] = [1, 1, 0, 1, 0, 0, 0, 1, 256, 0, 256, 256];
90
91/// Rewrites an external payload while it is copied: (table, column, payload).
92pub type PayloadHook<'a> = dyn Fn(&str, &str, Vec<u8>) -> Result<Vec<u8>> + 'a;
93/// Rewrites the copied Layer row before it is inserted.
94pub type RowHook<'a> = dyn FnMut(&mut Row) -> Result<()> + 'a;
95
96/// What `ParamScheme` says about one column.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ColumnInfo {
99    /// Table the column refers to (`ParamScheme` link), if any.
100    pub link_table: Option<String>,
101    /// `ParamScheme.OwnerType` of the column.
102    pub owner_type: i64,
103}
104
105fn table_columns(database: &Connection, table: &str) -> Result<Vec<String>> {
106    Ok(crate::database::columns(database, table)?.into_iter().map(|(name, _)| name).collect())
107}
108
109/// `ParamScheme` information for the columns of `table`.
110pub fn column_infos(database: &Connection, table: &str) -> Result<HashMap<String, ColumnInfo>> {
111    let mut infos = HashMap::new();
112    if has_table(database, "ParamScheme")? {
113        for row in
114            select(database, "SELECT LabelName, LinkTable, OwnerType FROM ParamScheme WHERE TableName = ?1", [table])?
115        {
116            let label = row.text("LabelName").unwrap_or_default();
117            let mut link_table = row.text("LinkTable").filter(|link| !link.is_empty() && link != "0");
118            if CHAIN_COLUMNS.contains(&(table, label.as_str())) {
119                link_table = Some(table.to_owned());
120            }
121            infos.insert(label, ColumnInfo { link_table, owner_type: row.int("OwnerType") });
122        }
123    }
124    for (chain_table, label) in CHAIN_COLUMNS {
125        if chain_table == table {
126            infos.entry(label.to_owned()).or_insert(ColumnInfo { link_table: Some(table.to_owned()), owner_type: 0 });
127        }
128    }
129    Ok(infos)
130}
131
132/// (table, column) pairs whose values are external chunk ids.
133pub fn external_columns(database: &Connection) -> Result<HashSet<(String, String)>> {
134    if !has_table(database, "ExternalTableAndColumnName")? {
135        return Ok(HashSet::new());
136    }
137    Ok(select(database, "SELECT TableName, ColumnName FROM ExternalTableAndColumnName", [])?
138        .iter()
139        .map(|row| (row.text("TableName").unwrap_or_default(), row.text("ColumnName").unwrap_or_default()))
140        .collect())
141}
142
143/// Insert `row` into `table`, keeping only the columns the table has.
144pub fn insert_row(database: &Connection, table: &str, row: &Row) -> Result<()> {
145    let known: HashSet<String> = table_columns(database, table)?.into_iter().collect();
146    let kept: Vec<(&str, &Value)> = row.iter().filter(|(column, _)| known.contains(*column)).collect();
147    let names: Vec<String> = kept.iter().map(|(column, _)| quote(column)).collect();
148    let marks: Vec<String> = (1..=kept.len()).map(|i| format!("?{i}")).collect();
149    database.execute(
150        &format!("INSERT INTO {} ({}) VALUES ({})", quote(table), names.join(", "), marks.join(", ")),
151        params_from_iter(kept.iter().map(|(_, value)| *value)),
152    )?;
153    Ok(())
154}
155
156/// A row with `column` set, added at the end when the row lacks it.
157fn with_value(row: Row, column: &str, value: Value) -> Row {
158    let mut row = row;
159    if !row.set(column, value.clone()) {
160        let mut columns = row.columns().to_vec();
161        let mut values = row.values().to_vec();
162        columns.push(column.to_owned());
163        values.push(value);
164        row = Row::new(columns, values);
165    }
166    row
167}
168
169/// The next `MainId` of `table` (see [`crate::schema::allocate_main_id`]).
170fn allocate(database: &Connection, table: &str) -> Result<i64> {
171    crate::schema::allocate_main_id(database, table)
172}
173
174/// Copies rows, and everything they link to, from one file's database to
175/// another's.
176pub struct RowCopier<'a> {
177    source: &'a ClipFile,
178    payload_hook: Option<&'a PayloadHook<'a>>,
179    /// The target's `Canvas.MainId`.
180    canvas_id: i64,
181    /// The layer id written into layer-owned columns.
182    pub owner_layer: i64,
183    /// (table, column) values copied unchanged (links not followed).
184    pub skip: HashSet<(String, String)>,
185    done: HashMap<(String, i64), i64>,
186    externals: HashSet<(String, String)>,
187    infos: HashMap<String, HashMap<String, ColumnInfo>>,
188    /// External chunks copied so far, to be stored in the target file.
189    pub new_externals: Vec<(String, Vec<u8>)>,
190}
191
192impl<'a> RowCopier<'a> {
193    /// A copier reading from `source`. `payload_hook` rewrites external payloads as they are copied; `canvas_id` is the target's `Canvas.MainId`.
194    pub fn new(source: &'a ClipFile, payload_hook: Option<&'a PayloadHook<'a>>, canvas_id: i64) -> Result<Self> {
195        Ok(RowCopier {
196            source,
197            payload_hook,
198            canvas_id,
199            owner_layer: 0,
200            skip: HashSet::new(),
201            done: HashMap::new(),
202            externals: external_columns(source.database())?,
203            infos: HashMap::new(),
204            new_externals: Vec::new(),
205        })
206    }
207
208    /// Copy row `main_id` of `table` into `target`; return its id there.
209    pub fn copy(&mut self, target: &Connection, table: &str, main_id: i64) -> Result<i64> {
210        let key = (table.to_owned(), main_id);
211        if let Some(id) = self.done.get(&key) {
212            return Ok(*id);
213        }
214        let row = self
215            .source
216            .row(table, main_id)?
217            .ok_or_else(|| Error::format(format!("{table} {main_id} not found in the source")))?;
218        let new_id = allocate(target, table)?;
219        self.done.insert(key, new_id);
220        let row = self.translate(target, table, row, new_id)?;
221        insert_row(target, table, &row)?;
222        Ok(new_id)
223    }
224
225    /// Rewrite the id, owner columns, links and external ids of one row.
226    pub fn translate(&mut self, target: &Connection, table: &str, row: Row, new_id: i64) -> Result<Row> {
227        if !self.infos.contains_key(table) {
228            self.infos.insert(table.to_owned(), column_infos(self.source.database(), table)?);
229        }
230        let mut columns = Vec::with_capacity(row.columns().len());
231        let mut values = Vec::with_capacity(row.columns().len());
232        for (column, value) in row.iter() {
233            if column == "_PW_ID" {
234                continue;
235            }
236            let info = self.infos[table].get(column).cloned();
237            let key = (table.to_owned(), column.to_owned());
238            let value = if column == "MainId" {
239                Value::Integer(new_id)
240            } else if self.skip.contains(&key) {
241                value.clone()
242            } else if info.as_ref().is_some_and(|info| info.owner_type == OWNER_CANVAS) {
243                Value::Integer(self.canvas_id)
244            } else if info.as_ref().is_some_and(|info| info.owner_type == OWNER_LAYER) {
245                Value::Integer(self.owner_layer)
246            } else if let (Some(link), Value::Integer(id)) = (info.and_then(|info| info.link_table), value)
247                && *id != 0
248            {
249                Value::Integer(self.copy(target, &link, *id)?)
250            } else if let Value::Blob(_) | Value::Text(_) = value
251                && self.externals.contains(&key)
252                && let Some(id) = value_as_text(value).filter(|id| !id.is_empty())
253            {
254                Value::Blob(self.copy_external(table, column, &id)?.into_bytes())
255            } else {
256                value.clone()
257            };
258            columns.push(column.to_owned());
259            values.push(value);
260        }
261        Ok(Row::new(columns, values))
262    }
263
264    fn copy_external(&mut self, table: &str, column: &str, external_id: &str) -> Result<String> {
265        let new_id = new_external_id();
266        if let Some(payload) = self.source.external(external_id) {
267            let mut payload = payload.to_vec();
268            if let Some(hook) = self.payload_hook {
269                payload = hook(table, column, payload)?;
270            }
271            self.new_externals.push((new_id.clone(), payload));
272        }
273        Ok(new_id)
274    }
275}
276
277/// Give `target` every table and column of `tables` that `source` has,
278/// with their `ParamScheme` and `ElemScheme` rows.
279pub fn merge_schema(target: &Connection, source: &Connection, tables: &[&str]) -> Result<()> {
280    for table in tables {
281        if !has_table(source, table)? {
282            continue;
283        }
284        if !has_table(target, table)? {
285            let sql: String = source.query_row(
286                "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?1",
287                [table],
288                |row| row.get(0),
289            )?;
290            target.execute(&sql, [])?;
291            copy_scheme_rows(target, source, table, None)?;
292            ensure_elem_scheme(target, source, table)?;
293            continue;
294        }
295        merge_columns(target, source, table, None)?;
296    }
297    Ok(())
298}
299
300/// Give `target`'s existing `table` the columns `source` has (only those in
301/// `only`, when given), with their `ParamScheme` rows.
302pub fn merge_columns(target: &Connection, source: &Connection, table: &str, only: Option<&[&str]>) -> Result<()> {
303    if !has_table(source, table)? || !has_table(target, table)? {
304        return Ok(());
305    }
306    let have: HashSet<String> = table_columns(target, table)?.into_iter().collect();
307    let mut statement = source.prepare(&format!("PRAGMA table_info({})", quote(table)))?;
308    let columns: Vec<(String, String, Option<String>)> = statement
309        .query_map([], |row| Ok((row.get(1)?, row.get(2)?, row.get(4)?)))?
310        .collect::<std::result::Result<_, _>>()?;
311    for (name, declared_type, default) in columns {
312        if have.contains(&name) || only.is_some_and(|only| !only.contains(&name.as_str())) {
313            continue;
314        }
315        let mut definition = format!("{} {declared_type}", quote(&name));
316        if let Some(default) = default {
317            definition += &format!(" DEFAULT {default}");
318        }
319        target.execute(&format!("ALTER TABLE {} ADD COLUMN {definition}", quote(table)), [])?;
320        copy_scheme_rows(target, source, table, Some(&name))?;
321    }
322    Ok(())
323}
324
325fn copy_scheme_rows(target: &Connection, source: &Connection, table: &str, column: Option<&str>) -> Result<()> {
326    if !has_table(target, "ParamScheme")? || !has_table(source, "ParamScheme")? {
327        return Ok(());
328    }
329    let target_columns: HashSet<String> = table_columns(target, "ParamScheme")?.into_iter().collect();
330    let common: Vec<String> = table_columns(source, "ParamScheme")?
331        .into_iter()
332        .filter(|name| name != "_PW_ID" && target_columns.contains(name))
333        .collect();
334    if !common.iter().any(|name| name == "LabelName") {
335        return Ok(());
336    }
337    let quoted: Vec<String> = common.iter().map(|name| quote(name)).collect();
338    let mut sql = format!("SELECT {} FROM ParamScheme WHERE TableName = ?1", quoted.join(", "));
339    let mut parameters = vec![table.to_owned()];
340    if let Some(column) = column {
341        sql += " AND LabelName = ?2";
342        parameters.push(column.to_owned());
343    }
344    let present: HashSet<String> = select(target, "SELECT LabelName FROM ParamScheme WHERE TableName = ?1", [table])?
345        .iter()
346        .filter_map(|row| row.text("LabelName"))
347        .collect();
348    let marks: Vec<String> = (1..=common.len()).map(|i| format!("?{i}")).collect();
349    let insert = format!("INSERT INTO ParamScheme ({}) VALUES ({})", quoted.join(", "), marks.join(", "));
350    for row in select(source, &sql, params_from_iter(parameters.iter()))? {
351        if row.text("LabelName").is_some_and(|label| present.contains(&label)) {
352            continue;
353        }
354        target.execute(&insert, params_from_iter(row.values().iter()))?;
355    }
356    Ok(())
357}
358
359fn ensure_elem_scheme(target: &Connection, source: &Connection, table: &str) -> Result<()> {
360    if !has_table(target, "ElemScheme")? {
361        return Ok(());
362    }
363    let query = "SELECT ElemType FROM ElemScheme WHERE TableName = ?1";
364    if target.query_row(query, [table], |_| Ok(())).optional()?.is_some() {
365        return Ok(());
366    }
367    let element_type: i64 = if has_table(source, "ElemScheme")? {
368        source.query_row(query, [table], |row| row.get::<_, Option<i64>>(0)).optional()?.flatten().unwrap_or(0)
369    } else {
370        0
371    };
372    target
373        .execute("INSERT INTO ElemScheme (TableName, ElemType, MaxIndex) VALUES (?1, ?2, 0)", (table, element_type))?;
374    Ok(())
375}
376
377/// Copy the rows of `tables` keeping their ids, for each table that is empty
378/// in `target`, and point `Canvas.BrushStyleManager` at the copied manager
379/// when the target has none. When brush styles are copied, the source's
380/// `Canvas.BrushStyleReadProtect070` comes with them. Returns the tables copied. Links out of the
381/// copied rows are left as they are (see [`ClipFile::prepare_copy_from`]
382/// for brush textures).
383pub fn share_rows(target: &Connection, source: &Connection, tables: &[&str]) -> Result<Vec<String>> {
384    let mut copied = Vec::new();
385    for table in tables {
386        if !has_table(source, table)? || !has_table(target, table)? {
387            continue;
388        }
389        let count: i64 = target.query_row(&format!("SELECT COUNT(*) FROM {}", quote(table)), [], |row| row.get(0))?;
390        if count > 0 {
391            continue;
392        }
393        for row in select(source, &format!("SELECT * FROM {}", quote(table)), [])? {
394            let kept: Vec<String> = row.columns().iter().filter(|c| *c != "_PW_ID").cloned().collect();
395            let values: Vec<Value> = kept.iter().map(|c| row.get(c).cloned().unwrap_or(Value::Null)).collect();
396            insert_row(target, table, &Row::new(kept, values))?;
397        }
398        copied.push((*table).to_owned());
399        if has_table(source, "ElemScheme")? && has_table(target, "ElemScheme")? {
400            let source_max: Option<i64> = source
401                .query_row("SELECT MaxIndex FROM ElemScheme WHERE TableName = ?1", [table], |row| row.get(0))
402                .optional()?
403                .flatten();
404            if let Some(source_max) = source_max {
405                target.execute(
406                    "UPDATE ElemScheme SET MaxIndex = MAX(MaxIndex, ?1) WHERE TableName = ?2",
407                    (source_max, table),
408                )?;
409            }
410        }
411    }
412    if has_column(target, "Canvas", "BrushStyleManager")? && has_column(source, "Canvas", "BrushStyleManager")? {
413        let manager: Value = source.query_row("SELECT BrushStyleManager FROM Canvas", [], |row| row.get(0))?;
414        let current: Value = target.query_row("SELECT BrushStyleManager FROM Canvas", [], |row| row.get(0))?;
415        if matches!(current, Value::Null | Value::Integer(0)) {
416            target.execute("UPDATE Canvas SET BrushStyleManager = ?1", [manager])?;
417        }
418    }
419    let flag = "BrushStyleReadProtect070";
420    if copied.iter().any(|table| table == "BrushStyle")
421        && has_column(target, "Canvas", flag)?
422        && has_column(source, "Canvas", flag)?
423    {
424        let value: Value = source.query_row(&format!("SELECT {flag} FROM Canvas"), [], |row| row.get(0))?;
425        target.execute(&format!("UPDATE Canvas SET {flag} = ?1"), [value])?;
426    }
427    Ok(copied)
428}
429
430/// Options for [`ClipFile::copy_layer_from`].
431#[derive(Default)]
432pub struct CopyLayer<'h> {
433    /// Rewrites external payloads while they are copied (for example the
434    /// copied layer's `VectorObjectList.VectorData`).
435    pub payload_hook: Option<&'h PayloadHook<'h>>,
436    /// Rewrites the new Layer row before it is inserted.
437    pub row_hook: Option<&'h mut RowHook<'h>>,
438    /// Folder the copy goes into (on top of its children); the root folder
439    /// when None.
440    pub parent: Option<i64>,
441}
442
443impl ClipFile {
444    /// Give this file the tables, columns and brush styles a layer copied
445    /// from `source` needs ([`LAYER_TABLES`], [`share_rows`]).
446    pub fn prepare_copy_from(&mut self, source: &ClipFile) -> Result<()> {
447        merge_schema(self.database(), source.database(), &LAYER_TABLES)?;
448        merge_columns(self.database(), source.database(), "Canvas", Some(&LAYER_CANVAS_COLUMNS))?;
449        let copied = share_rows(self.database(), source.database(), &SHARED_TABLES)?;
450        if copied.iter().any(|table| table == "BrushPatternImage") {
451            self.copy_pattern_images(source)?;
452        }
453        Ok(())
454    }
455
456    /// A brush texture (`BrushPatternImage`) is an image stored like a
457    /// layer's (Mipmap → MipmapInfo → Offscreen, owned by no layer). Copy
458    /// each one and point the copied row at it: left as it was, the row
459    /// names another image of this file, or none, and CSP crashes.
460    fn copy_pattern_images(&mut self, source: &ClipFile) -> Result<()> {
461        let images = select(source.database(), "SELECT MainId, Mipmap FROM BrushPatternImage", [])?;
462        let mut copier = RowCopier::new(source, None, self.canvas.main_id)?;
463        let mut moved = Vec::new();
464        for image in images {
465            let mipmap = image.int("Mipmap");
466            if mipmap != 0 {
467                moved.push((image.int("MainId"), copier.copy(self.database(), "Mipmap", mipmap)?));
468            }
469        }
470        for (id, payload) in copier.new_externals {
471            self.set_external(&id, payload);
472        }
473        for (image, mipmap) in moved {
474            self.database().execute("UPDATE BrushPatternImage SET Mipmap = ?1 WHERE MainId = ?2", (mipmap, image))?;
475        }
476        Ok(())
477    }
478
479    /// Copy layer `layer_id` of `source` (its row, without children) and the
480    /// rows it links to (vector data and the like) into this file; return
481    /// the new layer's id.
482    ///
483    /// The copy keeps its position (`LayerOffsetX/Y`), gets a new uuid and
484    /// new render offscreens, and loses what it cannot carry
485    /// ([`DROPPED_LAYER_LINKS`]: the layer mask, a frame folder's images, 3D,
486    /// rulers). A raster layer's stored pixels are copied with the area they
487    /// are stored in (which may reach past the canvas); other layers get
488    /// offscreens the size of this canvas, which CSP draws into when it opens
489    /// the file. Call [`ClipFile::prepare_copy_from`] first.
490    pub fn copy_layer_from(&mut self, source: &ClipFile, layer_id: i64, options: CopyLayer<'_>) -> Result<i64> {
491        let layer = source
492            .layers
493            .get(layer_id)
494            .ok_or_else(|| Error::format(format!("No layer {layer_id} in the source")))?
495            .clone();
496        if layer.kind == LayerKind::Root {
497            return Err(Error::format("The root folder cannot be copied"));
498        }
499        let mut row = layer.row.clone();
500        for column in DROPPED_LAYER_LINKS {
501            row.set(column, Value::Null);
502        }
503        let new_id = self.next_main_id("Layer")?;
504        let (translated, copied) = {
505            let mut copier = RowCopier::new(source, options.payload_hook, self.canvas.main_id)?;
506            copier.owner_layer = new_id;
507            for column in ["LayerRenderMipmap", "LayerRenderThumbnail", "LayerNextIndex", "LayerFirstChildIndex"] {
508                copier.skip.insert(("Layer".to_owned(), column.to_owned()));
509            }
510            let translated = copier.translate(self.database(), "Layer", row, new_id)?;
511            (translated, copier.new_externals)
512        };
513        for (id, payload) in copied {
514            self.set_external(&id, payload);
515        }
516        let raster = layer.layer_type & crate::model::LAYER_TYPE_RASTER != 0;
517        // A raster layer keeps the stored area of its pixels, which can reach
518        // past the canvas; other layers get offscreens the size of the canvas
519        // at its origin.
520        let stored = if raster { source.mipmap_base(layer.render_mipmap)? } else { None };
521        let (mipmap, render_offset) = match &stored {
522            Some(base) => (
523                self.new_mipmap_sized(new_id, (base.parameter.width, base.parameter.height), None, true)?,
524                layer.render_offset,
525            ),
526            None => (self.new_mipmap(new_id, None, raster)?, (-layer.offset.0, -layer.offset.1)),
527        };
528        let thumbnail = self.new_thumbnail(new_id)?;
529        let int = Value::Integer;
530        let mut row = translated;
531        // Without a mask the masked bit goes.
532        let layer_type = row.int("LayerType") & !LAYER_TYPE_MASKED;
533        for (column, value) in [
534            ("LayerType", int(layer_type)),
535            ("LayerRenderMipmap", int(mipmap)),
536            ("LayerRenderThumbnail", int(thumbnail)),
537            ("LayerNextIndex", int(0)),
538            ("LayerFirstChildIndex", int(0)),
539            ("LayerRenderOffscrOffsetX", int(render_offset.0)),
540            ("LayerRenderOffscrOffsetY", int(render_offset.1)),
541            ("LayerMaskOffsetX", int(0)),
542            ("LayerMaskOffsetY", int(0)),
543            ("LayerMaskOffscrOffsetX", int(0)),
544            ("LayerMaskOffscrOffsetY", int(0)),
545            ("LayerSelect", int(0)),
546            ("LayerUuid", Value::Text(crate::edit::layer_uuid())),
547        ] {
548            row = with_value(row, column, value);
549        }
550        let visibility = row.int("LayerVisibility");
551        if visibility & 2 != 0 {
552            row = with_value(row, "LayerVisibility", int(visibility & !2));
553        }
554        if let Some(hook) = options.row_hook {
555            hook(&mut row)?;
556        }
557        insert_row(self.database(), "Layer", &row)?;
558        let parent = options.parent.unwrap_or(self.canvas.root_folder);
559        self.link_on_top(parent, new_id)?;
560        self.reload()?;
561        if raster && let Some((image, origin)) = source.layer_pixels(&layer)? {
562            self.set_layer_pixels(new_id, &image, origin)?;
563        }
564        Ok(new_id)
565    }
566
567    /// Give a frame folder (コマ枠フォルダー) copied with
568    /// [`ClipFile::copy_layer_from`] its mask and border images: offscreens
569    /// the size of the canvas with no pixels stored, which CSP draws from
570    /// the frames when it opens the file (CSP saves a frame folder's so).
571    pub fn add_frame_images(&mut self, folder_id: i64) -> Result<()> {
572        let mask = self.new_mipmap(folder_id, None, false)?;
573        let mask_thumbnail = self.new_thumbnail(folder_id)?;
574        let line = self.new_mipmap(folder_id, None, false)?;
575        let levels = self.mipmap(mask)?.map(|mipmap| mipmap.levels).unwrap_or_default();
576        for level in levels {
577            let row = self
578                .row("Offscreen", level.offscreen)?
579                .ok_or_else(|| Error::format(format!("No Offscreen {}", level.offscreen)))?;
580            let parameter = Attribute::parse(row.blob("Attribute").unwrap_or_default())?.parameter()?;
581            self.database().execute(
582                "UPDATE Offscreen SET Attribute = ?1 WHERE MainId = ?2",
583                (mask_attribute(parameter.width, parameter.height), level.offscreen),
584            )?;
585        }
586        let int = Value::Integer;
587        for (column, value) in [
588            ("LayerLayerMaskMipmap", int(mask)),
589            ("LayerLayerMaskThumbnail", int(mask_thumbnail)),
590            ("ComicFrameLineMipmap", int(line)),
591        ] {
592            self.set_layer_value(folder_id, column, value)?;
593        }
594        self.reload()
595    }
596}
597
598/// The `Attribute` of a layer mask offscreen with no pixels stored.
599fn mask_attribute(width: u32, height: u32) -> Vec<u8> {
600    let mut attribute = Attribute::new(&Parameter::new(PixelKind::Color, width, height), None);
601    if let Some(section) = attribute.sections.iter_mut().find(|section| section.name == "Parameter") {
602        section.values[4..16].copy_from_slice(&MASK_PARAMETERS);
603    }
604    attribute.to_bytes()
605}
606
607/// `id` when `target` has that brush style, else its first brush style (or
608/// `id` when it has none): the brush a copied vector object should name.
609pub fn brush_style_in(target: &Connection, id: u32) -> Result<u32> {
610    if !has_table(target, "BrushStyle")? {
611        return Ok(id);
612    }
613    if target.query_row("SELECT 1 FROM BrushStyle WHERE MainId = ?1", [id], |_| Ok(())).optional()?.is_some() {
614        return Ok(id);
615    }
616    let first: Option<i64> = target.query_row("SELECT MIN(MainId) FROM BrushStyle", [], |row| row.get(0))?;
617    Ok(first.map_or(id, |first| first as u32))
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623    use crate::create::NewCanvas;
624    use crate::edit::NewLayer;
625    use crate::image::RgbaImage;
626
627    #[test]
628    fn raster_layers_copy_with_their_pixels() {
629        let mut source = ClipFile::create(&NewCanvas::default()).unwrap();
630        let mut image = RgbaImage::new(40, 30);
631        image.fill([10, 200, 30, 255]);
632        let id = source.add_raster_layer("source", &image, (100, 50)).unwrap();
633        source.set_layer_opacity(id, 0.5).unwrap();
634        let source = ClipFile::from_bytes(source.to_bytes().unwrap()).unwrap();
635
636        let mut target = ClipFile::create(&NewCanvas::default()).unwrap();
637        let folder = target.add_layer(target.canvas.root_folder, NewLayer::Folder, "folder").unwrap();
638        target.prepare_copy_from(&source).unwrap();
639        let mut rename = |row: &mut Row| {
640            row.set("LayerName", Value::Text("copy".into()));
641            Ok(())
642        };
643        let options = CopyLayer { row_hook: Some(&mut rename), parent: Some(folder), ..Default::default() };
644        let copy = target.copy_layer_from(&source, id, options).unwrap();
645        let target = ClipFile::from_bytes(target.to_bytes().unwrap()).unwrap();
646        let layer = target.layers.get(copy).unwrap();
647        assert_eq!(layer.name, "copy");
648        assert_eq!(layer.kind, LayerKind::Raster);
649        assert_eq!(target.layers.get(folder).unwrap().first_child, copy);
650        assert!((layer.opacity_fraction() - 0.5).abs() < 0.01);
651        assert_ne!(layer.row.text("LayerUuid"), source.layers.get(id).unwrap().row.text("LayerUuid"));
652        let part = target.layer_pixels_in_box(layer, (100, 50, 140, 80)).unwrap();
653        assert_eq!(part, image);
654        assert_eq!(target.layer_pixels_in_box(layer, (99, 49, 100, 50)).unwrap().pixel(0, 0), [0; 4]);
655        let report = crate::verify::check_clip(target.to_bytes().unwrap(), Default::default());
656        assert!(report.ok(), "{:?}", report.errors);
657    }
658
659    #[test]
660    fn dropped_links_become_null() {
661        let mut source = ClipFile::create(&NewCanvas::default()).unwrap();
662        let layer = source.add_raster_layer("one", &RgbaImage::new(4, 4), (0, 0)).unwrap();
663        source
664            .database()
665            .execute_batch(
666                "ALTER TABLE Layer ADD COLUMN ComicFrameLineMipmap INTEGER; UPDATE Layer SET ComicFrameLineMipmap = 0;",
667            )
668            .unwrap();
669        let source = ClipFile::from_bytes(source.to_bytes().unwrap()).unwrap();
670        let mut target = ClipFile::create(&NewCanvas::default()).unwrap();
671        target.prepare_copy_from(&source).unwrap();
672        let copy = target.copy_layer_from(&source, layer, CopyLayer::default()).unwrap();
673        let value: Value = target
674            .database()
675            .query_row("SELECT ComicFrameLineMipmap FROM Layer WHERE MainId = ?1", [copy], |row| row.get(0))
676            .unwrap();
677        assert_eq!(value, Value::Null);
678    }
679
680    #[test]
681    fn brush_styles_come_with_the_textures_they_use() {
682        // The texture image: a layer's mipmap, handed over to the pattern.
683        let mut source = ClipFile::create(&NewCanvas::default()).unwrap();
684        let mut texture = RgbaImage::new(8, 8);
685        texture.fill([90, 90, 90, 255]);
686        let layer = source.add_raster_layer("texture", &texture, (0, 0)).unwrap();
687        let texture_mipmap = source.layers.get(layer).unwrap().render_mipmap;
688        source
689            .database()
690            .execute_batch(
691                "CREATE TABLE BrushStyleManager(_PW_ID INTEGER PRIMARY KEY AUTOINCREMENT, MainId INTEGER, CanvasId INTEGER, FirstBrushStyle INTEGER, FirstPatternImage INTEGER);
692                 CREATE TABLE BrushStyle(_PW_ID INTEGER PRIMARY KEY AUTOINCREMENT, MainId INTEGER, NextIndex INTEGER, TexturePattern INTEGER);
693                 CREATE TABLE BrushPatternImage(_PW_ID INTEGER PRIMARY KEY AUTOINCREMENT, MainId INTEGER, NextIndex INTEGER, ImageData BLOB);
694                 INSERT INTO BrushStyleManager(MainId, CanvasId, FirstBrushStyle, FirstPatternImage) VALUES (1, 1, 1, 1);
695                 INSERT INTO BrushStyle(MainId, NextIndex, TexturePattern) VALUES (1, 2, 0), (2, 0, 1);
696                 INSERT INTO BrushPatternImage(MainId, NextIndex, ImageData) VALUES (1, 0, x'0102');",
697            )
698            .unwrap();
699        source.database().execute("ALTER TABLE BrushPatternImage ADD COLUMN Mipmap INTEGER", []).unwrap();
700        source.database().execute("UPDATE BrushPatternImage SET Mipmap = ?1", [texture_mipmap]).unwrap();
701        source
702            .database()
703            .execute(
704                "INSERT INTO ParamScheme (TableName, LabelName, DataType, OwnerType, LinkTable) VALUES ('BrushPatternImage', 'Mipmap', 1, 0, 'Mipmap')",
705                [],
706            )
707            .unwrap();
708        source
709            .database()
710            .execute_batch("ALTER TABLE Canvas ADD COLUMN BrushStyleReadProtect070 INTEGER; UPDATE Canvas SET BrushStyleReadProtect070 = 1;")
711            .unwrap();
712        let source = ClipFile::from_bytes(source.to_bytes().unwrap()).unwrap();
713        let mut target = ClipFile::create(&NewCanvas::default()).unwrap();
714        target.add_raster_layer("other", &RgbaImage::new(4, 4), (0, 0)).unwrap();
715        target.prepare_copy_from(&source).unwrap();
716        let target = ClipFile::from_bytes(target.to_bytes().unwrap()).unwrap();
717        let images = select(target.database(), "SELECT MainId, ImageData, Mipmap FROM BrushPatternImage", []).unwrap();
718        assert_eq!(images.len(), 1);
719        assert_eq!(images[0].int("MainId"), 1);
720        assert_eq!(images[0].blob("ImageData"), Some(&[1u8, 2][..]));
721        // The texture is a copy of its own, not whatever image had its id.
722        let mipmap = images[0].int("Mipmap");
723        assert!(target.layers.flatten().iter().all(|(layer, _)| layer.render_mipmap != mipmap));
724        let base = target.mipmap_base(mipmap).unwrap().unwrap();
725        let original = source.mipmap_base(texture_mipmap).unwrap().unwrap();
726        assert_eq!(base.parameter, original.parameter);
727        let levels = |file: &ClipFile, id| file.mipmap(id).unwrap().unwrap().levels.len();
728        assert_eq!(levels(&target, mipmap), levels(&source, texture_mipmap));
729        let styles = select(target.database(), "SELECT MainId FROM BrushStyle ORDER BY MainId", []).unwrap();
730        assert_eq!(styles.iter().map(|row| row.int("MainId")).collect::<Vec<_>>(), [1, 2]);
731        let flag: i64 =
732            target.database().query_row("SELECT BrushStyleReadProtect070 FROM Canvas", [], |row| row.get(0)).unwrap();
733        assert_eq!(flag, 1);
734    }
735}