Files
vectorcraft/crates/engine/src/cmd/place/dcs.rs
T
dotCooCoo 99f896f9b8 Place: DCS links follow their plate files, clips and document swatches
- The link watch and links.updateChanged look at a DCS file's plate files
  of their own, so a changed plate file is read again.
- links.update and links.relink apply the file's clipping path to the
  images in symbols, graph designs, patterns and masks too. When the new
  file has no clipping path, a clip group holding the old file's clipping
  path loses that path and stays as a plain group of the image, keeping
  its id, name, visibility and transparency. The image keeps that path with its plates to tell it from a
  clipping mask made in the document; a clipping path given another fill
  rule in the document stays. An even-odd clipping path in the file is
  kept as the same area under the nonzero rule.
- links.update and links.relink return the swatch warnings file.place
  gives, including two files that add one ink in two colors.
- links.relink with copy fails for a DCS file with plate files of its own.
- file.place answers the image id of a clipped DCS placement as image,
  and checks page and crop for a DCS file as it does for an EPS file.
- Print separations and Separations Preview take a spot plate's color
  from the document's swatch of its ink, and a reduced image's plates
  keep their coverage.
- Show DCS Transparency Interactions counts a transparent DCS image and
  characters filled or stroked with a transparent gradient.
- %%PlateFile names keep their spaces, and header bytes that are not
  UTF-8 are read as Latin-1.
- A plate program that draws more than one image, art besides its image,
  or its image through a clipping path is an error naming the plate
  instead of printing the wrong ink.
- Nested clipping paths around the composite clip the image together.
- A kept image with more plates than are read, or more TIFF directories
  than its composite and plates, is the CMYK image of its composite
  only, not a set of plates cut short.
- swatch.unused counts a spot or process swatch as used only while an
  image the art shows has a spot plate of its name.
2026-11-12 18:43:04 -08:00

133 lines
6.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! DCS files (`file.place` with Link): an EPS file with one program per printing plate
//! ([`vectorcraft_eps::dcs`]). It is placed as one linked image whose plates are kept with its
//! pixels ([`vectorcraft_doc::separated`]): the screen and composite output show the plates mixed
//! into CMYK, and print separations put each plate on its own ink. Its spot plates become spot
//! swatches, and the clipping path its composite is drawn through clips it. A DCS file can't be
//! opened or embedded (Embed, Place without Link): the image stays linked to its file, which the
//! Links commands read again ([`raster`]).
use vectorcraft_color::swatch::Swatch;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::separated::PlateFile;
use vectorcraft_doc::{Document, ImageBlob, Node, NodeKind};
use vectorcraft_geom::{Affine, Rect};
use super::super::fileio::{self, RasterImage};
use super::super::{EngineError, Result};
/// The largest plate file read (bytes).
const MAX_PLATE_FILE: u64 = 1 << 30;
/// What refuses to open or embed a DCS file.
pub(crate) const LINK_ONLY: &str = "DCS files can only be placed linked: use File › Place with Link on";
/// A DCS file read for placing.
pub(crate) struct DcsArt {
pub img: RasterImage,
/// The image's pixels onto the file's page (its bounding box, top-left at the origin).
pub xf: Affine,
pub page: Rect,
/// The clipping path the composite is drawn through, on the page.
pub clip: Option<Node>,
/// The spot plates' inks and their process equivalents.
pub spots: Vec<(String, [f32; 4])>,
pub warnings: Vec<String>,
}
/// Is `bytes` a DCS file (an EPS file naming its plates)?
pub(crate) fn is_dcs(bytes: &[u8]) -> bool {
vectorcraft_eps::dcs::read(bytes).is_some()
}
/// DCS file `bytes`, read from `path` (when there is one: plate files of their own are found in its
/// folder), with every plate.
pub(crate) fn read(bytes: &[u8], path: Option<&str>) -> Result<DcsArt> {
let dcs = vectorcraft_eps::dcs::read(bytes).ok_or_else(|| EngineError::Other("not a DCS file".into()))?;
let mut files = vec![];
let mut file = |name: &str| plate_file(path, name, &mut files);
let p = vectorcraft_eps::dcs::place(bytes, &dcs, &mut file).map_err(EngineError::Other)?;
// The clipping path in the image's pixel space, where the Links commands put it again; the
// image keeps it, so they know a clip group of that path as the file's.
let to_pixels = (p.xf.determinant().abs() > 1e-12).then(|| p.xf.inverse());
let clip = match (&p.clip, to_pixels) {
(Some(Node { kind: NodeKind::Path { path, .. }, .. }), Some(inv)) => Some(path.transformed(inv)),
_ => None,
};
let blob = ImageBlob::separated_tiff_clipped(&p.separated, p.ppi, &files, clip.as_ref())
.ok_or_else(|| EngineError::Other("the plates can't be kept".into()))?;
let img = RasterImage { key: blob.content_key(), blob, width: p.separated.width, height: p.separated.height, ppi: p.ppi, clip };
let spots = p.separated.spots().map(|s| (s.name.clone(), s.cmyk)).collect();
Ok(DcsArt { img, xf: p.xf, page: p.page, clip: p.clip, spots, warnings: p.warnings })
}
/// A linked file's image, as the Links commands read it again: a DCS file's plates
/// ([`read`]), else the image file ([`fileio::raster_image`]).
pub(crate) fn raster(bytes: &[u8], path: Option<&str>) -> Result<RasterImage> {
if is_dcs(bytes) { read(bytes, path).map(|a| a.img) } else { fileio::raster_image(bytes) }
}
/// The path of the plate file a DCS file at `main` names `name`: the file of that name (its last
/// component, of any system's path) in `main`'s folder, with that name.
fn plate_path(main: Option<&str>, name: &str) -> std::result::Result<(String, String), String> {
let base = name.rsplit(['/', '\\', ':']).next().unwrap_or(name);
let folder = main.map(std::path::Path::new).and_then(std::path::Path::parent);
let Some(folder) = folder.filter(|_| !base.is_empty()) else {
return Err(format!("`{name}` is a file of its own, which can only be found next to the DCS file's path"));
};
Ok((folder.join(base).to_string_lossy().into_owned(), base.to_string()))
}
/// The path of the plate file named `name` that the DCS file at `main` (a path as a link keeps
/// it) was read with ([`plate_path`]); `None` when `main` has no folder.
pub(crate) fn plate_file_path(main: &str, name: &str) -> Option<String> {
plate_path(Some(main), name).ok().map(|(path, _)| path)
}
/// The plate file a DCS file at `main` names `name` ([`plate_path`]), noted in `files` as it is now.
fn plate_file(main: Option<&str>, name: &str, files: &mut Vec<PlateFile>) -> std::result::Result<Vec<u8>, String> {
let (path, base) = plate_path(main, name)?;
match fileio::file_stamp(&path) {
None => Err(format!("`{base}` is not in the DCS file's folder")),
Some((size, _)) if size > MAX_PLATE_FILE => Err(format!("`{base}` is larger than {} MB", MAX_PLATE_FILE >> 20)),
Some((size, modified)) => {
let bytes = fileio::read_file(&path).map_err(|e| e.to_string())?;
files.push(PlateFile { name: base, size, modified: modified.unwrap_or(0) });
Ok(bytes)
}
}
}
/// Whether one of `files`, the plate files of their own a separated image was read from
/// ([`ImageBlob::plate_files`]), next to the DCS file at `main`, is gone or changed since.
pub(crate) fn plate_files_changed(files: &[PlateFile], main: &str) -> bool {
files.iter().any(|f| {
let stamp = plate_path(Some(main), &f.name).ok().and_then(|(path, _)| fileio::file_stamp(&path));
stamp.is_none_or(|(size, modified)| size != f.size || modified.unwrap_or(0) != f.modified)
})
}
/// Add `spots` to `d` as spot swatches, each whose name no swatch has, in its process equivalent.
pub(crate) fn add_spot_swatches(d: &mut Document, spots: &[(String, [f32; 4])]) {
for (name, [c, m, y, k]) in spots {
if d.swatch(name).is_none() {
d.swatches.push(Swatch { name: name.clone(), paint: Paint::solid(Color::cmyk(*c, *m, *y, *k)), global: true, spot: true });
}
}
}
/// A warning for each of `spots` whose name a swatch of `d` already has that isn't a spot color in
/// the same process equivalent: the document's swatch stays, and the plate prints by it.
pub(crate) fn swatch_notes(d: &Document, spots: &[(String, [f32; 4])]) -> Vec<String> {
let mut out = vec![];
for (name, cmyk) in spots {
let Some(sw) = d.swatch(name) else { continue };
let same = sw.spot
&& matches!(sw.paint.color(), Some(Color::Cmyk { c, m, y, k }) if [c, m, y, k].iter().zip(cmyk).all(|(a, b)| (a - b).abs() < 0.005));
if !same {
let how = if sw.spot { "a different color" } else { "a process color, so the plate prints as process inks" };
out.push(format!("the document's swatch `{name}` is kept: it is {how}"));
}
}
out
}