mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
H: place a chosen PDF page (Image Import Options)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1b778945af
commit
175aaf28fd
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
- View › Overprint Preview (⌥⇧⌘Y): overprinting fills, strokes and 100% [Black] text mix with what's beneath on screen.
|
||||
- Type on a Path (⇧T tool, `type.onPath`): a story along any line, curve or shape on screen and in PDF; Type on a Path Options (start, flip, baseline/ascender/descender/centre) and Delete Type from Path. Round-trips through IDML as TextPath; on-path caret drawing is still to come.
|
||||
|
||||
@@ -183,6 +183,7 @@ pub fn synthetic(spec: &Spec) -> Document {
|
||||
d.assets.insert(
|
||||
id,
|
||||
Arc::new(Asset {
|
||||
page: 0,
|
||||
id,
|
||||
name: format!("art-{k}.png"),
|
||||
mime: "image/png".into(),
|
||||
|
||||
@@ -249,6 +249,13 @@ pub struct Asset {
|
||||
/// Pixel size for raster images.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pixels: Option<(u32, u32)>,
|
||||
/// The page shown from a multi-page PDF (0-based; Image Import Options).
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
pub page: u32,
|
||||
}
|
||||
|
||||
fn is_zero(v: &u32) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -77,7 +77,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Place…",
|
||||
["File"],
|
||||
Some("Cmd+D"),
|
||||
"{path?|base64?, name?, frame?: id (place into), spread?, x?, y?, width?} — places an image (into the selected empty frame if any); text files (.txt, .docx, .rtf, .md) go into the insertion point, the selected frame or a new frame on `page`/`rect` — {autoflow?: adds pages with threaded frames until the text fits, removeStyles?}",
|
||||
"{path?|base64?, name?, frame?: id (place into), spread?, x?, y?, width?, pdfPage?: n (1-based, Image Import Options)} — places an image (into the selected empty frame if any); text files (.txt, .docx, .rtf, .md) go into the insertion point, the selected frame or a new frame on `page`/`rect` — {autoflow?: adds pages with threaded frames until the text fits, removeStyles?}",
|
||||
has_doc,
|
||||
file_place
|
||||
),
|
||||
@@ -203,7 +203,23 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if designcraft_textimport::is_text_file(&name) {
|
||||
return super::place_text::place_text(s, p, &name, &bytes);
|
||||
}
|
||||
let (pw, ph) = designcraft_render::image_size(&bytes).ok_or_else(|| bad("file.place", "unsupported or corrupt image"))?;
|
||||
// Image Import Options: which page of a PDF (1-based).
|
||||
let pdf_page = match p.get("pdfPage").and_then(Value::as_u64) {
|
||||
Some(n) if designcraft_render::is_pdf(&bytes) => {
|
||||
let count = designcraft_render::pdf_page_count(&bytes).unwrap_or(1);
|
||||
if n == 0 || n as usize > count {
|
||||
return Err(bad("file.place", format!("the PDF has {count} page(s)")));
|
||||
}
|
||||
n as u32 - 1
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let (pw, ph) = match pdf_page {
|
||||
0 => designcraft_render::image_size(&bytes).ok_or_else(|| bad("file.place", "unsupported or corrupt image"))?,
|
||||
n => designcraft_render::pdf_page_size(&bytes, n as usize)
|
||||
.map(|(w, h)| (w.round().max(1.0) as u32, h.round().max(1.0) as u32))
|
||||
.ok_or_else(|| bad("file.place", "can't read that PDF page"))?,
|
||||
};
|
||||
// 72 ppi by default unless the file says otherwise; scale so it fits the page when huge.
|
||||
let (nw, nh) = (pw as f64, ph as f64);
|
||||
let target_frame = super::id_param(p, "frame").or_else(|| {
|
||||
@@ -218,7 +234,7 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(|d, sel| {
|
||||
let aid = AssetId(d.alloc());
|
||||
let mime = designcraft_render::image_mime(&bytes).to_string();
|
||||
d.assets.insert(aid, Arc::new(Asset { id: aid, name, mime, link, data: Arc::new(bytes), pixels: Some((pw, ph)) }));
|
||||
d.assets.insert(aid, Arc::new(Asset { page: pdf_page, id: aid, name, mime, link, data: Arc::new(bytes), pixels: Some((pw, ph)) }));
|
||||
let (w, h) = match want_w {
|
||||
Some(w) => (w, w * nh / nw),
|
||||
None => {
|
||||
@@ -316,8 +332,10 @@ fn place_load(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let aid = s.edit(|d, _| {
|
||||
let aid = AssetId(d.alloc());
|
||||
let mime = designcraft_render::image_mime(&bytes).to_string();
|
||||
d.assets
|
||||
.insert(aid, Arc::new(Asset { id: aid, name: name.clone(), mime, link: link.clone(), data: Arc::new(bytes), pixels: Some((pw, ph)) }));
|
||||
d.assets.insert(
|
||||
aid,
|
||||
Arc::new(Asset { page: 0, id: aid, name: name.clone(), mime, link: link.clone(), data: Arc::new(bytes), pixels: Some((pw, ph)) }),
|
||||
);
|
||||
Ok(aid)
|
||||
})?;
|
||||
s.loaded = Some((aid, (pw as f64, ph as f64)));
|
||||
|
||||
@@ -290,3 +290,39 @@ mod reflow_tests {
|
||||
assert_eq!(s.doc().unwrap().doc.page_count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pdf_page_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn place_a_chosen_pdf_page() {
|
||||
let dir = std::env::temp_dir().join(format!("dc-pdf-page-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let src = dir.join("two.pdf");
|
||||
// Page 1 white, page 2 a different size with a black square.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 2, "facingPages": false, "width": 200, "height": 100})).unwrap();
|
||||
s.execute("layout.pageSize", &json!({"pages": [2], "width": 300, "height": 150})).unwrap();
|
||||
let id = s.execute("frame.create", &json!({"spread": 1, "rect": [0, 0, 300, 150]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.fill", &json!({"ids": [id], "swatch": "[Black]"})).unwrap();
|
||||
s.execute("file.exportPdf", &json!({"path": src.to_string_lossy()})).unwrap();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
assert!(s.execute("file.place", &json!({"path": src.to_string_lossy(), "pdfPage": 3})).is_err());
|
||||
s.execute("file.place", &json!({"path": src.to_string_lossy(), "pdfPage": 2, "x": 100, "y": 100})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let it = d.spreads[0].items.last().unwrap();
|
||||
let b = it.bounds();
|
||||
assert!((b.width() - 300.0).abs() < 1.0 && (b.height() - 150.0).abs() < 1.0, "page 2's size: {b:?}");
|
||||
assert_eq!(d.assets.values().next().unwrap().page, 1);
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
rr.threads = 0;
|
||||
let img = rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap();
|
||||
assert!(img.pixel(250, 170)[0] < 60, "page 2 (black) shows: {:?}", img.pixel(250, 170));
|
||||
let pdf = designcraft_pdf::export_pdf(&d, &s.cache, &Default::default()).unwrap();
|
||||
assert!(String::from_utf8_lossy(&pdf).contains("/Subtype /Form") || String::from_utf8_lossy(&pdf).contains("/Subtype/Form"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ fn para_style(name: &str, based: Option<&str>, para: ParaAttrs, chars: CharAttrs
|
||||
fn image(d: &mut Document, name: &str, w: u32, h: u32, hue: f32) -> AssetId {
|
||||
let id = AssetId(d.alloc());
|
||||
let data = art_png(w, h, hue);
|
||||
d.assets.insert(id, Arc::new(Asset { id, name: name.into(), mime: "image/png".into(), link: None, data: Arc::new(data), pixels: Some((w, h)) }));
|
||||
d.assets.insert(
|
||||
id,
|
||||
Arc::new(Asset { page: 0, id, name: name.into(), mime: "image/png".into(), link: None, data: Arc::new(data), pixels: Some((w, h)) }),
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ mod tests {
|
||||
d.assets.insert(
|
||||
aid,
|
||||
Arc::new(Asset {
|
||||
page: 0,
|
||||
id: aid,
|
||||
name: "x.png".into(),
|
||||
mime: "image/png".into(),
|
||||
|
||||
@@ -1851,7 +1851,7 @@ impl<'r> Importer<'r> {
|
||||
uri.as_deref().and_then(|p| p.rsplit(['/', '\\']).next()).filter(|n| !n.is_empty()).map(str::to_string).unwrap_or_else(|| "image".into());
|
||||
let id = AssetId(self.alloc());
|
||||
let link_path = uri.filter(|p| p.contains('/') || p.contains('\\'));
|
||||
self.assets.insert(id, Arc::new(Asset { id, name, mime, link: link_path, data: Arc::new(data), pixels }));
|
||||
self.assets.insert(id, Arc::new(Asset { page: 0, id, name, mime, link: link_path, data: Arc::new(data), pixels }));
|
||||
Content::Graphic(designcraft_doc::Graphic { asset: id, size, xf: gxf * Affine::translate((l, t)), auto_fit: Default::default() })
|
||||
}
|
||||
|
||||
|
||||
@@ -747,7 +747,7 @@ impl Exporter<'_> {
|
||||
match doc {
|
||||
Some(doc) => {
|
||||
s.push_transform(&tf(g.xf));
|
||||
s.draw_pdf_page(&doc, size, 0);
|
||||
s.draw_pdf_page(&doc, size, asset.page as usize);
|
||||
s.pop();
|
||||
}
|
||||
None => self.warn(format!("{}: can't read the placed PDF", asset.name)),
|
||||
|
||||
+18
-17
@@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use vello_cpu::Pixmap;
|
||||
|
||||
use crate::{decode_pixmap, halve};
|
||||
use crate::{decode_pixmap_page, halve};
|
||||
|
||||
/// Decoded pixels kept (bytes).
|
||||
const BUDGET: usize = 768 << 20;
|
||||
@@ -23,11 +23,12 @@ struct Entry {
|
||||
|
||||
#[derive(Default)]
|
||||
struct Cache {
|
||||
map: HashMap<(usize, u8), Entry>,
|
||||
/// (bytes address, PDF page, mip level).
|
||||
map: HashMap<(usize, u32, u8), Entry>,
|
||||
bytes: usize,
|
||||
clock: u64,
|
||||
/// Assets that failed to decode (don't retry every frame).
|
||||
bad: HashMap<usize, Arc<Vec<u8>>>,
|
||||
bad: HashMap<(usize, u32), Arc<Vec<u8>>>,
|
||||
}
|
||||
|
||||
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
|
||||
@@ -41,7 +42,7 @@ fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
|
||||
f(g.get_or_insert_with(Cache::default))
|
||||
}
|
||||
|
||||
fn lookup(key: (usize, u8)) -> Option<Arc<Pixmap>> {
|
||||
fn lookup(key: (usize, u32, u8)) -> Option<Arc<Pixmap>> {
|
||||
with(|c| {
|
||||
c.clock += 1;
|
||||
let clock = c.clock;
|
||||
@@ -52,17 +53,17 @@ fn lookup(key: (usize, u8)) -> Option<Arc<Pixmap>> {
|
||||
})
|
||||
}
|
||||
|
||||
fn store(bytes: &Arc<Vec<u8>>, level: u8, pm: Arc<Pixmap>) {
|
||||
fn store(bytes: &Arc<Vec<u8>>, page: u32, level: u8, pm: Arc<Pixmap>) {
|
||||
with(|c| {
|
||||
c.clock += 1;
|
||||
let key = (Arc::as_ptr(bytes) as usize, level);
|
||||
let key = (Arc::as_ptr(bytes) as usize, page, level);
|
||||
let n = size_of(&pm);
|
||||
if let Some(old) = c.map.insert(key, Entry { _bytes: bytes.clone(), pm, stamp: c.clock }) {
|
||||
c.bytes -= size_of(&old.pm);
|
||||
}
|
||||
c.bytes += n;
|
||||
if c.bytes > BUDGET {
|
||||
let mut v: Vec<(u64, (usize, u8))> = c.map.iter().map(|(k, e)| (e.stamp, *k)).collect();
|
||||
let mut v: Vec<(u64, (usize, u32, u8))> = c.map.iter().map(|(k, e)| (e.stamp, *k)).collect();
|
||||
v.sort_unstable();
|
||||
for (_, k) in v {
|
||||
if c.bytes <= BUDGET * 3 / 4 {
|
||||
@@ -80,23 +81,23 @@ fn store(bytes: &Arc<Vec<u8>>, level: u8, pm: Arc<Pixmap>) {
|
||||
}
|
||||
|
||||
/// Full-resolution decode of `bytes` (cached).
|
||||
pub fn decoded(bytes: &Arc<Vec<u8>>) -> Option<Arc<Pixmap>> {
|
||||
pub fn decoded(bytes: &Arc<Vec<u8>>, page: u32) -> Option<Arc<Pixmap>> {
|
||||
let id = Arc::as_ptr(bytes) as usize;
|
||||
if let Some(pm) = lookup((id, 0)) {
|
||||
if let Some(pm) = lookup((id, page, 0)) {
|
||||
return Some(pm);
|
||||
}
|
||||
if with(|c| c.bad.contains_key(&id)) {
|
||||
if with(|c| c.bad.contains_key(&(id, page))) {
|
||||
return None;
|
||||
}
|
||||
// Decode outside the lock: other threads keep drawing cached images meanwhile.
|
||||
match decode_pixmap(bytes) {
|
||||
match decode_pixmap_page(bytes, page) {
|
||||
Some(pm) => {
|
||||
let pm = Arc::new(pm);
|
||||
store(bytes, 0, pm.clone());
|
||||
store(bytes, page, 0, pm.clone());
|
||||
Some(pm)
|
||||
}
|
||||
None => {
|
||||
with(|c| c.bad.insert(id, bytes.clone()));
|
||||
with(|c| c.bad.insert((id, page), bytes.clone()));
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -104,19 +105,19 @@ pub fn decoded(bytes: &Arc<Vec<u8>>) -> Option<Arc<Pixmap>> {
|
||||
|
||||
/// The mip level of `bytes` closest above `on_screen` device pixels per point for a graphic
|
||||
/// `width_pt` points wide.
|
||||
pub fn mip(bytes: &Arc<Vec<u8>>, width_pt: f64, on_screen: f64) -> Option<Arc<Pixmap>> {
|
||||
let full = decoded(bytes)?;
|
||||
pub fn mip(bytes: &Arc<Vec<u8>>, page: u32, width_pt: f64, on_screen: f64) -> Option<Arc<Pixmap>> {
|
||||
let full = decoded(bytes, page)?;
|
||||
let ppt = full.width() as f64 / width_pt.max(1e-6);
|
||||
let id = Arc::as_ptr(bytes) as usize;
|
||||
let mut pm = full;
|
||||
let mut level = 0u8;
|
||||
while ppt / (1u64 << (level + 1)) as f64 >= on_screen * 1.2 && pm.width() > 64 && pm.height() > 64 && level < 8 {
|
||||
level += 1;
|
||||
pm = match lookup((id, level)) {
|
||||
pm = match lookup((id, page, level)) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let half = Arc::new(halve(&pm));
|
||||
store(bytes, level, half.clone());
|
||||
store(bytes, page, level, half.clone());
|
||||
half
|
||||
}
|
||||
};
|
||||
|
||||
@@ -589,7 +589,7 @@ impl Renderer {
|
||||
// Pick a mip level close to the on-screen size (Typical: a 72 ppi proxy).
|
||||
let on_screen = (f.view * xf * g.xf).determinant().abs().sqrt();
|
||||
let on_screen = if f.opts.quality == DisplayQuality::Typical { on_screen.min(1.0) } else { on_screen };
|
||||
let Some(pm) = images::mip(&asset.data, g.size.0, on_screen) else { return };
|
||||
let Some(pm) = images::mip(&asset.data, asset.page, g.size.0, on_screen) else { return };
|
||||
let rect = Rect::new(0.0, 0.0, g.size.0, g.size.1);
|
||||
ctx.set_transform(f.view * xf * g.xf);
|
||||
let sx = g.size.0 / pm.width().max(1) as f64;
|
||||
@@ -696,8 +696,13 @@ pub fn blend_mode(b: DcBlend) -> BlendMode {
|
||||
|
||||
/// Decode encoded image bytes into a premultiplied pixmap.
|
||||
pub fn decode_pixmap(bytes: &[u8]) -> Option<Pixmap> {
|
||||
decode_pixmap_page(bytes, 0)
|
||||
}
|
||||
|
||||
/// [`decode_pixmap`] for page `page` of a PDF (other formats have one page).
|
||||
pub fn decode_pixmap_page(bytes: &[u8], page: u32) -> Option<Pixmap> {
|
||||
if is_pdf(bytes) {
|
||||
return render_pdf_page(bytes, 0, 3000);
|
||||
return render_pdf_page(bytes, page as usize, 3000);
|
||||
}
|
||||
if designcraft_images::is_svg(bytes) {
|
||||
let (px, w, h) = designcraft_images::render_svg(bytes, 3000)?;
|
||||
@@ -933,6 +938,7 @@ mod tests {
|
||||
d.assets.insert(
|
||||
aid,
|
||||
Arc::new(designcraft_doc::Asset {
|
||||
page: 0,
|
||||
id: aid,
|
||||
name: "red.png".into(),
|
||||
mime: "image/png".into(),
|
||||
|
||||
@@ -558,6 +558,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
"polygonSettings" => "Polygon Settings",
|
||||
"preferences" => "Preferences",
|
||||
"print" => "Print",
|
||||
"pdfImport" => "Place PDF",
|
||||
"keyboardShortcuts" => "Keyboard Shortcuts",
|
||||
"colorPicker" => "Color Picker",
|
||||
id => match id.strip_prefix("cmd:").and_then(designcraft_engine::find_command) {
|
||||
@@ -697,6 +698,13 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
"colorPicker" => color_picker(ui, &mut d),
|
||||
"preferences" => preferences(ui, &mut d),
|
||||
"print" => print_dialog(ui, &mut d),
|
||||
"pdfImport" => {
|
||||
ui.label(egui::RichText::new(d.s("path")).size(11.0));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Page (1–{}):", d.n("pages").unwrap_or(1.0)));
|
||||
text_field(ui, &mut d, "page", 60.0);
|
||||
});
|
||||
}
|
||||
"keyboardShortcuts" => keyboard_shortcuts(app, ui, &mut d),
|
||||
"polygonSettings" => {
|
||||
egui::Grid::new("poly").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
@@ -858,6 +866,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
let hex = d.s("hex");
|
||||
app.run("object.color", json!({"color": hex, "target": d.s("target")}))
|
||||
}
|
||||
"pdfImport" => app.run("file.place", json!({"path": d.s("path"), "pdfPage": d.n("page").unwrap_or(1.0).max(1.0) as u64})),
|
||||
"print" => {
|
||||
let pages = if d.s("range") == "all" { Value::Null } else { json!(d.s("pages")) };
|
||||
let printer = d.s("printer");
|
||||
|
||||
@@ -354,6 +354,17 @@ impl DesignApp {
|
||||
let cmd = if purpose == "place" { "file.place" } else { "file.open" };
|
||||
if let Some(pick) = self.services.pick_open.as_mut() {
|
||||
return match pick(purpose) {
|
||||
// Image Import Options: a multi-page PDF asks which page.
|
||||
Some(path) if purpose == "place" && path.to_ascii_lowercase().ends_with(".pdf") => {
|
||||
let pages =
|
||||
self.services.read.as_mut().and_then(|r| r(&path).ok()).and_then(|b| designcraft_render::pdf_page_count(&b)).unwrap_or(1);
|
||||
if pages > 1 {
|
||||
self.ui.dialog = Some(dialogs::Dialog::new("pdfImport", json!({"path": path, "page": "1", "pages": pages})));
|
||||
Ok(Value::Null)
|
||||
} else {
|
||||
self.run(cmd, json!({"path": path}))
|
||||
}
|
||||
}
|
||||
Some(path) => self.run(cmd, json!({"path": path})),
|
||||
None => Ok(Value::Null),
|
||||
};
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 65 | 25 | 7 | 80% |
|
||||
| P1 | 97 | 65 | 26 | 6 | 80% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
@@ -22,7 +22,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Transform | 93% |
|
||||
| Fill, stroke, colour | 81% |
|
||||
| Effects & transparency | 50% |
|
||||
| Placing & links | 78% |
|
||||
| Placing & links | 81% |
|
||||
| Type & text frames | 85% |
|
||||
| Typography | 85% |
|
||||
| Styles | 92% |
|
||||
@@ -177,7 +177,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Placing & links | Relink to folder / extension | P1 | D |
|
||||
| Placing & links | Copy Links To / Package | P1 | D |
|
||||
| Placing & links | Captions | P2 | M |
|
||||
| Placing & links | Image import options | P1 | M |
|
||||
| Placing & links | Image import options | P1 | P |
|
||||
| Placing & links | Linked text/stories | P2 | M |
|
||||
| Placing & links | Display performance | P0 | D |
|
||||
| Placing & links | Video/audio | P2 | M |
|
||||
|
||||
Reference in New Issue
Block a user