mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-15 23:02:32 +00:00
Text wrap: Alpha Channel outlines fit composition, and an empty one wraps nothing
An outline with more path elements than composition follows is traced on a coarser grid, so a fragmented image keeps an Alpha Channel contour instead of falling back to the frame. An image with no pixel more opaque than the threshold leaves the text alone.
This commit is contained in:
1 parent
5b89a13c2e
commit
9fcab8e66b
2 files changed
+71
-13
No files matched your search
@@ -386,7 +386,7 @@ pub struct Contour {
|
||||
}
|
||||
|
||||
/// Points a contour outline may have; a larger outline wraps around its bounding box.
|
||||
const MAX_CONTOUR_POINTS: usize = 8192;
|
||||
pub const MAX_CONTOUR_POINTS: usize = 8192;
|
||||
|
||||
impl Contour {
|
||||
/// The outline of `path` (after `to_inner`), flattened; None if it is empty or too detailed.
|
||||
@@ -587,7 +587,8 @@ pub fn exclusion(doc: &Document, other: &Item, xf: Affine, inv: Affine, left_pag
|
||||
let group = other.shape == designcraft_doc::Shape::Group;
|
||||
let b =
|
||||
if group { (xf * other.xf.inverse()).transform_rect_bbox(other.bounds()) } else { other.path.transformed(xf).bounds().unwrap_or(Rect::ZERO) };
|
||||
wrap_exclusion(&other.wrap, b, || (!group).then(|| contour_path(doc, other, &other.wrap)), xf, inv, left_page)
|
||||
let e = wrap_exclusion(&other.wrap, b, || (!group).then(|| contour_path(doc, other, &other.wrap)), xf, inv, left_page);
|
||||
nothing_visible_wraps_nothing(doc, other, &other.wrap, e)
|
||||
}
|
||||
|
||||
/// The wrap exclusion of the graphic placed in frame `it` (arguments as for [`exclusion`]): around
|
||||
@@ -595,7 +596,26 @@ pub fn exclusion(doc: &Document, other: &Item, xf: Affine, inv: Affine, left_pag
|
||||
pub fn graphic_exclusion(doc: &Document, it: &Item, xf: Affine, inv: Affine, left_page: bool) -> Option<Exclusion> {
|
||||
let g = it.graphic()?;
|
||||
let shown = g.shown_bounds(it.inner_bounds())?;
|
||||
Some(wrap_exclusion(&g.wrap, xf.transform_rect_bbox(shown), || Some(contour_path(doc, it, &g.wrap)), xf, inv, left_page))
|
||||
let e = wrap_exclusion(&g.wrap, xf.transform_rect_bbox(shown), || Some(contour_path(doc, it, &g.wrap)), xf, inv, left_page);
|
||||
Some(nothing_visible_wraps_nothing(doc, it, &g.wrap, e))
|
||||
}
|
||||
|
||||
/// An Alpha Channel contour around an image with no pixel more opaque than the threshold leaves
|
||||
/// the text alone (the exclusion's mode becomes None).
|
||||
fn nothing_visible_wraps_nothing(doc: &Document, it: &Item, wrap: &designcraft_doc::TextWrap, mut e: Exclusion) -> Exclusion {
|
||||
if wrap.mode == WrapMode::Contour
|
||||
&& wrap.contour == designcraft_doc::ContourType::AlphaChannel
|
||||
&& wrap.path.is_none()
|
||||
&& let Some(g) = it.graphic()
|
||||
&& let Some(o) = doc.alpha_outline(g.asset, wrap.alpha_threshold)
|
||||
&& o.width > 0
|
||||
&& o.height > 0
|
||||
&& o.outer.subpaths.is_empty()
|
||||
{
|
||||
e.mode = WrapMode::None;
|
||||
e.contour = None;
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
/// The exclusion of `wrap` around an object with spread-space bounds `b`; `outline` gives its
|
||||
@@ -637,8 +657,9 @@ fn wrap_exclusion(
|
||||
/// The outline a contour wrap of `kind` around frame `it` follows, in the frame's inner space, and
|
||||
/// for an outline taken from the placed graphic, the frame path that clips it. Bounding Box
|
||||
/// follows the part of the placed graphic the frame shows; Same As Clipping follows the graphic's
|
||||
/// clipping path when it has one; detected edges, alpha channels and image paths need the image,
|
||||
/// so they follow the frame's path (as other frames do).
|
||||
/// clipping path when it has one; Alpha Channel follows the image's traced outline when the
|
||||
/// engine has traced one; detected edges and image paths follow the frame's path (as other
|
||||
/// frames do).
|
||||
fn contour_path(doc: &Document, it: &Item, wrap: &designcraft_doc::TextWrap) -> (designcraft_geom::BezPath, Option<designcraft_geom::BezPath>) {
|
||||
let frame = designcraft_geom::corners::apply(&it.path, &it.corners);
|
||||
let kind = wrap.contour;
|
||||
@@ -765,11 +786,14 @@ fn wrap_obstacles(
|
||||
}
|
||||
let xf = parent * it.xf;
|
||||
let left = || left_page(parent.transform_rect_bbox(it.bounds()).center().x);
|
||||
// An exclusion whose mode came back None (nothing visible to wrap) is left out.
|
||||
if it.wrap.mode != WrapMode::None {
|
||||
out.push(exclusion(doc, it, xf, inv, left()));
|
||||
out.extend(Some(exclusion(doc, it, xf, inv, left())).filter(|e| e.mode != WrapMode::None));
|
||||
}
|
||||
match &it.content {
|
||||
Content::Graphic(g) if g.wrap.mode != WrapMode::None => out.extend(graphic_exclusion(doc, it, xf, inv, left())),
|
||||
Content::Graphic(g) if g.wrap.mode != WrapMode::None => {
|
||||
out.extend(graphic_exclusion(doc, it, xf, inv, left()).filter(|e| e.mode != WrapMode::None))
|
||||
}
|
||||
Content::Group { .. } => {
|
||||
for c in it.shown_children() {
|
||||
wrap_obstacles(doc, c, xf, inv, skip, left_page, depth + 1, out);
|
||||
|
||||
@@ -91,7 +91,12 @@ pub(crate) fn trace(data: &Arc<Vec<u8>>, asset: AssetId, threshold: u8) -> Optio
|
||||
mask
|
||||
}
|
||||
};
|
||||
if let Some((outer, holes)) = rings(&mask, cols as usize, rows as usize) {
|
||||
// Composition follows an outline of at most MAX_CONTOUR_POINTS path elements (holes
|
||||
// included, for Include Inside Edges): a ring of k points is k + 1 elements with its close.
|
||||
let fits = |outer: &[Vec<(i32, i32)>], holes: &[Vec<(i32, i32)>]| {
|
||||
outer.iter().chain(holes).map(|r| r.len() + 1).sum::<usize>() <= designcraft_compose::MAX_CONTOUR_POINTS
|
||||
};
|
||||
if let Some((outer, holes)) = rings(&mask, cols as usize, rows as usize).filter(|(o, h)| fits(o, h)) {
|
||||
let (sx, sy) = (f64::from(w) / f64::from(cols), f64::from(h) / f64::from(rows));
|
||||
let path = |rings: &[Vec<(i32, i32)>]| {
|
||||
let mut p = BezPath::new();
|
||||
@@ -240,15 +245,44 @@ mod tests {
|
||||
|
||||
/// A 64 × 64 PNG: visible where `on(x, y)`.
|
||||
fn png(on: impl Fn(usize, usize) -> bool) -> Vec<u8> {
|
||||
let mut pixels = vec![0; 64 * 64 * 4];
|
||||
for y in 0..64 {
|
||||
for x in 0..64 {
|
||||
let i = (y * 64 + x) * 4;
|
||||
png_sized(64, on)
|
||||
}
|
||||
|
||||
/// A `size` × `size` PNG: visible where `on(x, y)`.
|
||||
fn png_sized(size: usize, on: impl Fn(usize, usize) -> bool) -> Vec<u8> {
|
||||
let mut pixels = vec![0; size * size * 4];
|
||||
for y in 0..size {
|
||||
for x in 0..size {
|
||||
let i = (y * size + x) * 4;
|
||||
pixels[i] = 120;
|
||||
pixels[i + 3] = if on(x, y) { 255 } else { 0 };
|
||||
}
|
||||
}
|
||||
designcraft_render::Rendered { width: 64, height: 64, pixels }.to_png()
|
||||
designcraft_render::Rendered { width: size as u32, height: size as u32, pixels }.to_png()
|
||||
}
|
||||
|
||||
/// An outline too detailed for composition (here 4,096 separate dots, 20,480 path elements
|
||||
/// on the finest grid) is traced on a coarser grid until composition can follow it.
|
||||
#[test]
|
||||
fn a_fragmented_outline_is_traced_coarser_until_composition_can_follow_it() {
|
||||
let dots = png_sized(256, |x, y| x % 4 == 0 && y % 4 == 0);
|
||||
let o = trace(&Arc::new(dots), AssetId(1), 25).unwrap();
|
||||
let elements = o.outer.to_bezpath().elements().len() + o.holes.to_bezpath().elements().len();
|
||||
assert!(elements > 0 && elements <= designcraft_compose::MAX_CONTOUR_POINTS, "{elements} path elements");
|
||||
}
|
||||
|
||||
/// An image with no pixel more opaque than the threshold leaves the text alone: with the
|
||||
/// frame's contour the caption over it is overset, with Alpha Channel it is set.
|
||||
#[test]
|
||||
fn an_image_with_nothing_visible_wraps_no_text() {
|
||||
let (mut s, image, story) = page(&png(|_, _| false), [150.0, 150.0, 178.0, 178.0]);
|
||||
assert!(overset(&s, story), "the frame's contour covers the caption");
|
||||
s.execute("object.textWrap", &json!({"ids": [image], "contour": "alphaChannel"})).unwrap();
|
||||
assert!(!overset(&s, story), "nothing visible: nothing wraps");
|
||||
// The same for the wrap set on the placed graphic.
|
||||
s.execute("object.textWrap", &json!({"ids": [image], "mode": "none"})).unwrap();
|
||||
s.execute("object.textWrap", &json!({"ids": [image], "content": true, "mode": "contour", "contour": "alphaChannel"})).unwrap();
|
||||
assert!(!overset(&s, story));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user