mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 22:35:26 +00:00
Merge main into feat/duplicate-layer
This commit is contained in:
commit
8522253d74
21 files changed
+454
-377
No files matched your search
@@ -128,21 +128,33 @@ struct Rep {
|
||||
text: String,
|
||||
}
|
||||
|
||||
/// Where `fill_row` looks for linked images: the merge options and the two search folders.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Sources<'a> {
|
||||
pub options: &'a MergeOptions,
|
||||
pub data_dir: Option<&'a Path>,
|
||||
pub doc_dir: Option<&'a Path>,
|
||||
}
|
||||
|
||||
/// Replacements and hyperlinks collected while walking the placeholders of one record.
|
||||
#[derive(Default)]
|
||||
struct Pending {
|
||||
reps: Vec<Rep>,
|
||||
links: Vec<(PlaceholderAnchor, HyperlinkDest, String)>,
|
||||
parent: bool,
|
||||
report: FillReport,
|
||||
}
|
||||
|
||||
/// Fill `row` (0-based) into the items `maps` points at. `record` is the 1-based source row.
|
||||
pub fn fill_row(
|
||||
doc: &mut designcraft_doc::Document,
|
||||
fields: &[DataField],
|
||||
row: &[String],
|
||||
record: u32,
|
||||
options: &MergeOptions,
|
||||
maps: &Maps,
|
||||
data_dir: Option<&Path>,
|
||||
doc_dir: Option<&Path>,
|
||||
sources: &Sources,
|
||||
) -> FillReport {
|
||||
let mut report = FillReport::default();
|
||||
let mut parent = false;
|
||||
let mut reps: Vec<Rep> = Vec::new();
|
||||
let mut links: Vec<(PlaceholderAnchor, HyperlinkDest, String)> = Vec::new();
|
||||
let mut pending = Pending::default();
|
||||
let placeholders = doc.data_merge.placeholders.clone();
|
||||
for ph in &placeholders {
|
||||
if !fields.iter().any(|f| f.name == ph.field) {
|
||||
@@ -150,43 +162,11 @@ pub fn fill_row(
|
||||
}
|
||||
let cell = cell_of(fields, row, &ph.field).to_string();
|
||||
match &ph.anchor {
|
||||
PlaceholderAnchor::Text { story, start, end } => {
|
||||
let Some(&mapped) = maps.stories.get(story) else {
|
||||
if anchor_is_parent(doc, &ph.anchor) {
|
||||
parent = true;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
match ph.role {
|
||||
PlaceholderRole::Text => reps.push(Rep { story: mapped, start: *start, end: *end, text: cell }),
|
||||
PlaceholderRole::Hyperlink => {
|
||||
if let Some(dest) = classify_link(&cell) {
|
||||
links.push((PlaceholderAnchor::Text { story: mapped, start: *start, end: *end }, dest, ph.field.clone()));
|
||||
}
|
||||
}
|
||||
PlaceholderRole::Image | PlaceholderRole::Qr => {}
|
||||
}
|
||||
}
|
||||
PlaceholderAnchor::Item { id } => {
|
||||
let Some(&mapped) = maps.items.get(id) else {
|
||||
if anchor_is_parent(doc, &ph.anchor) {
|
||||
parent = true;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
match ph.role {
|
||||
PlaceholderRole::Image => place_image(doc, mapped, &ph.field, &cell, record, options, data_dir, doc_dir, &mut report),
|
||||
PlaceholderRole::Qr => place_qr(doc, mapped, &ph.field, &cell, record, &mut report),
|
||||
PlaceholderRole::Hyperlink => {
|
||||
if let Some(dest) = classify_link(&cell) {
|
||||
links.push((PlaceholderAnchor::Item { id: mapped }, dest, ph.field.clone()));
|
||||
}
|
||||
}
|
||||
PlaceholderRole::Text => {}
|
||||
}
|
||||
}
|
||||
PlaceholderAnchor::Text { story, start, end } => plan_text(doc, maps, ph, (*story, *start, *end), cell, &mut pending),
|
||||
PlaceholderAnchor::Item { .. } => plan_item(doc, maps, ph, &cell, record, sources, &mut pending),
|
||||
}
|
||||
}
|
||||
let Pending { mut reps, links, parent, mut report } = pending;
|
||||
typed_reps(doc, fields, row, maps, &placeholders, &mut reps);
|
||||
apply_reps(doc, &reps);
|
||||
for (anchor, dest, field) in links {
|
||||
@@ -207,6 +187,59 @@ pub fn fill_row(
|
||||
report
|
||||
}
|
||||
|
||||
fn plan_text(
|
||||
doc: &designcraft_doc::Document,
|
||||
maps: &Maps,
|
||||
ph: &designcraft_doc::Placeholder,
|
||||
(story, start, end): (StoryId, usize, usize),
|
||||
cell: String,
|
||||
pending: &mut Pending,
|
||||
) {
|
||||
let Some(&mapped) = maps.stories.get(&story) else {
|
||||
if anchor_is_parent(doc, &ph.anchor) {
|
||||
pending.parent = true;
|
||||
}
|
||||
return;
|
||||
};
|
||||
match ph.role {
|
||||
PlaceholderRole::Text => pending.reps.push(Rep { story: mapped, start, end, text: cell }),
|
||||
PlaceholderRole::Hyperlink => {
|
||||
if let Some(dest) = classify_link(&cell) {
|
||||
pending.links.push((PlaceholderAnchor::Text { story: mapped, start, end }, dest, ph.field.clone()));
|
||||
}
|
||||
}
|
||||
PlaceholderRole::Image | PlaceholderRole::Qr => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_item(
|
||||
doc: &mut designcraft_doc::Document,
|
||||
maps: &Maps,
|
||||
ph: &designcraft_doc::Placeholder,
|
||||
cell: &str,
|
||||
record: u32,
|
||||
sources: &Sources,
|
||||
pending: &mut Pending,
|
||||
) {
|
||||
let PlaceholderAnchor::Item { id } = &ph.anchor else { return };
|
||||
let Some(&mapped) = maps.items.get(id) else {
|
||||
if anchor_is_parent(doc, &ph.anchor) {
|
||||
pending.parent = true;
|
||||
}
|
||||
return;
|
||||
};
|
||||
match ph.role {
|
||||
PlaceholderRole::Image => place_image(doc, mapped, &ph.field, cell, record, sources, &mut pending.report),
|
||||
PlaceholderRole::Qr => place_qr(doc, mapped, &ph.field, cell, record, &mut pending.report),
|
||||
PlaceholderRole::Hyperlink => {
|
||||
if let Some(dest) = classify_link(cell) {
|
||||
pending.links.push((PlaceholderAnchor::Item { id: mapped }, dest, ph.field.clone()));
|
||||
}
|
||||
}
|
||||
PlaceholderRole::Text => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn typed_reps(
|
||||
doc: &designcraft_doc::Document,
|
||||
fields: &[DataField],
|
||||
@@ -326,17 +359,8 @@ fn create_link(doc: &mut designcraft_doc::Document, anchor: PlaceholderAnchor, d
|
||||
true
|
||||
}
|
||||
|
||||
fn place_image(
|
||||
doc: &mut designcraft_doc::Document,
|
||||
item: ItemId,
|
||||
field: &str,
|
||||
cell: &str,
|
||||
record: u32,
|
||||
options: &MergeOptions,
|
||||
data_dir: Option<&Path>,
|
||||
doc_dir: Option<&Path>,
|
||||
report: &mut FillReport,
|
||||
) {
|
||||
fn place_image(doc: &mut designcraft_doc::Document, item: ItemId, field: &str, cell: &str, record: u32, sources: &Sources, report: &mut FillReport) {
|
||||
let Sources { options, data_dir, doc_dir } = *sources;
|
||||
let cands = image_candidates(cell, data_dir, doc_dir);
|
||||
let found = cands.iter().find_map(|p| {
|
||||
// Refuse huge files before reading them; a too-large image counts as missing.
|
||||
|
||||
@@ -18,7 +18,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, id_param, ok, str_param};
|
||||
use crate::{DocState, Result, Session};
|
||||
use fill::{FillReport, Maps, MissingImage, copy_item, fill_row, identity_map};
|
||||
use fill::{FillReport, Maps, MissingImage, Sources, copy_item, fill_row, identity_map};
|
||||
use parse::{
|
||||
Table, TileInput, extension, fingerprint_of, relative_between, select_records, status_of, table_from_bytes, table_from_grid, table_from_objects,
|
||||
tile,
|
||||
@@ -210,7 +210,8 @@ fn preview_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let row = source.rows.get((record as usize).saturating_sub(1)).cloned().unwrap_or_default();
|
||||
let data_dir = source_file(&source, st.path.as_deref().map(Path::new)).as_deref().and_then(Path::parent).map(Path::to_path_buf);
|
||||
let doc_dir = st.path.as_deref().and_then(parent_dir);
|
||||
fill_row(&mut doc, &source.fields, &row, record as u32, &options, &maps, data_dir.as_deref(), doc_dir.as_deref());
|
||||
let sources = Sources { options: &options, data_dir: data_dir.as_deref(), doc_dir: doc_dir.as_deref() };
|
||||
fill_row(&mut doc, &source.fields, &row, record as u32, &maps, &sources);
|
||||
st.doc = Arc::new(doc);
|
||||
st.preview_record = Some(record as u32);
|
||||
st.bump_revision();
|
||||
@@ -245,10 +246,12 @@ fn merge_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
warnings.extend(table.warnings.iter().cloned());
|
||||
let mut merged = src.clone();
|
||||
let mut missing = Vec::new();
|
||||
let sources = Sources { options: &options, data_dir: data_dir.as_deref(), doc_dir: doc_dir.as_deref() };
|
||||
let mut sink = Sink { warnings: &mut warnings, missing: &mut missing, parent: false };
|
||||
if options.per_page == "multiple" {
|
||||
tile_records(&mut merged, &src, &table, &selected, &options, data_dir.as_deref(), doc_dir.as_deref(), &mut warnings, &mut missing)?;
|
||||
tile_records(&mut merged, &src, &table, &selected, &sources, &mut sink)?;
|
||||
} else {
|
||||
copy_records(&mut merged, &src, &table, &selected, &options, data_dir.as_deref(), doc_dir.as_deref(), &mut warnings, &mut missing)?;
|
||||
copy_records(&mut merged, &src, &table, &selected, &sources, &mut sink)?;
|
||||
}
|
||||
// Status is written only after every error return. The new document then becomes active,
|
||||
// so this template edit does not push an undo step. The fingerprint stays the last read.
|
||||
@@ -272,25 +275,14 @@ fn merge_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn copy_records(
|
||||
dst: &mut Document,
|
||||
src: &Document,
|
||||
table: &Table,
|
||||
selected: &[usize],
|
||||
options: &MergeOptions,
|
||||
data_dir: Option<&Path>,
|
||||
doc_dir: Option<&Path>,
|
||||
warnings: &mut Vec<String>,
|
||||
missing: &mut Vec<MissingImage>,
|
||||
) -> Result<()> {
|
||||
let mut parent = false;
|
||||
fn copy_records(dst: &mut Document, src: &Document, table: &Table, selected: &[usize], sources: &Sources, sink: &mut Sink) -> Result<()> {
|
||||
if let Some(&idx) = selected.first() {
|
||||
let maps = identity_map(dst);
|
||||
fill_at(dst, table, idx, options, &maps, data_dir, doc_dir, warnings, missing, &mut parent);
|
||||
fill_at(dst, table, idx, sources, &maps, sink);
|
||||
}
|
||||
for &idx in selected.iter().skip(1) {
|
||||
let maps = append_copy(dst, src)?;
|
||||
fill_at(dst, table, idx, options, &maps, data_dir, doc_dir, warnings, missing, &mut parent);
|
||||
fill_at(dst, table, idx, sources, &maps, sink);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -320,17 +312,8 @@ fn append_copy(dst: &mut Document, src: &Document) -> Result<Maps> {
|
||||
Ok(maps)
|
||||
}
|
||||
|
||||
fn tile_records(
|
||||
dst: &mut Document,
|
||||
src: &Document,
|
||||
table: &Table,
|
||||
selected: &[usize],
|
||||
options: &MergeOptions,
|
||||
data_dir: Option<&Path>,
|
||||
doc_dir: Option<&Path>,
|
||||
warnings: &mut Vec<String>,
|
||||
missing: &mut Vec<MissingImage>,
|
||||
) -> Result<()> {
|
||||
fn tile_records(dst: &mut Document, src: &Document, table: &Table, selected: &[usize], sources: &Sources, sink: &mut Sink) -> Result<()> {
|
||||
let options = sources.options;
|
||||
let (block, ids) = block_of(src);
|
||||
let (width, height) = src.page(0).map(|p| (p.width, p.height)).unwrap_or((612.0, 792.0));
|
||||
let plan = tile(&TileInput {
|
||||
@@ -344,7 +327,7 @@ fn tile_records(
|
||||
count: selected.len(),
|
||||
});
|
||||
if plan.one_per_page {
|
||||
warnings.push("Only one record fits on the page, so each record has its own page.".into());
|
||||
sink.warnings.push("Only one record fits on the page, so each record has its own page.".into());
|
||||
}
|
||||
if plan.pages > 1 {
|
||||
let parent = src.page(0).and_then(|p| p.parent);
|
||||
@@ -356,12 +339,11 @@ fn tile_records(
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut parent = false;
|
||||
for hit in plan.hits {
|
||||
let Some(&idx) = selected.get(hit.index) else { continue };
|
||||
if hit.index == 0 && hit.page == 0 && hit.col == 0 && hit.row == 0 {
|
||||
let maps = identity_map(dst);
|
||||
fill_at(dst, table, idx, options, &maps, data_dir, doc_dir, warnings, missing, &mut parent);
|
||||
fill_at(dst, table, idx, sources, &maps, sink);
|
||||
continue;
|
||||
}
|
||||
let Some((dsi, _)) = dst.page_loc(hit.page) else { continue };
|
||||
@@ -370,39 +352,37 @@ fn tile_records(
|
||||
let Some(it) = src.item(*id) else { continue };
|
||||
copy_item(dst, src, it, SpreadRef::Doc(dsi), Vec2::new(hit.dx, hit.dy), &mut maps)?;
|
||||
}
|
||||
fill_at(dst, table, idx, options, &maps, data_dir, doc_dir, warnings, missing, &mut parent);
|
||||
fill_at(dst, table, idx, sources, &maps, sink);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fill_at(
|
||||
doc: &mut Document,
|
||||
table: &Table,
|
||||
idx: usize,
|
||||
options: &MergeOptions,
|
||||
maps: &Maps,
|
||||
data_dir: Option<&Path>,
|
||||
doc_dir: Option<&Path>,
|
||||
warnings: &mut Vec<String>,
|
||||
missing: &mut Vec<MissingImage>,
|
||||
parent: &mut bool,
|
||||
) {
|
||||
fn fill_at(doc: &mut Document, table: &Table, idx: usize, sources: &Sources, maps: &Maps, sink: &mut Sink) {
|
||||
let Some(row) = table.rows.get(idx) else { return };
|
||||
let report = fill_row(doc, &table.fields, row, (idx as u32).saturating_add(1), options, maps, data_dir, doc_dir);
|
||||
absorb(report, warnings, missing, parent);
|
||||
let report = fill_row(doc, &table.fields, row, (idx as u32).saturating_add(1), maps, sources);
|
||||
sink.absorb(report);
|
||||
}
|
||||
|
||||
fn absorb(report: FillReport, warnings: &mut Vec<String>, missing: &mut Vec<MissingImage>, parent: &mut bool) {
|
||||
for w in report.warnings {
|
||||
if w == PARENT_WARNING {
|
||||
if *parent {
|
||||
continue;
|
||||
/// Warnings and missing images collected across every record, with the parent-page warning kept to one.
|
||||
struct Sink<'a> {
|
||||
warnings: &'a mut Vec<String>,
|
||||
missing: &'a mut Vec<MissingImage>,
|
||||
parent: bool,
|
||||
}
|
||||
|
||||
impl Sink<'_> {
|
||||
fn absorb(&mut self, report: FillReport) {
|
||||
for w in report.warnings {
|
||||
if w == PARENT_WARNING {
|
||||
if self.parent {
|
||||
continue;
|
||||
}
|
||||
self.parent = true;
|
||||
}
|
||||
*parent = true;
|
||||
self.warnings.push(w);
|
||||
}
|
||||
warnings.push(w);
|
||||
self.missing.extend(report.missing);
|
||||
}
|
||||
missing.extend(report.missing);
|
||||
}
|
||||
|
||||
fn block_of(doc: &Document) -> (Rect, Vec<ItemId>) {
|
||||
|
||||
@@ -128,7 +128,7 @@ pub fn table_from_objects(rows: &[Value]) -> Result<Table, String> {
|
||||
warnings.push(format!("Row {} has an extra field \"{key}\". It was ignored.", i + 1));
|
||||
}
|
||||
}
|
||||
if cells.iter().all(|c| c.is_empty()) {
|
||||
if cells.iter().all(String::is_empty) {
|
||||
continue;
|
||||
}
|
||||
out.push(cells);
|
||||
@@ -159,38 +159,7 @@ pub fn select_records(count: usize, records: &str, one: u32, range: &str, limit:
|
||||
let i = record_index(one, count)?;
|
||||
idx.push(i);
|
||||
}
|
||||
"range" => {
|
||||
if range.trim().is_empty() {
|
||||
return Err("no records".into());
|
||||
}
|
||||
for token in range.split(',') {
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some((a, b)) = token.split_once('-') {
|
||||
let a = parse_record_no(a.trim())?;
|
||||
let b = parse_record_no(b.trim())?;
|
||||
if a > b {
|
||||
return Err(format!("range {a}-{b} is reversed"));
|
||||
}
|
||||
let mut n = a;
|
||||
while n <= b {
|
||||
let i = record_index(n, count)?;
|
||||
if seen.insert(i) {
|
||||
idx.push(i);
|
||||
}
|
||||
n = n.saturating_add(1);
|
||||
}
|
||||
} else {
|
||||
let n = parse_record_no(token)?;
|
||||
let i = record_index(n, count)?;
|
||||
if seen.insert(i) {
|
||||
idx.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"range" => select_range(range, count, &mut idx, &mut seen)?,
|
||||
other => return Err(format!("unknown records value \"{other}\"")),
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
@@ -205,6 +174,41 @@ pub fn select_records(count: usize, records: &str, one: u32, range: &str, limit:
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
/// Expand a comma-separated list of record numbers and `a-b` ranges into `idx`, skipping repeats.
|
||||
fn select_range(range: &str, count: usize, idx: &mut Vec<usize>, seen: &mut std::collections::HashSet<usize>) -> Result<(), String> {
|
||||
if range.trim().is_empty() {
|
||||
return Err("no records".into());
|
||||
}
|
||||
for token in range.split(',') {
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = match token.split_once('-') {
|
||||
Some((a, b)) => (parse_record_no(a.trim())?, parse_record_no(b.trim())?),
|
||||
None => {
|
||||
let n = parse_record_no(token)?;
|
||||
(n, n)
|
||||
}
|
||||
};
|
||||
if a > b {
|
||||
return Err(format!("range {a}-{b} is reversed"));
|
||||
}
|
||||
let mut n = a;
|
||||
while n <= b {
|
||||
let i = record_index(n, count)?;
|
||||
if seen.insert(i) {
|
||||
idx.push(i);
|
||||
}
|
||||
if n == b {
|
||||
break;
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_record_no(s: &str) -> Result<u32, String> {
|
||||
s.parse::<u32>().map_err(|_| format!("bad record \"{s}\""))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! These helpers write real files, so they live in their own `_tests.rs` file: the
|
||||
//! file-system guard in `tests_file_guard.rs` checks `src` files but exempts test ones.
|
||||
|
||||
use super::*;
|
||||
use super::{Arc, AssetId, Content, ItemId, Session, base64_encode, json};
|
||||
|
||||
pub(crate) fn tiff_eps() -> Vec<u8> {
|
||||
let mut tiff = b"II".to_vec();
|
||||
|
||||
@@ -323,18 +323,38 @@ fn replacement(p: &Value) -> String {
|
||||
if bool_or(p, "grep", false) { translate_replacement(c) } else { metachars(c) }
|
||||
}
|
||||
|
||||
/// Repair byte validity of a text selection once against the final story, without remapping valid offsets.
|
||||
fn repair_selection(t: &mut designcraft_doc::TextSel, st: &designcraft_doc::Story) {
|
||||
let range = t.range();
|
||||
let start = floor_char_boundary(&st.text, range.start);
|
||||
let mut end = range.end.min(st.len());
|
||||
if t.is_caret() {
|
||||
end = start;
|
||||
} else if !st.text.is_char_boundary(end) {
|
||||
let floor = floor_char_boundary(&st.text, end);
|
||||
end = st.text.get(floor..).and_then(|rest| rest.chars().next()).map_or(floor, |ch| floor + ch.len_utf8());
|
||||
}
|
||||
(t.anchor, t.focus) = if t.anchor <= t.focus { (start, end) } else { (end, start) };
|
||||
}
|
||||
|
||||
/// The character overrides a Change applies to every replacement, from its `attrs` object.
|
||||
fn char_overrides(attrs: Option<&Value>) -> Result<designcraft_doc::CharAttrs> {
|
||||
let mut over = designcraft_doc::CharAttrs::default();
|
||||
if let Some(o) = attrs.and_then(Value::as_object) {
|
||||
for (k, v) in o {
|
||||
over.set_json(k, v).map_err(|e| bad("find.change", e))?;
|
||||
}
|
||||
}
|
||||
Ok(over)
|
||||
}
|
||||
|
||||
fn change(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let re = build(p)?;
|
||||
let grep = bool_or(p, "grep", false);
|
||||
let rep = replacement(p);
|
||||
let first_only = bool_or(p, "first", false);
|
||||
let attrs = p.get("attrs").cloned();
|
||||
let mut over = designcraft_doc::CharAttrs::default();
|
||||
if let Some(o) = attrs.as_ref().and_then(Value::as_object) {
|
||||
for (k, v) in o {
|
||||
over.set_json(k, v).map_err(|e| bad("find.change", e))?;
|
||||
}
|
||||
}
|
||||
let over = char_overrides(attrs.as_ref())?;
|
||||
super::text::validate_char_fill(&s.doc()?.doc, &over, "find.change")?;
|
||||
let targets = scope(s, p)?.targets;
|
||||
s.edit(|d, sel| {
|
||||
@@ -361,17 +381,7 @@ fn change(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
count += 1;
|
||||
}
|
||||
if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid && t.cell == cell) {
|
||||
// Repair byte validity once against the final story, without remapping valid offsets.
|
||||
let range = t.range();
|
||||
let start = floor_char_boundary(&st.text, range.start);
|
||||
let mut end = range.end.min(st.len());
|
||||
if t.is_caret() {
|
||||
end = start;
|
||||
} else if !st.text.is_char_boundary(end) {
|
||||
let floor = floor_char_boundary(&st.text, end);
|
||||
end = st.text.get(floor..).and_then(|rest| rest.chars().next()).map_or(floor, |ch| floor + ch.len_utf8());
|
||||
}
|
||||
(t.anchor, t.focus) = if t.anchor <= t.focus { (start, end) } else { (end, start) };
|
||||
repair_selection(t, st);
|
||||
}
|
||||
// `first` is the first match of the whole search, not of each story.
|
||||
if first_only && count > 0 {
|
||||
|
||||
@@ -81,36 +81,41 @@ fn list(d: &Document) -> Vec<Value> {
|
||||
used_fonts(d).into_iter().map(|font| json!(font)).collect()
|
||||
}
|
||||
|
||||
pub(super) fn used_fonts(d: &Document) -> Vec<UsedFont> {
|
||||
let db = designcraft_fonts::FontDb::global().scoped(d.font_scope);
|
||||
let mut used: BTreeMap<(String, String), (usize, std::collections::BTreeSet<char>)> = BTreeMap::new();
|
||||
for_each_story(d, &mut |st| {
|
||||
let ranges = st.para_ranges();
|
||||
for (pi, r) in ranges.iter().enumerate() {
|
||||
let (_, base) = d.styles.resolve_para(&st.paras[pi]);
|
||||
for (rr, fmt) in st.runs() {
|
||||
let n = st.text[rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))].chars().count();
|
||||
if n == 0 && !(r.is_empty() && rr.contains(&r.start)) {
|
||||
continue;
|
||||
}
|
||||
let p = d.styles.resolve_char(&base, fmt);
|
||||
if let Some(f) = d.styles.composite_font(&p.font_family) {
|
||||
let text = st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("");
|
||||
for c in text.chars() {
|
||||
if let Some(e) = f.entry(c) {
|
||||
let entry = used.entry((e.family.clone(), e.style.clone())).or_default();
|
||||
entry.0 += 1;
|
||||
entry.1.insert(c);
|
||||
}
|
||||
type FontTally = BTreeMap<(String, String), (usize, std::collections::BTreeSet<char>)>;
|
||||
|
||||
/// Count the characters of every run of one story against the font each resolves to.
|
||||
fn tally_story(d: &Document, st: &Story, used: &mut FontTally) {
|
||||
let ranges = st.para_ranges();
|
||||
for (pi, r) in ranges.iter().enumerate() {
|
||||
let (_, base) = d.styles.resolve_para(&st.paras[pi]);
|
||||
for (rr, fmt) in st.runs() {
|
||||
let n = st.text[rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))].chars().count();
|
||||
if n == 0 && !(r.is_empty() && rr.contains(&r.start)) {
|
||||
continue;
|
||||
}
|
||||
let p = d.styles.resolve_char(&base, fmt);
|
||||
if let Some(f) = d.styles.composite_font(&p.font_family) {
|
||||
let text = st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("");
|
||||
for c in text.chars() {
|
||||
if let Some(e) = f.entry(c) {
|
||||
let entry = used.entry((e.family.clone(), e.style.clone())).or_default();
|
||||
entry.0 += 1;
|
||||
entry.1.insert(c);
|
||||
}
|
||||
} else {
|
||||
let entry = used.entry((p.font_family, p.font_style)).or_default();
|
||||
entry.0 += n;
|
||||
entry.1.extend(st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("").chars());
|
||||
}
|
||||
} else {
|
||||
let entry = used.entry((p.font_family, p.font_style)).or_default();
|
||||
entry.0 += n;
|
||||
entry.1.extend(st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("").chars());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn used_fonts(d: &Document) -> Vec<UsedFont> {
|
||||
let db = designcraft_fonts::FontDb::global().scoped(d.font_scope);
|
||||
let mut used = FontTally::new();
|
||||
for_each_story(d, &mut |st| tally_story(d, st, &mut used));
|
||||
let mut out: Vec<UsedFont> = used
|
||||
.into_iter()
|
||||
.map(|((family, style), (n, characters))| {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use designcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use super::{Arc, Session, ViewInfo, ime_delete_ranges};
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use super::{Session, doc};
|
||||
|
||||
fn session_with_frame() -> (Session, u64, u64) {
|
||||
let mut s = Session::new();
|
||||
|
||||
+15
-10
@@ -983,6 +983,20 @@ impl<'a, 'f> StoryWriter<'a, 'f> {
|
||||
out
|
||||
}
|
||||
|
||||
/// One table cell with its row and column spans, as `th` or `td`.
|
||||
fn table_cell(&mut self, text: &Story, cell_tag: &str, (rs, cs): (usize, usize), depth: usize, out: &mut String) {
|
||||
let _ = write!(out, "<{cell_tag}");
|
||||
if rs > 1 {
|
||||
let _ = write!(out, " rowspan=\"{rs}\"");
|
||||
}
|
||||
if cs > 1 {
|
||||
let _ = write!(out, " colspan=\"{cs}\"");
|
||||
}
|
||||
out.push('>');
|
||||
self.paragraphs(text, depth, "", out);
|
||||
let _ = writeln!(out, "</{cell_tag}>");
|
||||
}
|
||||
|
||||
/// A table: header rows as `thead`, footer rows as `tfoot`, merged cells as row and column
|
||||
/// spans (kept within their row group).
|
||||
fn table(&mut self, t: &Table, depth: usize, out: &mut String) {
|
||||
@@ -1016,16 +1030,7 @@ impl<'a, 'f> StoryWriter<'a, 'f> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = write!(out, "<{cell_tag}");
|
||||
if rs > 1 {
|
||||
let _ = write!(out, " rowspan=\"{rs}\"");
|
||||
}
|
||||
if cs > 1 {
|
||||
let _ = write!(out, " colspan=\"{cs}\"");
|
||||
}
|
||||
out.push('>');
|
||||
self.paragraphs(&cell.text, depth, "", out);
|
||||
let _ = writeln!(out, "</{cell_tag}>");
|
||||
self.table_cell(&cell.text, cell_tag, (rs, cs), depth, out);
|
||||
}
|
||||
out.push_str("</tr>\n");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use super::{Arc, DOCUMENT_FONTS_FOLDER, FALLBACK_FAMILY, FontDb, FontSource, MAX_DOCUMENT_FONT_BYTES, MAX_DOCUMENT_FONT_FILES};
|
||||
use crate::testing::{font_with, font_with_names, font_with_wws, with_fs_type};
|
||||
|
||||
/// An empty temporary folder unique to `name` and this process.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! CJK fallback follows the character's language (and the script where it is unambiguous).
|
||||
|
||||
use super::*;
|
||||
use super::FontDb;
|
||||
use crate::testing::font_with;
|
||||
|
||||
const HAN: char = '直';
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
//! The synthetic font (Source Sans 3 with `vhea`/`vmtx`, and `VORG` where asked) runs everywhere;
|
||||
//! the checks against Shippori Mincho's own tables run when built with craft-fonts.
|
||||
|
||||
use skrifa::MetadataProvider;
|
||||
use skrifa::raw::TableProvider;
|
||||
|
||||
use super::*;
|
||||
use super::{Arc, FontBytes, FontDb, FontFace, FontSource, GlyphId, LocationRef, Size, make_face};
|
||||
use crate::testing::{font_mapping, font_with, with_base, with_code_pages, with_table_len, with_table_u16, with_vmtx, with_vorg};
|
||||
use crate::{ShapeContext, feature, shape_vertical, shape_with_context};
|
||||
|
||||
|
||||
+62
-51
@@ -440,6 +440,45 @@ impl<'a> Ex<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
/// A `Gradient` element; stop colours missing from the swatches become anonymous colours.
|
||||
fn gradient_el(
|
||||
&mut self,
|
||||
s: &designcraft_color::Swatch,
|
||||
gradient: &designcraft_color::Gradient,
|
||||
colors: &mut Vec<El>,
|
||||
by_value: &mut Vec<(Color, String)>,
|
||||
) -> El {
|
||||
let id = self.sw(&s.name);
|
||||
let mut g = El::new("Gradient")
|
||||
.attr("Self", &id)
|
||||
.attr("Type", if gradient.kind == GradientKind::Radial { "Radial" } else { "Linear" })
|
||||
.attr("Name", &s.name)
|
||||
.attr("ColorEditable", "true")
|
||||
.attr("ColorRemovable", "true")
|
||||
.attr("Visible", "true")
|
||||
.attr("SwatchCreatorID", "7937");
|
||||
for (i, stop) in gradient.stops.iter().enumerate() {
|
||||
let cid = match by_value.iter().find(|(c, _)| *c == stop.color) {
|
||||
Some((_, id)) => id.clone(),
|
||||
None => {
|
||||
let cid = format!("Color/{}", self.fresh());
|
||||
colors.push(color_el(&cid, "$ID/", "Process", &stop.color, "Normal", (true, true, false)));
|
||||
by_value.push((stop.color, cid.clone()));
|
||||
cid
|
||||
}
|
||||
};
|
||||
let mut e = El::new("GradientStop")
|
||||
.attr("Self", format!("{}GradientStop{i}", id.replace(['/', ' '], "_")))
|
||||
.attr("StopColor", cid)
|
||||
.attr("Location", pct(stop.offset as f64));
|
||||
if i > 0 {
|
||||
e.set("Midpoint", pct(gradient.stops[i - 1].midpoint as f64));
|
||||
}
|
||||
g.push(e);
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
// ---------- Graphic.xml ----------
|
||||
|
||||
fn graphic_part(&mut self) -> Vec<u8> {
|
||||
@@ -449,39 +488,21 @@ impl<'a> Ex<'a> {
|
||||
let mut tints: Vec<El> = Vec::new();
|
||||
let mut gradients: Vec<El> = Vec::new();
|
||||
let mut inks: Vec<El> = Vec::new();
|
||||
let color_el = |self_id: &str, name: &str, model: &str, c: &Color, over: &str, editable: bool, removable: bool, visible: bool| {
|
||||
let (space, val) = color_value(c);
|
||||
El::new("Color")
|
||||
.attr("Self", self_id)
|
||||
.attr("Model", model)
|
||||
.attr("Space", space)
|
||||
.attr("ColorValue", val)
|
||||
.attr("ColorOverride", over)
|
||||
.attr("AlternateSpace", "NoAlternateColor")
|
||||
.attr("AlternateColorValue", "")
|
||||
.attr("Name", name)
|
||||
.attr("ColorEditable", bool_s(editable))
|
||||
.attr("ColorRemovable", bool_s(removable))
|
||||
.attr("Visible", bool_s(visible))
|
||||
.attr("SwatchCreatorID", "7937")
|
||||
};
|
||||
// Specials (always present).
|
||||
colors.push(color_el("Color/Black", "Black", "Process", &Color::cmyk(0.0, 0.0, 0.0, 1.0), "Specialblack", false, false, true));
|
||||
colors.push(color_el("Color/Black", "Black", "Process", &Color::cmyk(0.0, 0.0, 0.0, 1.0), "Specialblack", (false, false, true)));
|
||||
let paper = match d.swatch(swatch::PAPER).map(|s| &s.value) {
|
||||
Some(SwatchValue::Paper { color }) => *color,
|
||||
_ => Color::cmyk(0.0, 0.0, 0.0, 0.0),
|
||||
};
|
||||
let paper = if paper == Color::WHITE { Color::cmyk(0.0, 0.0, 0.0, 0.0) } else { paper };
|
||||
colors.push(color_el("Color/Paper", "Paper", "Process", &paper, "Specialpaper", true, false, true));
|
||||
colors.push(color_el("Color/Paper", "Paper", "Process", &paper, "Specialpaper", (true, false, true)));
|
||||
colors.push(color_el(
|
||||
"Color/Registration",
|
||||
"Registration",
|
||||
"Registration",
|
||||
&Color::cmyk(1.0, 1.0, 1.0, 1.0),
|
||||
"Specialregistration",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
(false, false, true),
|
||||
));
|
||||
for (i, ink) in
|
||||
[("Process Cyan", 75, 0.61), ("Process Magenta", 15, 0.76), ("Process Yellow", 0, 0.16), ("Process Black", 45, 1.7)].iter().enumerate()
|
||||
@@ -509,7 +530,7 @@ impl<'a> Ex<'a> {
|
||||
match &s.value {
|
||||
SwatchValue::Color { color, color_type } => {
|
||||
let model = if *color_type == ColorType::Spot { "Spot" } else { "Process" };
|
||||
colors.push(color_el(&id, &s.name, model, color, "Normal", true, true, true));
|
||||
colors.push(color_el(&id, &s.name, model, color, "Normal", (true, true, true)));
|
||||
by_value.push((*color, id.clone()));
|
||||
if *color_type == ColorType::Spot {
|
||||
let mut ink = El::new("Ink");
|
||||
@@ -551,35 +572,7 @@ impl<'a> Ex<'a> {
|
||||
}
|
||||
for s in &d.swatches {
|
||||
let SwatchValue::Gradient { gradient } = &s.value else { continue };
|
||||
let id = self.sw(&s.name);
|
||||
let mut g = El::new("Gradient")
|
||||
.attr("Self", &id)
|
||||
.attr("Type", if gradient.kind == GradientKind::Radial { "Radial" } else { "Linear" })
|
||||
.attr("Name", &s.name)
|
||||
.attr("ColorEditable", "true")
|
||||
.attr("ColorRemovable", "true")
|
||||
.attr("Visible", "true")
|
||||
.attr("SwatchCreatorID", "7937");
|
||||
for (i, stop) in gradient.stops.iter().enumerate() {
|
||||
let cid = match by_value.iter().find(|(c, _)| *c == stop.color) {
|
||||
Some((_, id)) => id.clone(),
|
||||
None => {
|
||||
let cid = format!("Color/{}", self.fresh());
|
||||
colors.push(color_el(&cid, "$ID/", "Process", &stop.color, "Normal", true, true, false));
|
||||
by_value.push((stop.color, cid.clone()));
|
||||
cid
|
||||
}
|
||||
};
|
||||
let mut e = El::new("GradientStop")
|
||||
.attr("Self", format!("{}GradientStop{i}", id.replace(['/', ' '], "_")))
|
||||
.attr("StopColor", cid)
|
||||
.attr("Location", pct(stop.offset as f64));
|
||||
if i > 0 {
|
||||
e.set("Midpoint", pct(gradient.stops[i - 1].midpoint as f64));
|
||||
}
|
||||
g.push(e);
|
||||
}
|
||||
gradients.push(g);
|
||||
gradients.push(self.gradient_el(s, gradient, &mut colors, &mut by_value));
|
||||
}
|
||||
for c in colors {
|
||||
root.push(c);
|
||||
@@ -3294,6 +3287,24 @@ fn layer_el(l: &designcraft_doc::Layer) -> El {
|
||||
with_props(el, vec![color])
|
||||
}
|
||||
|
||||
/// A `Color` element. `flags` is (editable, removable, visible).
|
||||
fn color_el(self_id: &str, name: &str, model: &str, c: &Color, over: &str, (editable, removable, visible): (bool, bool, bool)) -> El {
|
||||
let (space, val) = color_value(c);
|
||||
El::new("Color")
|
||||
.attr("Self", self_id)
|
||||
.attr("Model", model)
|
||||
.attr("Space", space)
|
||||
.attr("ColorValue", val)
|
||||
.attr("ColorOverride", over)
|
||||
.attr("AlternateSpace", "NoAlternateColor")
|
||||
.attr("AlternateColorValue", "")
|
||||
.attr("Name", name)
|
||||
.attr("ColorEditable", bool_s(editable))
|
||||
.attr("ColorRemovable", bool_s(removable))
|
||||
.attr("Visible", bool_s(visible))
|
||||
.attr("SwatchCreatorID", "7937")
|
||||
}
|
||||
|
||||
/// A text variable definition with its type's settings element.
|
||||
fn text_variable_el(v: &designcraft_doc::vars::TextVariable) -> El {
|
||||
use designcraft_doc::vars::{Use, VarKind};
|
||||
|
||||
+121
-96
@@ -919,6 +919,45 @@ impl<'r> Importer<'r> {
|
||||
Some((c, ty))
|
||||
}
|
||||
|
||||
/// Custom stroke styles.
|
||||
fn read_stroke_styles(&mut self, top: &[El]) {
|
||||
for e in top.iter().filter(|e| matches!(e.local(), "StripedStrokeStyle" | "DashedStrokeStyle" | "DottedStrokeStyle")) {
|
||||
let (Some(id), Some(name)) = (e.get("Self"), e.get("Name")) else { continue };
|
||||
let kind = match e.local() {
|
||||
"StripedStrokeStyle" => designcraft_doc::StrokeType::Stripes {
|
||||
bands: nums(e.get("StripeArray").unwrap_or(""))
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| (c[0] / 100.0, (c[1] - c[0]) / 100.0))
|
||||
.collect(),
|
||||
},
|
||||
"DashedStrokeStyle" => designcraft_doc::StrokeType::Dashed { pattern: nums(e.get("DashArray").unwrap_or("")) },
|
||||
_ => designcraft_doc::StrokeType::Dotted,
|
||||
};
|
||||
let _ = id;
|
||||
self.stroke_styles.push(designcraft_doc::StrokeStyleDef { name: name.to_string(), kind });
|
||||
}
|
||||
}
|
||||
|
||||
/// Ink Manager settings: inks converted to process and ink aliases.
|
||||
fn read_inks(&mut self, top: &[El]) {
|
||||
for e in top.iter().filter(|e| e.local() == "Ink") {
|
||||
let Some(name) = e
|
||||
.get("Name")
|
||||
.filter(|n| !n.starts_with("$ID/") && !["Process Cyan", "Process Magenta", "Process Yellow", "Process Black"].contains(n))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if e.get("ConvertToProcess") == Some("true") {
|
||||
self.inks.to_process.push(name.to_string());
|
||||
}
|
||||
if let Some(a) = e.get("AliasInkName").filter(|a| !a.is_empty() && *a != name && !a.starts_with("$ID/")) {
|
||||
self.inks.aliases.push((name.to_string(), a.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn graphics(&mut self, top: &[El]) {
|
||||
let mut order: Vec<(String, Swatch)> = Vec::new();
|
||||
// Colours.
|
||||
@@ -962,39 +1001,8 @@ impl<'r> Importer<'r> {
|
||||
Swatch { name, value: SwatchValue::Tint { base, tint: (t / 100.0) as f32 }, locked: false, named: true, hidden: false },
|
||||
));
|
||||
}
|
||||
// Custom stroke styles.
|
||||
for e in top.iter().filter(|e| matches!(e.local(), "StripedStrokeStyle" | "DashedStrokeStyle" | "DottedStrokeStyle")) {
|
||||
let (Some(id), Some(name)) = (e.get("Self"), e.get("Name")) else { continue };
|
||||
let kind = match e.local() {
|
||||
"StripedStrokeStyle" => designcraft_doc::StrokeType::Stripes {
|
||||
bands: nums(e.get("StripeArray").unwrap_or(""))
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| (c[0] / 100.0, (c[1] - c[0]) / 100.0))
|
||||
.collect(),
|
||||
},
|
||||
"DashedStrokeStyle" => designcraft_doc::StrokeType::Dashed { pattern: nums(e.get("DashArray").unwrap_or("")) },
|
||||
_ => designcraft_doc::StrokeType::Dotted,
|
||||
};
|
||||
let _ = id;
|
||||
self.stroke_styles.push(designcraft_doc::StrokeStyleDef { name: name.to_string(), kind });
|
||||
}
|
||||
// Ink Manager.
|
||||
for e in top.iter().filter(|e| e.local() == "Ink") {
|
||||
let Some(name) = e
|
||||
.get("Name")
|
||||
.filter(|n| !n.starts_with("$ID/") && !["Process Cyan", "Process Magenta", "Process Yellow", "Process Black"].contains(n))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if e.get("ConvertToProcess") == Some("true") {
|
||||
self.inks.to_process.push(name.to_string());
|
||||
}
|
||||
if let Some(a) = e.get("AliasInkName").filter(|a| !a.is_empty() && *a != name && !a.starts_with("$ID/")) {
|
||||
self.inks.aliases.push((name.to_string(), a.to_string()));
|
||||
}
|
||||
}
|
||||
self.read_stroke_styles(top);
|
||||
self.read_inks(top);
|
||||
for e in top.iter().filter(|e| e.local() == "Gradient") {
|
||||
let Some(id) = e.get("Self") else { continue };
|
||||
let mut stops: Vec<GradientStop> = Vec::new();
|
||||
@@ -2249,43 +2257,7 @@ impl<'r> Importer<'r> {
|
||||
cell.style = if cell_style == designcraft_doc::NO_CELL_STYLE { String::new() } else { cell_style };
|
||||
let default_para = cell.text.paras.first().cloned().unwrap_or_default();
|
||||
cell.text = self.story_with_para(StoryId(0), ce, default_para);
|
||||
if let Some(fc) = ce.get("FillColor") {
|
||||
cell.fill = self.swatch_ref(fc);
|
||||
}
|
||||
if let Some(tint) = tint(ce.num("FillTint")) {
|
||||
cell.fill_tint = tint;
|
||||
}
|
||||
for (i, k) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().enumerate() {
|
||||
if let Some(v) = ce.num(k) {
|
||||
cell.insets[i] = v.max(0.0);
|
||||
}
|
||||
}
|
||||
if let Some(vj) = ce.get("VerticalJustification") {
|
||||
cell.vj = names::vj_in(vj);
|
||||
}
|
||||
if let Some(fb) = ce.get("FirstBaselineOffset") {
|
||||
cell.first_baseline = names::first_baseline_in(fb);
|
||||
}
|
||||
if let Some(min) = ce.num("MinimumFirstBaselineOffset").filter(|v| v.is_finite()) {
|
||||
cell.first_baseline_min = min.clamp(0.0, 8640.0);
|
||||
}
|
||||
cell.rotation = ce.num("RotationAngle").unwrap_or(cell.rotation);
|
||||
if cell.rotation != 0.0 {
|
||||
self.rotated_cells += 1;
|
||||
}
|
||||
for (i, side) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().enumerate() {
|
||||
let attrs = self.cell_stroke_attrs(ce, side);
|
||||
attrs.apply_to(&mut cell.strokes[i]);
|
||||
cell.stroke_defined[i] |= !attrs.is_empty();
|
||||
let priority = ce.prop(&format!("{side}StrokePriority")).and_then(|v| v.trim().parse::<i32>().ok()).unwrap_or(0);
|
||||
cell.stroke_priorities[i] = priority;
|
||||
// Preserve style-defined edges and #102's positive-priority local overrides.
|
||||
cell.border_overrides[i] |= priority > 0;
|
||||
// No table or table style border is given anywhere, so the built-in 1 pt default must not
|
||||
// draw over an outer edge the cell explicitly hides (weight 0 or the None swatch), as InDesign draws nothing.
|
||||
let hidden = attrs.weight == Some(0.0) || attrs.color.as_deref() == Some(swatch::NONE);
|
||||
cell.border_overrides[i] |= border_unset && hidden;
|
||||
}
|
||||
self.read_cell_attrs(&mut cell, ce, border_unset);
|
||||
if let Some(slot) = t.cell_mut(r, c) {
|
||||
*slot = cell;
|
||||
}
|
||||
@@ -2299,6 +2271,47 @@ impl<'r> Importer<'r> {
|
||||
t
|
||||
}
|
||||
|
||||
/// A cell's local fill, insets, alignment, rotation and edge strokes.
|
||||
fn read_cell_attrs(&mut self, cell: &mut designcraft_doc::Cell, ce: &El, border_unset: bool) {
|
||||
if let Some(fc) = ce.get("FillColor") {
|
||||
cell.fill = self.swatch_ref(fc);
|
||||
}
|
||||
if let Some(tint) = tint(ce.num("FillTint")) {
|
||||
cell.fill_tint = tint;
|
||||
}
|
||||
for (i, k) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().enumerate() {
|
||||
if let Some(v) = ce.num(k) {
|
||||
cell.insets[i] = v.max(0.0);
|
||||
}
|
||||
}
|
||||
if let Some(vj) = ce.get("VerticalJustification") {
|
||||
cell.vj = names::vj_in(vj);
|
||||
}
|
||||
if let Some(fb) = ce.get("FirstBaselineOffset") {
|
||||
cell.first_baseline = names::first_baseline_in(fb);
|
||||
}
|
||||
if let Some(min) = ce.num("MinimumFirstBaselineOffset").filter(|v| v.is_finite()) {
|
||||
cell.first_baseline_min = min.clamp(0.0, 8640.0);
|
||||
}
|
||||
cell.rotation = ce.num("RotationAngle").unwrap_or(cell.rotation);
|
||||
if cell.rotation != 0.0 {
|
||||
self.rotated_cells += 1;
|
||||
}
|
||||
for (i, side) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().enumerate() {
|
||||
let attrs = self.cell_stroke_attrs(ce, side);
|
||||
attrs.apply_to(&mut cell.strokes[i]);
|
||||
cell.stroke_defined[i] |= !attrs.is_empty();
|
||||
let priority = ce.prop(&format!("{side}StrokePriority")).and_then(|v| v.trim().parse::<i32>().ok()).unwrap_or(0);
|
||||
cell.stroke_priorities[i] = priority;
|
||||
// Preserve style-defined edges and #102's positive-priority local overrides.
|
||||
cell.border_overrides[i] |= priority > 0;
|
||||
// No table or table style border is given anywhere, so the built-in 1 pt default must not
|
||||
// draw over an outer edge the cell explicitly hides (weight 0 or the None swatch), as InDesign draws nothing.
|
||||
let hidden = attrs.weight == Some(0.0) || attrs.color.as_deref() == Some(swatch::NONE);
|
||||
cell.border_overrides[i] |= border_unset && hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/// The format of the text in a `CharacterStyleRange`: its style, and its local attributes over
|
||||
/// the enclosing paragraph range's, inside a tracked change if `outer` (the enclosing format) is.
|
||||
fn range_format(&mut self, c: &El, pchars: &CharAttrs, outer: &CharFormat) -> CharFormat {
|
||||
@@ -2530,10 +2543,9 @@ impl<'r> Importer<'r> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn spread(&mut self, e: &El, id: SpreadId, master: bool) -> Spread {
|
||||
let facing = self.settings.facing_pages;
|
||||
// Page rectangles in IDML spread coordinates.
|
||||
let mut pages: Vec<(Rect, &El)> = Vec::new();
|
||||
/// The page rectangles of a spread element, in IDML spread coordinates.
|
||||
fn page_rects<'a>(&self, e: &'a El) -> Vec<(Rect, &'a El)> {
|
||||
let mut pages: Vec<(Rect, &'a El)> = Vec::new();
|
||||
for p in e.find_all("Page") {
|
||||
let gb = nums(p.get("GeometricBounds").unwrap_or(""));
|
||||
let (t, l, bt, r) =
|
||||
@@ -2543,6 +2555,40 @@ impl<'r> Importer<'r> {
|
||||
let c = xf * Point::new(r, bt);
|
||||
pages.push((Rect::from_points(a, c), p));
|
||||
}
|
||||
pages
|
||||
}
|
||||
|
||||
/// A page's margins and columns, and whether they are local (differ from its parent page's).
|
||||
fn page_margins(&self, p: &El, r: &Rect, master: bool, parent: Option<SpreadId>, side: PageSide) -> (Margins, Columns, bool) {
|
||||
// A document page's margins are local when they differ from its parent page's; a
|
||||
// page without its own takes the parent page's.
|
||||
let from_parent = if master {
|
||||
None
|
||||
} else {
|
||||
parent
|
||||
.and_then(|id| self.parents.iter().find(|sp| sp.id == id))
|
||||
.and_then(|sp| sp.pages.get(sp.parent_page_for_side(side)?))
|
||||
.map(|q| (q.margins, q.columns.clone()))
|
||||
};
|
||||
let (margins, columns) = match (p.find("MarginPreference"), &from_parent) {
|
||||
(Some(m), _) => {
|
||||
let (margins, columns) = margins_of(m);
|
||||
(margins, fit_columns(columns, &margins, r.width()))
|
||||
}
|
||||
(None, Some(own)) => own.clone(),
|
||||
(None, None) => {
|
||||
let (margins, columns) = self.default_margins.clone().unwrap_or((Margins::uniform(36.0), Columns::default()));
|
||||
(margins, fit_columns(columns, &margins, r.width()))
|
||||
}
|
||||
};
|
||||
let local_margins = from_parent.is_some_and(|(m, c)| m != margins || c != columns);
|
||||
(margins, columns, local_margins)
|
||||
}
|
||||
|
||||
fn spread(&mut self, e: &El, id: SpreadId, master: bool) -> Spread {
|
||||
let facing = self.settings.facing_pages;
|
||||
// Page rectangles in IDML spread coordinates.
|
||||
let pages = self.page_rects(e);
|
||||
let min_x = pages.iter().map(|(r, _)| r.x0).fold(f64::INFINITY, f64::min);
|
||||
let min_y = pages.iter().map(|(r, _)| r.y0).fold(f64::INFINITY, f64::min);
|
||||
let (min_x, min_y) = if min_x.is_finite() { (min_x, min_y) } else { (0.0, 0.0) };
|
||||
@@ -2569,28 +2615,7 @@ impl<'r> Importer<'r> {
|
||||
PageSide::Right
|
||||
};
|
||||
let parent = p.get("AppliedMaster").and_then(|m| self.parent_ids.get(m)).copied();
|
||||
// A document page's margins are local when they differ from its parent page's; a
|
||||
// page without its own takes the parent page's.
|
||||
let from_parent = if master {
|
||||
None
|
||||
} else {
|
||||
parent
|
||||
.and_then(|id| self.parents.iter().find(|sp| sp.id == id))
|
||||
.and_then(|sp| sp.pages.get(sp.parent_page_for_side(side)?))
|
||||
.map(|q| (q.margins, q.columns.clone()))
|
||||
};
|
||||
let (margins, columns) = match (p.find("MarginPreference"), &from_parent) {
|
||||
(Some(m), _) => {
|
||||
let (margins, columns) = margins_of(m);
|
||||
(margins, fit_columns(columns, &margins, r.width()))
|
||||
}
|
||||
(None, Some(own)) => own.clone(),
|
||||
(None, None) => {
|
||||
let (margins, columns) = self.default_margins.clone().unwrap_or((Margins::uniform(36.0), Columns::default()));
|
||||
(margins, fit_columns(columns, &margins, r.width()))
|
||||
}
|
||||
};
|
||||
let local_margins = from_parent.is_some_and(|(m, c)| m != margins || c != columns);
|
||||
let (margins, columns, local_margins) = self.page_margins(p, r, master, parent, side);
|
||||
out_pages.push(Page {
|
||||
id: pid,
|
||||
width: r.width(),
|
||||
|
||||
@@ -88,7 +88,7 @@ impl El {
|
||||
if let Some(v) = self.get(k) {
|
||||
return Some(v.to_string());
|
||||
}
|
||||
self.prop_el(k).map(|e| e.text_content())
|
||||
self.prop_el(k).map(El::text_content)
|
||||
}
|
||||
pub fn prop_el(&self, k: &str) -> Option<&El> {
|
||||
self.find("Properties").and_then(|p| p.find(k))
|
||||
|
||||
@@ -8,7 +8,7 @@ use designcraft_doc::{Align, Document, ParaAttrs, ParaFormat, SpreadRef, TextDir
|
||||
use designcraft_geom::Rect;
|
||||
|
||||
use crate::tests::extract_text;
|
||||
use crate::*;
|
||||
use crate::{PdfOptions, export_pdf};
|
||||
|
||||
const HEBREW: &str = "DC Test PDF Hebrew";
|
||||
const ARABIC: &str = "DC Test PDF Arabic";
|
||||
|
||||
+29
-24
@@ -233,6 +233,34 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// One line's glyphs as tagged runs; a glyph that is turned or slanted gets a transform of its own.
|
||||
fn line_glyphs(&mut self, s: &mut Surface, cs: &ComposedStory, ft: &FrameText, l: &designcraft_compose::Line, story: &str) {
|
||||
let gs = &l.glyphs;
|
||||
let mut i = 0;
|
||||
while i < gs.len() {
|
||||
let g = &gs[i];
|
||||
if !g.visible || g.sx <= 0.0 || g.sy <= 0.0 {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
// Vertical type: an upright glyph turns back a quarter about its em box centre.
|
||||
// Shatai slants each glyph about its own em box centre first.
|
||||
let turn = if ft.vertical { g.vertical_xf(l.baseline) } else { None };
|
||||
let shatai = cs.styles.get(g.style as usize).and_then(|st| st.shatai_xf(g, l.baseline));
|
||||
if turn.is_some() || shatai.is_some() {
|
||||
let m = turn.unwrap_or(Affine::IDENTITY) * shatai.unwrap_or(Affine::IDENTITY);
|
||||
s.push_transform(&crate::export::tf(m));
|
||||
self.tagged_run(s, cs, &gs[i..i + 1], l.baseline, story);
|
||||
s.pop();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let j = run_end(ft, gs, i);
|
||||
self.tagged_run(s, cs, &gs[i..j], l.baseline, story);
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn frame_text(&mut self, s: &mut Surface, cs: &ComposedStory, ft: &FrameText, story: &str) {
|
||||
// Paragraph shading and rules under the text.
|
||||
self.as_artifact(s, |me, s| {
|
||||
@@ -260,30 +288,7 @@ impl Exporter<'_> {
|
||||
});
|
||||
}
|
||||
for l in &ft.lines {
|
||||
let gs = &l.glyphs;
|
||||
let mut i = 0;
|
||||
while i < gs.len() {
|
||||
let g = &gs[i];
|
||||
if !g.visible || g.sx <= 0.0 || g.sy <= 0.0 {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
// Vertical type: an upright glyph turns back a quarter about its em box centre.
|
||||
// Shatai slants each glyph about its own em box centre first.
|
||||
let turn = if ft.vertical { g.vertical_xf(l.baseline) } else { None };
|
||||
let shatai = cs.styles.get(g.style as usize).and_then(|st| st.shatai_xf(g, l.baseline));
|
||||
if turn.is_some() || shatai.is_some() {
|
||||
let m = turn.unwrap_or(Affine::IDENTITY) * shatai.unwrap_or(Affine::IDENTITY);
|
||||
s.push_transform(&crate::export::tf(m));
|
||||
self.tagged_run(s, cs, &gs[i..i + 1], l.baseline, story);
|
||||
s.pop();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let j = run_end(ft, gs, i);
|
||||
self.tagged_run(s, cs, &gs[i..j], l.baseline, story);
|
||||
i = j;
|
||||
}
|
||||
self.line_glyphs(s, cs, ft, l, story);
|
||||
}
|
||||
self.as_artifact(s, |me, s| {
|
||||
for (rule, r) in text_rules(cs, ft, false) {
|
||||
|
||||
@@ -173,7 +173,7 @@ pub fn records(bytes: &[u8], sheet: Option<&str>) -> Result<Vec<Vec<String>>, Im
|
||||
extra.push(v.clone());
|
||||
}
|
||||
}
|
||||
if *r != header_row && row.iter().all(|c| c.is_empty()) && extra.is_empty() {
|
||||
if *r != header_row && row.iter().all(String::is_empty) && extra.is_empty() {
|
||||
continue;
|
||||
}
|
||||
row.extend(extra);
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
use egui_kittest::Harness;
|
||||
|
||||
use super::*;
|
||||
use super::{
|
||||
CanvasLayout, DesignApp, GuideDrag, GuidePress, Mods, Overlay, Point, Pos2, RULER, SpreadRef, ToolKey, Xf, guide_drag_delta, guide_spot, json,
|
||||
moving_guides, pos2, vec2,
|
||||
};
|
||||
|
||||
fn harness(app: DesignApp) -> Harness<'static, DesignApp> {
|
||||
let mut h = Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
CANVAS, Column, DOCK_LABELS, DesignApp, Floating, ICON_GROUPS, Layout, Node, PanelContent, Placed, Side, Value, area_id, cancel_gesture,
|
||||
close_visible, default_layout, dock_columns, hit_id, icon_column, in_iconic_column, json, layout_of, modal_open, open_window_drops, panel_menu,
|
||||
set_iconic, showing, tab_group, tab_id, valid, window_groups,
|
||||
};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{CanvasLayout, Color32, DesignApp, Point, Pos2, Rect, View, Xf, json, render_texture, vec2};
|
||||
|
||||
fn app_with_document_object() -> DesignApp {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
Reference in new issue
Block a user