mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 05:56:55 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f033c3f81d |
@@ -77,6 +77,12 @@ pub fn fill_el(imp: &mut Imp, part: &Part, f: &El) -> Option<Fill> {
|
|||||||
"solidFill" => Fill::Solid { color: color(f).unwrap_or(ColorRef::rgb(Rgba::BLACK)) },
|
"solidFill" => Fill::Solid { color: color(f).unwrap_or(ColorRef::rgb(Rgba::BLACK)) },
|
||||||
"gradFill" => Fill::Gradient(gradient(f)),
|
"gradFill" => Fill::Gradient(gradient(f)),
|
||||||
"blipFill" => match blip_fill(imp, part, f) {
|
"blipFill" => match blip_fill(imp, part, f) {
|
||||||
|
// A picture filling a shape or background with neither `a:stretch` nor `a:tile` is
|
||||||
|
// tiled at its own size, as PowerPoint draws it.
|
||||||
|
Some(mut p) if f.child("stretch").is_none() && f.child("tile").is_none() => {
|
||||||
|
p.mode = PictureMode::Tile { tx: 0.0, ty: 0.0, sx: 1.0, sy: 1.0, flip: "none".into(), align: "tl".into() };
|
||||||
|
Fill::Picture(p)
|
||||||
|
}
|
||||||
Some(p) => Fill::Picture(p),
|
Some(p) => Fill::Picture(p),
|
||||||
None => Fill::None,
|
None => Fill::None,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//! Fills read from decks written by other tools.
|
||||||
|
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
|
|
||||||
|
use deckcraft_geom::Xfrm;
|
||||||
|
use deckcraft_model::style::{Fill, PictureFill, PictureMode};
|
||||||
|
use deckcraft_model::{Shape, ShapeId, defaults};
|
||||||
|
|
||||||
|
/// `bytes` with `edit` applied to every slide part.
|
||||||
|
fn edit_slides(bytes: &[u8], edit: impl Fn(String) -> String) -> Vec<u8> {
|
||||||
|
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
|
||||||
|
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||||
|
let o = zip::write::SimpleFileOptions::default();
|
||||||
|
for i in 0..z.len() {
|
||||||
|
let mut f = z.by_index(i).expect("entry");
|
||||||
|
let mut d = vec![];
|
||||||
|
f.read_to_end(&mut d).expect("read");
|
||||||
|
let name = f.name().to_string();
|
||||||
|
if name.starts_with("ppt/slides/slide") && name.ends_with(".xml") {
|
||||||
|
d = edit(String::from_utf8(d).expect("utf8")).into_bytes();
|
||||||
|
}
|
||||||
|
out.start_file(name, o).expect("start");
|
||||||
|
out.write_all(&d).expect("write");
|
||||||
|
}
|
||||||
|
out.finish().expect("finish").into_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn png() -> Vec<u8> {
|
||||||
|
let mut out = Cursor::new(vec![]);
|
||||||
|
image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(4, 4, image::Rgb([10, 200, 30])))
|
||||||
|
.write_to(&mut out, image::ImageFormat::Png)
|
||||||
|
.expect("png");
|
||||||
|
out.into_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A deck with one shape filled with a stretched picture, exported.
|
||||||
|
fn deck() -> Vec<u8> {
|
||||||
|
let mut p = defaults::new_presentation(None);
|
||||||
|
let media = p.add_media("tile.png", "image/png", png());
|
||||||
|
let id = ShapeId(p.alloc_id());
|
||||||
|
let first = p.slides[0].id;
|
||||||
|
p.slide_mut(first).expect("slide").shapes.push(Shape {
|
||||||
|
id,
|
||||||
|
name: "Textured".into(),
|
||||||
|
xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 100.0)),
|
||||||
|
fill: Some(Fill::Picture(PictureFill { media, ..Default::default() })),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
deckcraft_pptx::export(&p).expect("export")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn textured_mode(bytes: &[u8]) -> PictureMode {
|
||||||
|
let q = deckcraft_pptx::import(bytes).expect("import");
|
||||||
|
let s = q.slides[0].shapes.iter().find(|s| s.name == "Textured").expect("shape");
|
||||||
|
match &s.fill {
|
||||||
|
Some(Fill::Picture(pf)) => pf.mode.clone(),
|
||||||
|
other => panic!("{other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_shape_picture_fill_without_stretch_or_tile_tiles() {
|
||||||
|
// tdf#128596: `<a:blipFill><a:blip/></a:blipFill>` on a shape, as some tools write it;
|
||||||
|
// PowerPoint tiles the picture at its own size.
|
||||||
|
let bare = edit_slides(&deck(), |s| {
|
||||||
|
let s = regex_free_strip(&s, "<a:stretch>", "</a:stretch>");
|
||||||
|
assert!(!s.contains("<a:stretch"), "the test deck lost its stretch");
|
||||||
|
s
|
||||||
|
});
|
||||||
|
assert!(matches!(textured_mode(&bare), PictureMode::Tile { sx, sy, .. } if sx == 1.0 && sy == 1.0));
|
||||||
|
// An explicit stretch is still a stretch.
|
||||||
|
assert!(matches!(textured_mode(&deck()), PictureMode::Stretch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `s` without the first `open`…`close` span inside a `blipFill`.
|
||||||
|
fn regex_free_strip(s: &str, open: &str, close: &str) -> String {
|
||||||
|
let Some(b) = s.find("<a:blipFill") else { return s.to_string() };
|
||||||
|
let Some(i) = s[b..].find(open).map(|i| i + b) else { return s.to_string() };
|
||||||
|
let Some(j) = s[i..].find(close).map(|j| j + i + close.len()) else { return s.to_string() };
|
||||||
|
format!("{}{}", &s[..i], &s[j..])
|
||||||
|
}
|
||||||
+4
-10
@@ -566,9 +566,6 @@ fn shape_cells(ctx: &Ctx, text: &str, char_style: &[usize], styles: &[Style], fa
|
|||||||
cells
|
cells
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Size of superscript and subscript text relative to the run's size.
|
|
||||||
const SUB_SUPER: f64 = 0.66;
|
|
||||||
|
|
||||||
/// Shape cells `range` (one item) and hang each cluster's glyphs on its first cell, sharing the
|
/// Shape cells `range` (one item) and hang each cluster's glyphs on its first cell, sharing the
|
||||||
/// cluster's advance among its graphemes (by the ligature's GDEF carets when it has them).
|
/// cluster's advance among its graphemes (by the ligature's GDEF carets when it has them).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -596,11 +593,7 @@ fn shape_item(
|
|||||||
let st = styles.get(cell.style);
|
let st = styles.get(cell.style);
|
||||||
let size = st.map(|s| s.size).unwrap_or(18.0);
|
let size = st.map(|s| s.size).unwrap_or(18.0);
|
||||||
let small = st.is_some_and(|s| s.caps == Caps::Small) && cell.ch.is_lowercase();
|
let small = st.is_some_and(|s| s.caps == Caps::Small) && cell.ch.is_lowercase();
|
||||||
let size = if small { size * 0.8 } else { size };
|
(if small { size * 0.8 } else { size }) / upem
|
||||||
// Superscript and subscript glyphs are drawn at two thirds of the size, so they advance by
|
|
||||||
// two thirds too.
|
|
||||||
let size = if st.is_some_and(|s| s.baseline != 0.0) { size * SUB_SUPER } else { size };
|
|
||||||
size / upem
|
|
||||||
};
|
};
|
||||||
// Cluster heads in this item, ascending; each cluster spans to the next head.
|
// Cluster heads in this item, ascending; each cluster spans to the next head.
|
||||||
let mut heads: Vec<usize> = glyphs.iter().filter_map(|g| char_at_byte.get(&g.cluster).copied()).filter(|h| range.contains(h)).collect();
|
let mut heads: Vec<usize> = glyphs.iter().filter_map(|g| char_at_byte.get(&g.cluster).copied()).filter(|h| range.contains(h)).collect();
|
||||||
@@ -1162,7 +1155,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||||||
let Some(st) = p.styles.get(cell.style) else { continue };
|
let Some(st) = p.styles.get(cell.style) else { continue };
|
||||||
let Some(&(cx, cx1)) = line.edges.get(k) else { continue };
|
let Some(&(cx, cx1)) = line.edges.get(k) else { continue };
|
||||||
let base_y = line.baseline - st.baseline * st.size;
|
let base_y = line.baseline - st.baseline * st.size;
|
||||||
let size = if st.baseline != 0.0 { st.size * SUB_SUPER } else { st.size };
|
let size = if st.baseline != 0.0 { st.size * 0.66 } else { st.size };
|
||||||
let size = if st.caps == Caps::Small && cell.ch.is_lowercase() { size * 0.8 } else { size };
|
let size = if st.caps == Caps::Small && cell.ch.is_lowercase() { size * 0.8 } else { size };
|
||||||
let key = (cell.style, cell.face);
|
let key = (cell.style, cell.face);
|
||||||
if key != cur_key {
|
if key != cur_key {
|
||||||
@@ -1211,8 +1204,9 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let k66 = if st.baseline != 0.0 { 0.66 } else { 1.0 };
|
||||||
for (gid, gx, gy) in &cell.glyphs {
|
for (gid, gx, gy) in &cell.glyphs {
|
||||||
r.glyphs.push((*gid, cl + kash + gx, base_y + gy));
|
r.glyphs.push((*gid, cl + kash + gx * k66, base_y + gy));
|
||||||
r.cells.push((ci, cell.ch));
|
r.cells.push((ci, cell.ch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,41 +395,3 @@ fn hostile_bidi_input_never_panics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Width of `word` laid out in a run with `baseline` (0 = normal, 0.3 = superscript …).
|
|
||||||
fn run_width(word: &str, baseline: Option<f64>) -> f64 {
|
|
||||||
let mut run = Run::new(word);
|
|
||||||
run.props.size = Some(24.0);
|
|
||||||
run.props.baseline = baseline;
|
|
||||||
let (p, sh) = bidi_setup(vec![run], 2000.0, false, Align::Left);
|
|
||||||
let l = lay(&p, &sh, 2000.0, 300.0);
|
|
||||||
let line = &l.lines[0];
|
|
||||||
line.caret_x.last().unwrap() - line.caret_x.first().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn superscript_and_subscript_advance_at_their_drawn_size() {
|
|
||||||
// The glyphs are drawn at two thirds of the size; advancing by the full size spread the
|
|
||||||
// letters apart ("s u p e r").
|
|
||||||
let normal = run_width("superscript", None);
|
|
||||||
for b in [Some(0.3), Some(-0.25)] {
|
|
||||||
let w = run_width("superscript", b);
|
|
||||||
assert!((w / normal - SUB_SUPER).abs() < 0.02, "{b:?}: {w} vs {normal}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn superscript_glyphs_sit_inside_their_cells() {
|
|
||||||
let mut run = Run::new("super");
|
|
||||||
run.props.size = Some(24.0);
|
|
||||||
run.props.baseline = Some(0.3);
|
|
||||||
let (p, sh) = bidi_setup(vec![run], 2000.0, false, Align::Left);
|
|
||||||
let l = lay(&p, &sh, 2000.0, 300.0);
|
|
||||||
let line = &l.lines[0];
|
|
||||||
let r = l.runs.iter().find(|r| !r.cells.is_empty()).unwrap();
|
|
||||||
assert!((r.size - 24.0 * SUB_SUPER).abs() < 1e-6);
|
|
||||||
for (g, (ci, _)) in r.glyphs.iter().zip(&r.cells) {
|
|
||||||
let (x0, x1) = line.edges[*ci - line.start];
|
|
||||||
assert!(g.1 >= x0 - 0.01 && g.1 < x1, "glyph at {} outside its cell {x0}..{x1}", g.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user