Image fit modes
@ -14,7 +14,7 @@ use ttf_parser::{name_id, GlyphId, Tag};
|
||||
|
||||
use super::subset;
|
||||
use crate::font::{find_name, FaceId, FontStore};
|
||||
use crate::frame::{Element, Frame, Geometry};
|
||||
use crate::frame::{Element, Frame, Geometry, Text};
|
||||
use crate::geom::{self, Color, Em, Length, Paint, Size};
|
||||
use crate::image::{Image, ImageId, ImageStore};
|
||||
use crate::Context;
|
||||
@ -36,16 +36,16 @@ struct PdfExporter<'a> {
|
||||
frames: &'a [Rc<Frame>],
|
||||
fonts: &'a FontStore,
|
||||
images: &'a ImageStore,
|
||||
glyphs: HashMap<FaceId, HashSet<u16>>,
|
||||
font_map: Remapper<FaceId>,
|
||||
image_map: Remapper<ImageId>,
|
||||
glyphs: HashMap<FaceId, HashSet<u16>>,
|
||||
}
|
||||
|
||||
impl<'a> PdfExporter<'a> {
|
||||
fn new(ctx: &'a Context, frames: &'a [Rc<Frame>]) -> Self {
|
||||
let mut glyphs = HashMap::<FaceId, HashSet<u16>>::new();
|
||||
let mut font_map = Remapper::new();
|
||||
let mut image_map = Remapper::new();
|
||||
let mut glyphs = HashMap::<FaceId, HashSet<u16>>::new();
|
||||
let mut alpha_masks = 0;
|
||||
|
||||
for frame in frames {
|
||||
@ -56,7 +56,6 @@ impl<'a> PdfExporter<'a> {
|
||||
let set = glyphs.entry(text.face_id).or_default();
|
||||
set.extend(text.glyphs.iter().map(|g| g.id));
|
||||
}
|
||||
Element::Geometry(_, _) => {}
|
||||
Element::Image(id, _) => {
|
||||
let img = ctx.images.get(id);
|
||||
if img.buf.color().has_alpha() {
|
||||
@ -64,7 +63,7 @@ impl<'a> PdfExporter<'a> {
|
||||
}
|
||||
image_map.insert(id);
|
||||
}
|
||||
Element::Link(_, _) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -120,8 +119,8 @@ impl<'a> PdfExporter<'a> {
|
||||
for ((page_id, content_id), page) in
|
||||
self.refs.pages().zip(self.refs.contents()).zip(self.frames)
|
||||
{
|
||||
let w = page.size.w.to_pt() as f32;
|
||||
let h = page.size.h.to_pt() as f32;
|
||||
let w = page.size.w.to_f32();
|
||||
let h = page.size.h.to_f32();
|
||||
|
||||
let mut page_writer = self.writer.page(page_id);
|
||||
page_writer
|
||||
@ -131,10 +130,10 @@ impl<'a> PdfExporter<'a> {
|
||||
let mut annotations = page_writer.annotations();
|
||||
for (pos, element) in page.elements() {
|
||||
if let Element::Link(href, size) = element {
|
||||
let x = pos.x.to_pt() as f32;
|
||||
let y = (page.size.h - pos.y).to_pt() as f32;
|
||||
let w = size.w.to_pt() as f32;
|
||||
let h = size.h.to_pt() as f32;
|
||||
let x = pos.x.to_f32();
|
||||
let y = (page.size.h - pos.y).to_f32();
|
||||
let w = size.w.to_f32();
|
||||
let h = size.h.to_f32();
|
||||
|
||||
annotations
|
||||
.push()
|
||||
@ -158,148 +157,9 @@ impl<'a> PdfExporter<'a> {
|
||||
}
|
||||
|
||||
fn write_page(&mut self, id: Ref, page: &'a Frame) {
|
||||
let mut content = Content::new();
|
||||
|
||||
// We only write font switching actions when the used face changes. To
|
||||
// do that, we need to remember the active face.
|
||||
let mut face_id = None;
|
||||
let mut size = Length::zero();
|
||||
let mut fill: Option<Paint> = None;
|
||||
let mut in_text_state = false;
|
||||
|
||||
for (pos, element) in page.elements() {
|
||||
// Make sure the content stream is in the correct state.
|
||||
match element {
|
||||
Element::Text(_) if !in_text_state => {
|
||||
content.begin_text();
|
||||
in_text_state = true;
|
||||
}
|
||||
|
||||
Element::Geometry(..) | Element::Image(..) if in_text_state => {
|
||||
content.end_text();
|
||||
in_text_state = false;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let x = pos.x.to_pt() as f32;
|
||||
let y = (page.size.h - pos.y).to_pt() as f32;
|
||||
|
||||
match *element {
|
||||
Element::Text(ref text) => {
|
||||
if fill != Some(text.fill) {
|
||||
write_fill(&mut content, text.fill);
|
||||
fill = Some(text.fill);
|
||||
}
|
||||
|
||||
// Then, also check if we need to issue a font switching
|
||||
// action.
|
||||
if face_id != Some(text.face_id) || text.size != size {
|
||||
face_id = Some(text.face_id);
|
||||
size = text.size;
|
||||
|
||||
let name = format_eco!("F{}", self.font_map.map(text.face_id));
|
||||
content.set_font(Name(name.as_bytes()), size.to_pt() as f32);
|
||||
}
|
||||
|
||||
let face = self.fonts.get(text.face_id);
|
||||
|
||||
// Position the text.
|
||||
content.set_text_matrix([1.0, 0.0, 0.0, 1.0, x, y]);
|
||||
|
||||
let mut positioned = content.show_positioned();
|
||||
let mut items = positioned.items();
|
||||
let mut adjustment = Em::zero();
|
||||
let mut encoded = vec![];
|
||||
|
||||
// Write the glyphs with kerning adjustments.
|
||||
for glyph in &text.glyphs {
|
||||
adjustment += glyph.x_offset;
|
||||
|
||||
if !adjustment.is_zero() {
|
||||
if !encoded.is_empty() {
|
||||
items.show(Str(&encoded));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
items.adjust(-adjustment.to_pdf());
|
||||
adjustment = Em::zero();
|
||||
}
|
||||
|
||||
encoded.push((glyph.id >> 8) as u8);
|
||||
encoded.push((glyph.id & 0xff) as u8);
|
||||
|
||||
if let Some(advance) = face.advance(glyph.id) {
|
||||
adjustment += glyph.x_advance - advance;
|
||||
}
|
||||
|
||||
adjustment -= glyph.x_offset;
|
||||
}
|
||||
|
||||
if !encoded.is_empty() {
|
||||
items.show(Str(&encoded));
|
||||
}
|
||||
}
|
||||
|
||||
Element::Geometry(ref geometry, paint) => {
|
||||
content.save_state();
|
||||
|
||||
match *geometry {
|
||||
Geometry::Rect(Size { w, h }) => {
|
||||
let w = w.to_pt() as f32;
|
||||
let h = h.to_pt() as f32;
|
||||
if w > 0.0 && h > 0.0 {
|
||||
write_fill(&mut content, paint);
|
||||
content.rect(x, y - h, w, h);
|
||||
content.fill_nonzero();
|
||||
}
|
||||
}
|
||||
Geometry::Ellipse(size) => {
|
||||
let path = geom::Path::ellipse(size);
|
||||
write_fill(&mut content, paint);
|
||||
write_path(&mut content, x, y, &path);
|
||||
}
|
||||
Geometry::Line(target, thickness) => {
|
||||
write_stroke(&mut content, paint, thickness.to_pt() as f32);
|
||||
content.move_to(x, y);
|
||||
content.line_to(
|
||||
x + target.x.to_pt() as f32,
|
||||
y - target.y.to_pt() as f32,
|
||||
);
|
||||
content.stroke();
|
||||
}
|
||||
Geometry::Path(ref path) => {
|
||||
write_fill(&mut content, paint);
|
||||
write_path(&mut content, x, y, path)
|
||||
}
|
||||
}
|
||||
|
||||
content.restore_state();
|
||||
}
|
||||
|
||||
Element::Image(id, Size { w, h }) => {
|
||||
let name = format!("Im{}", self.image_map.map(id));
|
||||
let w = w.to_pt() as f32;
|
||||
let h = h.to_pt() as f32;
|
||||
|
||||
content.save_state();
|
||||
content.concat_matrix([w, 0.0, 0.0, h, x, y - h]);
|
||||
content.x_object(Name(name.as_bytes()));
|
||||
content.restore_state();
|
||||
}
|
||||
|
||||
Element::Link(_, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if in_text_state {
|
||||
content.end_text();
|
||||
}
|
||||
|
||||
self.writer
|
||||
.stream(id, &deflate(&content.finish()))
|
||||
.filter(Filter::FlateDecode);
|
||||
let writer = PageExporter::new(self);
|
||||
let content = writer.write(page);
|
||||
self.writer.stream(id, &deflate(&content)).filter(Filter::FlateDecode);
|
||||
}
|
||||
|
||||
fn write_fonts(&mut self) {
|
||||
@ -350,7 +210,7 @@ impl<'a> PdfExporter<'a> {
|
||||
let num_glyphs = ttf.number_of_glyphs();
|
||||
(0 .. num_glyphs).map(|g| {
|
||||
let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
|
||||
face.to_em(x).to_pdf()
|
||||
face.to_em(x).to_font_units()
|
||||
})
|
||||
});
|
||||
|
||||
@ -363,16 +223,16 @@ impl<'a> PdfExporter<'a> {
|
||||
|
||||
let global_bbox = ttf.global_bounding_box();
|
||||
let bbox = Rect::new(
|
||||
face.to_em(global_bbox.x_min).to_pdf(),
|
||||
face.to_em(global_bbox.y_min).to_pdf(),
|
||||
face.to_em(global_bbox.x_max).to_pdf(),
|
||||
face.to_em(global_bbox.y_max).to_pdf(),
|
||||
face.to_em(global_bbox.x_min).to_font_units(),
|
||||
face.to_em(global_bbox.y_min).to_font_units(),
|
||||
face.to_em(global_bbox.x_max).to_font_units(),
|
||||
face.to_em(global_bbox.y_max).to_font_units(),
|
||||
);
|
||||
|
||||
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
|
||||
let ascender = face.ascender.to_pdf();
|
||||
let descender = face.descender.to_pdf();
|
||||
let cap_height = face.cap_height.to_pdf();
|
||||
let ascender = face.ascender.to_font_units();
|
||||
let descender = face.descender.to_font_units();
|
||||
let cap_height = face.cap_height.to_font_units();
|
||||
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
|
||||
|
||||
// Write the font descriptor (contains metrics about the font).
|
||||
@ -474,45 +334,244 @@ impl<'a> PdfExporter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a fill change into a content stream.
|
||||
fn write_fill(content: &mut Content, fill: Paint) {
|
||||
let Paint::Color(Color::Rgba(c)) = fill;
|
||||
content.set_fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0);
|
||||
/// A writer for the contents of a single page.
|
||||
struct PageExporter<'a> {
|
||||
fonts: &'a FontStore,
|
||||
font_map: &'a Remapper<FaceId>,
|
||||
image_map: &'a Remapper<ImageId>,
|
||||
content: Content,
|
||||
in_text_state: bool,
|
||||
face_id: Option<FaceId>,
|
||||
font_size: Length,
|
||||
font_fill: Option<Paint>,
|
||||
}
|
||||
|
||||
/// Write a stroke change into a content stream.
|
||||
fn write_stroke(content: &mut Content, stroke: Paint, thickness: f32) {
|
||||
match stroke {
|
||||
Paint::Color(Color::Rgba(c)) => {
|
||||
content.set_stroke_rgb(
|
||||
c.r as f32 / 255.0,
|
||||
c.g as f32 / 255.0,
|
||||
c.b as f32 / 255.0,
|
||||
);
|
||||
impl<'a> PageExporter<'a> {
|
||||
/// Create a new page exporter.
|
||||
fn new(exporter: &'a PdfExporter) -> Self {
|
||||
Self {
|
||||
fonts: exporter.fonts,
|
||||
font_map: &exporter.font_map,
|
||||
image_map: &exporter.image_map,
|
||||
content: Content::new(),
|
||||
in_text_state: false,
|
||||
face_id: None,
|
||||
font_size: Length::zero(),
|
||||
font_fill: None,
|
||||
}
|
||||
}
|
||||
content.set_line_width(thickness);
|
||||
}
|
||||
|
||||
/// Write a path into a content stream.
|
||||
fn write_path(content: &mut Content, x: f32, y: f32, path: &geom::Path) {
|
||||
let f = |length: Length| length.to_pt() as f32;
|
||||
for elem in &path.0 {
|
||||
match elem {
|
||||
geom::PathElement::MoveTo(p) => content.move_to(x + f(p.x), y + f(p.y)),
|
||||
geom::PathElement::LineTo(p) => content.line_to(x + f(p.x), y + f(p.y)),
|
||||
geom::PathElement::CubicTo(p1, p2, p3) => content.cubic_to(
|
||||
x + f(p1.x),
|
||||
y + f(p1.y),
|
||||
x + f(p2.x),
|
||||
y + f(p2.y),
|
||||
x + f(p3.x),
|
||||
y + f(p3.y),
|
||||
),
|
||||
geom::PathElement::ClosePath => content.close_path(),
|
||||
};
|
||||
/// Write the page frame into the content stream.
|
||||
fn write(mut self, frame: &Frame) -> Vec<u8> {
|
||||
self.write_frame(0.0, frame.size.h.to_f32(), frame);
|
||||
self.content.finish()
|
||||
}
|
||||
|
||||
/// Write a frame into the content stream.
|
||||
fn write_frame(&mut self, x: f32, y: f32, frame: &Frame) {
|
||||
if frame.clips {
|
||||
let w = frame.size.w.to_f32();
|
||||
let h = frame.size.h.to_f32();
|
||||
self.content.save_state();
|
||||
self.content.move_to(x, y);
|
||||
self.content.line_to(x + w, y);
|
||||
self.content.line_to(x + w, y - h);
|
||||
self.content.line_to(x, y - h);
|
||||
self.content.clip_nonzero();
|
||||
self.content.end_path();
|
||||
}
|
||||
|
||||
for (offset, element) in &frame.elements {
|
||||
// Make sure the content stream is in the correct state.
|
||||
match element {
|
||||
Element::Text(_) if !self.in_text_state => {
|
||||
self.content.begin_text();
|
||||
self.in_text_state = true;
|
||||
}
|
||||
|
||||
Element::Geometry(..) | Element::Image(..) if self.in_text_state => {
|
||||
self.content.end_text();
|
||||
self.in_text_state = false;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let x = x + offset.x.to_f32();
|
||||
let y = y - offset.y.to_f32();
|
||||
|
||||
match *element {
|
||||
Element::Text(ref text) => {
|
||||
self.write_text(x, y, text);
|
||||
}
|
||||
Element::Geometry(ref geometry, paint) => {
|
||||
self.write_geometry(x, y, geometry, paint);
|
||||
}
|
||||
Element::Image(id, size) => {
|
||||
self.write_image(x, y, id, size);
|
||||
}
|
||||
Element::Link(_, _) => {}
|
||||
Element::Frame(ref frame) => {
|
||||
self.write_frame(x, y, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.in_text_state {
|
||||
self.content.end_text();
|
||||
}
|
||||
|
||||
if frame.clips {
|
||||
self.content.restore_state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a glyph run into the content stream.
|
||||
fn write_text(&mut self, x: f32, y: f32, text: &Text) {
|
||||
if self.font_fill != Some(text.fill) {
|
||||
self.write_fill(text.fill);
|
||||
self.font_fill = Some(text.fill);
|
||||
}
|
||||
|
||||
// Then, also check if we need to issue a font switching
|
||||
// action.
|
||||
if self.face_id != Some(text.face_id) || self.font_size != text.size {
|
||||
self.face_id = Some(text.face_id);
|
||||
self.font_size = text.size;
|
||||
|
||||
let name = format_eco!("F{}", self.font_map.map(text.face_id));
|
||||
self.content.set_font(Name(name.as_bytes()), text.size.to_f32());
|
||||
}
|
||||
|
||||
let face = self.fonts.get(text.face_id);
|
||||
|
||||
// Position the text.
|
||||
self.content.set_text_matrix([1.0, 0.0, 0.0, 1.0, x, y]);
|
||||
|
||||
let mut positioned = self.content.show_positioned();
|
||||
let mut items = positioned.items();
|
||||
let mut adjustment = Em::zero();
|
||||
let mut encoded = vec![];
|
||||
|
||||
// Write the glyphs with kerning adjustments.
|
||||
for glyph in &text.glyphs {
|
||||
adjustment += glyph.x_offset;
|
||||
|
||||
if !adjustment.is_zero() {
|
||||
if !encoded.is_empty() {
|
||||
items.show(Str(&encoded));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
items.adjust(-adjustment.to_font_units());
|
||||
adjustment = Em::zero();
|
||||
}
|
||||
|
||||
encoded.push((glyph.id >> 8) as u8);
|
||||
encoded.push((glyph.id & 0xff) as u8);
|
||||
|
||||
if let Some(advance) = face.advance(glyph.id) {
|
||||
adjustment += glyph.x_advance - advance;
|
||||
}
|
||||
|
||||
adjustment -= glyph.x_offset;
|
||||
}
|
||||
|
||||
if !encoded.is_empty() {
|
||||
items.show(Str(&encoded));
|
||||
}
|
||||
}
|
||||
|
||||
/// Write an image into the content stream.
|
||||
fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
|
||||
let name = format!("Im{}", self.image_map.map(id));
|
||||
let w = size.w.to_f32();
|
||||
let h = size.h.to_f32();
|
||||
|
||||
self.content.save_state();
|
||||
self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]);
|
||||
self.content.x_object(Name(name.as_bytes()));
|
||||
self.content.restore_state();
|
||||
}
|
||||
|
||||
/// Write a geometrical shape into the content stream.
|
||||
fn write_geometry(&mut self, x: f32, y: f32, geometry: &Geometry, paint: Paint) {
|
||||
self.content.save_state();
|
||||
|
||||
match *geometry {
|
||||
Geometry::Rect(Size { w, h }) => {
|
||||
let w = w.to_f32();
|
||||
let h = h.to_f32();
|
||||
if w > 0.0 && h > 0.0 {
|
||||
self.write_fill(paint);
|
||||
self.content.rect(x, y - h, w, h);
|
||||
self.content.fill_nonzero();
|
||||
}
|
||||
}
|
||||
Geometry::Ellipse(size) => {
|
||||
let path = geom::Path::ellipse(size);
|
||||
self.write_fill(paint);
|
||||
self.write_filled_path(x, y, &path);
|
||||
}
|
||||
Geometry::Line(target, thickness) => {
|
||||
self.write_stroke(paint, thickness.to_f32());
|
||||
self.content.move_to(x, y);
|
||||
self.content.line_to(x + target.x.to_f32(), y - target.y.to_f32());
|
||||
self.content.stroke();
|
||||
}
|
||||
Geometry::Path(ref path) => {
|
||||
self.write_fill(paint);
|
||||
self.write_filled_path(x, y, path)
|
||||
}
|
||||
}
|
||||
|
||||
self.content.restore_state();
|
||||
}
|
||||
|
||||
/// Write and fill path into a content stream.
|
||||
fn write_filled_path(&mut self, x: f32, y: f32, path: &geom::Path) {
|
||||
for elem in &path.0 {
|
||||
match elem {
|
||||
geom::PathElement::MoveTo(p) => {
|
||||
self.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
|
||||
}
|
||||
geom::PathElement::LineTo(p) => {
|
||||
self.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
|
||||
}
|
||||
geom::PathElement::CubicTo(p1, p2, p3) => self.content.cubic_to(
|
||||
x + p1.x.to_f32(),
|
||||
y + p1.y.to_f32(),
|
||||
x + p2.x.to_f32(),
|
||||
y + p2.y.to_f32(),
|
||||
x + p3.x.to_f32(),
|
||||
y + p3.y.to_f32(),
|
||||
),
|
||||
geom::PathElement::ClosePath => self.content.close_path(),
|
||||
};
|
||||
}
|
||||
self.content.fill_nonzero();
|
||||
}
|
||||
|
||||
/// Write a fill change into a content stream.
|
||||
fn write_fill(&mut self, fill: Paint) {
|
||||
let Paint::Color(Color::Rgba(c)) = fill;
|
||||
self.content.set_fill_rgb(
|
||||
c.r as f32 / 255.0,
|
||||
c.g as f32 / 255.0,
|
||||
c.b as f32 / 255.0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Write a stroke change into a content stream.
|
||||
fn write_stroke(&mut self, stroke: Paint, thickness: f32) {
|
||||
let Paint::Color(Color::Rgba(c)) = stroke;
|
||||
self.content.set_stroke_rgb(
|
||||
c.r as f32 / 255.0,
|
||||
c.g as f32 / 255.0,
|
||||
c.b as f32 / 255.0,
|
||||
);
|
||||
self.content.set_line_width(thickness);
|
||||
}
|
||||
content.fill_nonzero();
|
||||
}
|
||||
|
||||
/// The compression level for the deflating.
|
||||
@ -693,14 +752,26 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods for [`Length`].
|
||||
trait LengthExt {
|
||||
/// Convert an em length to a number of points.
|
||||
fn to_f32(self) -> f32;
|
||||
}
|
||||
|
||||
impl LengthExt for Length {
|
||||
fn to_f32(self) -> f32 {
|
||||
self.to_pt() as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods for [`Em`].
|
||||
trait EmExt {
|
||||
/// Convert an em length to a number of PDF font units.
|
||||
fn to_pdf(self) -> f32;
|
||||
fn to_font_units(self) -> f32;
|
||||
}
|
||||
|
||||
impl EmExt for Em {
|
||||
fn to_pdf(self) -> f32 {
|
||||
fn to_font_units(self) -> f32 {
|
||||
1000.0 * self.get() as f32
|
||||
}
|
||||
}
|
||||
|
81
src/frame.rs
@ -16,8 +16,10 @@ pub struct Frame {
|
||||
pub size: Size,
|
||||
/// The baseline of the frame measured from the top.
|
||||
pub baseline: Length,
|
||||
/// Whether this frame should be a clipping boundary.
|
||||
pub clips: bool,
|
||||
/// The elements composing this layout.
|
||||
pub children: Vec<(Point, FrameChild)>,
|
||||
pub elements: Vec<(Point, Element)>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
@ -25,37 +27,44 @@ impl Frame {
|
||||
#[track_caller]
|
||||
pub fn new(size: Size, baseline: Length) -> Self {
|
||||
assert!(size.is_finite());
|
||||
Self { size, baseline, children: vec![] }
|
||||
}
|
||||
|
||||
/// Add an element at a position in the foreground.
|
||||
pub fn push(&mut self, pos: Point, element: Element) {
|
||||
self.children.push((pos, FrameChild::Element(element)));
|
||||
Self {
|
||||
size,
|
||||
baseline,
|
||||
elements: vec![],
|
||||
clips: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an element at a position in the background.
|
||||
pub fn prepend(&mut self, pos: Point, element: Element) {
|
||||
self.children.insert(0, (pos, FrameChild::Element(element)));
|
||||
self.elements.insert(0, (pos, element));
|
||||
}
|
||||
|
||||
/// Add an element at a position in the foreground.
|
||||
pub fn push(&mut self, pos: Point, element: Element) {
|
||||
self.elements.push((pos, element));
|
||||
}
|
||||
|
||||
/// Add a frame element.
|
||||
pub fn push_frame(&mut self, pos: Point, subframe: Rc<Self>) {
|
||||
self.children.push((pos, FrameChild::Group(subframe)))
|
||||
self.elements.push((pos, Element::Frame(subframe)))
|
||||
}
|
||||
|
||||
/// Add all elements of another frame, placing them relative to the given
|
||||
/// position.
|
||||
pub fn merge_frame(&mut self, pos: Point, subframe: Self) {
|
||||
if pos == Point::zero() && self.children.is_empty() {
|
||||
self.children = subframe.children;
|
||||
if subframe.clips {
|
||||
self.push_frame(pos, Rc::new(subframe));
|
||||
} else if pos == Point::zero() && self.elements.is_empty() {
|
||||
self.elements = subframe.elements;
|
||||
} else {
|
||||
for (subpos, child) in subframe.children {
|
||||
self.children.push((pos + subpos, child));
|
||||
for (subpos, child) in subframe.elements {
|
||||
self.elements.push((pos + subpos, child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over all elements in the frame and its children.
|
||||
/// An iterator over all non-frame elements in this and nested frames.
|
||||
pub fn elements(&self) -> Elements {
|
||||
Elements { stack: vec![(0, Point::zero(), self)] }
|
||||
}
|
||||
@ -63,7 +72,7 @@ impl Frame {
|
||||
|
||||
impl Debug for Frame {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
struct Children<'a>(&'a [(Point, FrameChild)]);
|
||||
struct Children<'a>(&'a [(Point, Element)]);
|
||||
|
||||
impl Debug for Children<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
@ -74,30 +83,12 @@ impl Debug for Frame {
|
||||
f.debug_struct("Frame")
|
||||
.field("size", &self.size)
|
||||
.field("baseline", &self.baseline)
|
||||
.field("children", &Children(&self.children))
|
||||
.field("clips", &self.clips)
|
||||
.field("children", &Children(&self.elements))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame can contain two different kinds of children: a leaf element or a
|
||||
/// nested frame.
|
||||
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum FrameChild {
|
||||
/// A leaf node in the frame tree.
|
||||
Element(Element),
|
||||
/// An interior group.
|
||||
Group(Rc<Frame>),
|
||||
}
|
||||
|
||||
impl Debug for FrameChild {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Element(element) => element.fmt(f),
|
||||
Self::Group(frame) => frame.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over all elements in a frame, alongside with their positions.
|
||||
pub struct Elements<'a> {
|
||||
stack: Vec<(usize, Point, &'a Frame)>,
|
||||
@ -108,23 +99,21 @@ impl<'a> Iterator for Elements<'a> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let (cursor, offset, frame) = self.stack.last_mut()?;
|
||||
match frame.children.get(*cursor) {
|
||||
Some((pos, FrameChild::Group(f))) => {
|
||||
if let Some((pos, e)) = frame.elements.get(*cursor) {
|
||||
if let Element::Frame(f) = e {
|
||||
let new_offset = *offset + *pos;
|
||||
self.stack.push((0, new_offset, f.as_ref()));
|
||||
self.next()
|
||||
}
|
||||
Some((pos, FrameChild::Element(e))) => {
|
||||
} else {
|
||||
*cursor += 1;
|
||||
Some((*offset + *pos, e))
|
||||
}
|
||||
None => {
|
||||
self.stack.pop();
|
||||
if let Some((cursor, _, _)) = self.stack.last_mut() {
|
||||
*cursor += 1;
|
||||
}
|
||||
self.next()
|
||||
} else {
|
||||
self.stack.pop();
|
||||
if let Some((cursor, _, _)) = self.stack.last_mut() {
|
||||
*cursor += 1;
|
||||
}
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,6 +130,8 @@ pub enum Element {
|
||||
Image(ImageId, Size),
|
||||
/// A link to an external resource.
|
||||
Link(String, Size),
|
||||
/// A subframe, which can be a clipping boundary.
|
||||
Frame(Rc<Frame>),
|
||||
}
|
||||
|
||||
/// A run of shaped text.
|
||||
|
@ -127,11 +127,15 @@ impl Length {
|
||||
self == other || (self - other).to_raw().abs() < 1e-6
|
||||
}
|
||||
|
||||
/// Perform a checked division by a number, returning `None` if the result
|
||||
/// Perform a checked division by a number, returning zero if the result
|
||||
/// is not finite.
|
||||
pub fn div_finite(self, number: f64) -> Option<Self> {
|
||||
pub fn safe_div(self, number: f64) -> Self {
|
||||
let result = self.to_raw() / number;
|
||||
result.is_finite().then(|| Self::raw(result))
|
||||
if result.is_finite() {
|
||||
Self::raw(result)
|
||||
} else {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,8 +16,9 @@ use std::rc::Rc;
|
||||
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::geom::{Linear, Spec};
|
||||
use crate::image::ImageStore;
|
||||
use crate::library::DocumentNode;
|
||||
use crate::library::{DocumentNode, SizedNode};
|
||||
use crate::Context;
|
||||
|
||||
/// Layout a document node into a collection of frames.
|
||||
@ -93,6 +94,23 @@ pub struct PackedNode {
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl PackedNode {
|
||||
/// Force a size for this node.
|
||||
///
|
||||
/// If at least one of `width` and `height` is `Some`, this wraps the node
|
||||
/// in a [`SizedNode`]. Otherwise, it returns the node unchanged.
|
||||
pub fn sized(self, width: Option<Linear>, height: Option<Linear>) -> PackedNode {
|
||||
if width.is_some() || height.is_some() {
|
||||
Layout::pack(SizedNode {
|
||||
sizing: Spec::new(width, height),
|
||||
child: self,
|
||||
})
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for PackedNode {
|
||||
fn layout(
|
||||
&self,
|
||||
|
@ -102,9 +102,9 @@ pub enum LineKind {
|
||||
impl LineDecoration {
|
||||
/// Apply a line decoration to a all text elements in a frame.
|
||||
pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
|
||||
for i in 0 .. frame.children.len() {
|
||||
let (pos, child) = &frame.children[i];
|
||||
if let FrameChild::Element(Element::Text(text)) = child {
|
||||
for i in 0 .. frame.elements.len() {
|
||||
let (pos, child) = &frame.elements[i];
|
||||
if let Element::Text(text) = child {
|
||||
let face = ctx.fonts.get(text.face_id);
|
||||
let metrics = match self.kind {
|
||||
LineKind::Underline => face.underline,
|
||||
|
@ -167,7 +167,8 @@ impl<'a> GridLayouter<'a> {
|
||||
cols.pop();
|
||||
rows.pop();
|
||||
|
||||
// We use the regions only for auto row measurement and constraints.
|
||||
// We use the regions for auto row measurement. Since at that moment,
|
||||
// columns are already sized, we can enable horizontal expansion.
|
||||
let expand = regions.expand;
|
||||
regions.expand = Spec::new(true, false);
|
||||
|
||||
|
@ -9,7 +9,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
let path = args.expect::<Spanned<EcoString>>("path to image file")?;
|
||||
let width = args.named("width")?;
|
||||
let height = args.named("height")?;
|
||||
let fit = args.named("fit")?.unwrap_or_default();
|
||||
|
||||
// Load the image.
|
||||
let full = ctx.make_path(&path.v);
|
||||
let id = ctx.images.load(&full).map_err(|err| {
|
||||
Error::boxed(path.span, match err.kind() {
|
||||
@ -18,10 +20,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(Value::Template(Template::from_inline(move |_| ImageNode {
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
Ok(Value::Template(Template::from_inline(move |_| {
|
||||
ImageNode { id, fit }.pack().sized(width, height)
|
||||
})))
|
||||
}
|
||||
|
||||
@ -30,10 +30,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
pub struct ImageNode {
|
||||
/// The id of the image file.
|
||||
pub id: ImageId,
|
||||
/// The fixed width, if any.
|
||||
pub width: Option<Linear>,
|
||||
/// The fixed height, if any.
|
||||
pub height: Option<Linear>,
|
||||
/// How the image should adjust itself to a given area.
|
||||
pub fit: ImageFit,
|
||||
}
|
||||
|
||||
impl Layout for ImageNode {
|
||||
@ -42,36 +40,77 @@ impl Layout for ImageNode {
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let &Regions { current, expand, .. } = regions;
|
||||
|
||||
let img = ctx.images.get(self.id);
|
||||
let pixel_size = Spec::new(img.width() as f64, img.height() as f64);
|
||||
let pixel_ratio = pixel_size.x / pixel_size.y;
|
||||
let pixel_w = img.width() as f64;
|
||||
let pixel_h = img.height() as f64;
|
||||
|
||||
let width = self.width.map(|w| w.resolve(regions.base.w));
|
||||
let height = self.height.map(|w| w.resolve(regions.base.h));
|
||||
let region_ratio = current.w / current.h;
|
||||
let pixel_ratio = pixel_w / pixel_h;
|
||||
let wide = region_ratio < pixel_ratio;
|
||||
|
||||
let mut cts = Constraints::new(regions.expand);
|
||||
cts.set_base_if_linear(regions.base, Spec::new(self.width, self.height));
|
||||
|
||||
let size = match (width, height) {
|
||||
(Some(width), Some(height)) => Size::new(width, height),
|
||||
(Some(width), None) => Size::new(width, width / pixel_ratio),
|
||||
(None, Some(height)) => Size::new(height * pixel_ratio, height),
|
||||
(None, None) => {
|
||||
cts.exact.x = Some(regions.current.w);
|
||||
if regions.current.w.is_finite() {
|
||||
// Fit to width.
|
||||
Size::new(regions.current.w, regions.current.w / pixel_ratio)
|
||||
} else {
|
||||
// Unbounded width, we have to make up something,
|
||||
// so it is 1pt per pixel.
|
||||
pixel_size.map(Length::pt).to_size()
|
||||
}
|
||||
}
|
||||
// The space into which the image will be placed according to its fit.
|
||||
let canvas = if expand.x && expand.y {
|
||||
current
|
||||
} else if expand.x || (wide && current.w.is_finite()) {
|
||||
Size::new(current.w, current.w.safe_div(pixel_ratio))
|
||||
} else if current.h.is_finite() {
|
||||
Size::new(current.h * pixel_ratio, current.h)
|
||||
} else {
|
||||
Size::new(Length::pt(pixel_w), Length::pt(pixel_h))
|
||||
};
|
||||
|
||||
let mut frame = Frame::new(size, size.h);
|
||||
frame.push(Point::zero(), Element::Image(self.id, size));
|
||||
// The actual size of the fitted image.
|
||||
let size = match self.fit {
|
||||
ImageFit::Contain | ImageFit::Cover => {
|
||||
if wide == (self.fit == ImageFit::Contain) {
|
||||
Size::new(canvas.w, canvas.w / pixel_ratio)
|
||||
} else {
|
||||
Size::new(canvas.h * pixel_ratio, canvas.h)
|
||||
}
|
||||
}
|
||||
ImageFit::Stretch => canvas,
|
||||
};
|
||||
|
||||
// The position of the image so that it is centered in the canvas.
|
||||
let mut frame = Frame::new(canvas, canvas.h);
|
||||
frame.clips = self.fit == ImageFit::Cover;
|
||||
frame.push(
|
||||
Point::new((canvas.w - size.w) / 2.0, (canvas.h - size.h) / 2.0),
|
||||
Element::Image(self.id, size),
|
||||
);
|
||||
|
||||
let mut cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.to_spec().map(Some);
|
||||
vec![frame.constrain(cts)]
|
||||
}
|
||||
}
|
||||
|
||||
/// How an image should adjust itself to a given area.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ImageFit {
|
||||
/// The image should be fully contained in the area.
|
||||
Contain,
|
||||
/// The image should completely cover the area.
|
||||
Cover,
|
||||
/// The image should be stretched so that it exactly fills the area.
|
||||
Stretch,
|
||||
}
|
||||
|
||||
castable! {
|
||||
ImageFit,
|
||||
Expected: "string",
|
||||
Value::Str(string) => match string.as_str() {
|
||||
"contain" => Self::Contain,
|
||||
"cover" => Self::Cover,
|
||||
"stretch" => Self::Stretch,
|
||||
_ => Err(r#"expected "contain", "cover" or "stretch""#)?,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for ImageFit {
|
||||
fn default() -> Self {
|
||||
Self::Contain
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,7 @@ impl Layout for PadNode {
|
||||
frames.iter_mut().zip(regions.iter())
|
||||
{
|
||||
fn solve_axis(length: Length, padding: Linear) -> Length {
|
||||
(length + padding.abs)
|
||||
.div_finite(1.0 - padding.rel.get())
|
||||
.unwrap_or_default()
|
||||
(length + padding.abs).safe_div(1.0 - padding.rel.get())
|
||||
}
|
||||
|
||||
// Solve for the size `padded` that satisfies (approximately):
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use super::prelude::*;
|
||||
use super::{PadNode, SizedNode};
|
||||
use super::PadNode;
|
||||
use crate::util::RcExt;
|
||||
|
||||
/// `rect`: A rectangle with optional content.
|
||||
@ -65,20 +65,13 @@ fn shape_impl(
|
||||
let fill = fill.unwrap_or(Color::Rgba(RgbaColor::gray(175)));
|
||||
|
||||
Value::Template(Template::from_inline(move |style| {
|
||||
let shape = Layout::pack(ShapeNode {
|
||||
ShapeNode {
|
||||
kind,
|
||||
fill: Some(Paint::Color(fill)),
|
||||
child: body.as_ref().map(|body| body.pack(style)),
|
||||
});
|
||||
|
||||
if width.is_some() || height.is_some() {
|
||||
Layout::pack(SizedNode {
|
||||
sizing: Spec::new(width, height),
|
||||
child: shape,
|
||||
})
|
||||
} else {
|
||||
shape
|
||||
}
|
||||
.pack()
|
||||
.sized(width, height)
|
||||
}))
|
||||
}
|
||||
|
||||
@ -112,7 +105,7 @@ impl Layout for ShapeNode {
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
// Layout.
|
||||
// Layout, either with or without child.
|
||||
let mut frame = if let Some(child) = &self.child {
|
||||
let mut node: &dyn Layout = child;
|
||||
|
||||
@ -141,15 +134,18 @@ impl Layout for ShapeNode {
|
||||
frames = node.layout(ctx, &pod);
|
||||
}
|
||||
|
||||
// Validate and set constraints.
|
||||
assert_eq!(frames.len(), 1);
|
||||
// Extract the frame.
|
||||
Rc::take(frames.into_iter().next().unwrap().item)
|
||||
} else {
|
||||
// When there's no child, fill the area if expansion is on,
|
||||
// otherwise fall back to a default size.
|
||||
let default = Length::pt(30.0);
|
||||
let size = Size::new(
|
||||
if regions.expand.x {
|
||||
regions.current.w
|
||||
} else {
|
||||
// For rectangle and ellipse, the default shape is a bit
|
||||
// wider than high.
|
||||
match self.kind {
|
||||
ShapeKind::Square | ShapeKind::Circle => default,
|
||||
ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * default,
|
||||
@ -161,7 +157,7 @@ impl Layout for ShapeNode {
|
||||
Frame::new(size, size.h)
|
||||
};
|
||||
|
||||
// Add background shape if desired.
|
||||
// Add background fill if desired.
|
||||
if let Some(fill) = self.fill {
|
||||
let (pos, geometry) = match self.kind {
|
||||
ShapeKind::Square | ShapeKind::Rect => {
|
||||
@ -175,11 +171,10 @@ impl Layout for ShapeNode {
|
||||
frame.prepend(pos, Element::Geometry(geometry, fill));
|
||||
}
|
||||
|
||||
// Generate tight constraints for now.
|
||||
// Return tight constraints for now.
|
||||
let mut cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.to_spec().map(Some);
|
||||
cts.base = regions.base.to_spec().map(Some);
|
||||
|
||||
vec![frame.constrain(cts)]
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,7 @@ pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
let height = args.named("height")?;
|
||||
let body: Template = args.find().unwrap_or_default();
|
||||
Ok(Value::Template(Template::from_inline(move |style| {
|
||||
let child = body.pack(style);
|
||||
if width.is_some() || height.is_some() {
|
||||
Layout::pack(SizedNode { sizing: Spec::new(width, height), child })
|
||||
} else {
|
||||
child
|
||||
}
|
||||
body.pack(style).sized(width, height)
|
||||
})))
|
||||
}
|
||||
|
||||
@ -21,12 +16,7 @@ pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
let height = args.named("height")?;
|
||||
let body: Template = args.find().unwrap_or_default();
|
||||
Ok(Value::Template(Template::from_block(move |style| {
|
||||
let child = body.pack(style);
|
||||
if width.is_some() || height.is_some() {
|
||||
Layout::pack(SizedNode { sizing: Spec::new(width, height), child })
|
||||
} else {
|
||||
child
|
||||
}
|
||||
body.pack(style).sized(width, height)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ impl Layout for MoveNode {
|
||||
self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(),
|
||||
);
|
||||
|
||||
for (point, _) in &mut Rc::make_mut(frame).children {
|
||||
for (point, _) in &mut Rc::make_mut(frame).elements {
|
||||
*point += offset;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 181 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 317 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
@ -5,30 +5,42 @@
|
||||
|
||||
// Load an RGBA PNG image.
|
||||
#image("../../res/rhino.png")
|
||||
#pagebreak()
|
||||
|
||||
// Load an RGB JPEG image.
|
||||
#page(height: 60pt)
|
||||
#image("../../res/tiger.jpg")
|
||||
|
||||
---
|
||||
// Test configuring the size and fitting behaviour of images.
|
||||
|
||||
// Set width explicitly.
|
||||
#image("../../res/rhino.png", width: 50pt)
|
||||
|
||||
// Set height explicitly.
|
||||
#image("../../res/rhino.png", height: 50pt)
|
||||
// Set width and height explicitly.
|
||||
#image("../../res/rhino.png", width: 30pt)
|
||||
#image("../../res/rhino.png", height: 30pt)
|
||||
|
||||
// Set width and height explicitly and force stretching.
|
||||
#image("../../res/rhino.png", width: 25pt, height: 50pt)
|
||||
#image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch")
|
||||
|
||||
// Make sure the bounding-box of the image is correct.
|
||||
#align(bottom, right)
|
||||
#image("../../res/tiger.jpg", width: 60pt)
|
||||
#image("../../res/tiger.jpg", width: 40pt)
|
||||
|
||||
---
|
||||
// Does not fit to height of page.
|
||||
// Test all three fit modes.
|
||||
#page(height: 50pt, margins: 0pt)
|
||||
#grid(
|
||||
columns: 3,
|
||||
rows: 100%,
|
||||
gutter: 3pt,
|
||||
image("../../res/tiger.jpg", fit: "contain"),
|
||||
image("../../res/tiger.jpg", fit: "cover"),
|
||||
image("../../res/tiger.jpg", fit: "stretch"),
|
||||
)
|
||||
|
||||
---
|
||||
// Does not fit to remaining height of page.
|
||||
#page(height: 60pt)
|
||||
Stuff \
|
||||
Stuff
|
||||
#image("../../res/rhino.png")
|
||||
|
||||
---
|
||||
|
@ -23,7 +23,7 @@
|
||||
columns: 4 * (1fr,),
|
||||
row-gutter: 10pt,
|
||||
column-gutter: (0pt, 10%),
|
||||
image("../../res/rhino.png"),
|
||||
align(top, image("../../res/rhino.png")),
|
||||
align(right, rect(width: 100%, fill: eastern)[LoL]),
|
||||
[rofl],
|
||||
[\ A] * 3,
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
#page(width: 50pt, margins: 0pt)
|
||||
#stack(dir: btt, ..items)
|
||||
#pagebreak()
|
||||
|
||||
---
|
||||
// Test spacing.
|
||||
|
149
tests/typeset.rs
@ -13,9 +13,7 @@ use typst::diag::Error;
|
||||
use typst::eval::Value;
|
||||
use typst::font::Face;
|
||||
use typst::frame::{Element, Frame, Geometry, Text};
|
||||
use typst::geom::{
|
||||
self, Color, Length, Paint, PathElement, Point, RgbaColor, Sides, Size,
|
||||
};
|
||||
use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size};
|
||||
use typst::image::Image;
|
||||
use typst::layout::layout;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
@ -390,66 +388,96 @@ fn draw(ctx: &Context, frames: &[Rc<Frame>], dpp: f32) -> sk::Pixmap {
|
||||
let width = 2.0 * pad + frames.iter().map(|l| l.size.w).max().unwrap_or_default();
|
||||
let height = pad + frames.iter().map(|l| l.size.h + pad).sum::<Length>();
|
||||
|
||||
let pixel_width = (dpp * width.to_pt() as f32) as u32;
|
||||
let pixel_height = (dpp * height.to_pt() as f32) as u32;
|
||||
if pixel_width > 4000 || pixel_height > 4000 {
|
||||
let pxw = (dpp * width.to_pt() as f32) as u32;
|
||||
let pxh = (dpp * height.to_pt() as f32) as u32;
|
||||
if pxw > 4000 || pxh > 4000 {
|
||||
panic!(
|
||||
"overlarge image: {} by {} ({:?} x {:?})",
|
||||
pixel_width, pixel_height, width, height,
|
||||
pxw, pxh, width, height,
|
||||
);
|
||||
}
|
||||
|
||||
let mut canvas = sk::Pixmap::new(pixel_width, pixel_height).unwrap();
|
||||
let ts = sk::Transform::from_scale(dpp, dpp);
|
||||
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
|
||||
canvas.fill(sk::Color::BLACK);
|
||||
|
||||
let mut origin = Point::splat(pad);
|
||||
let mut mask = sk::ClipMask::new();
|
||||
let rect = sk::Rect::from_xywh(0.0, 0.0, pxw as f32, pxh as f32).unwrap();
|
||||
let path = sk::PathBuilder::from_rect(rect);
|
||||
mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false);
|
||||
|
||||
let mut ts = sk::Transform::from_scale(dpp, dpp)
|
||||
.pre_translate(pad.to_pt() as f32, pad.to_pt() as f32);
|
||||
|
||||
for frame in frames {
|
||||
let mut paint = sk::Paint::default();
|
||||
paint.set_color(sk::Color::WHITE);
|
||||
canvas.fill_rect(
|
||||
sk::Rect::from_xywh(
|
||||
origin.x.to_pt() as f32,
|
||||
origin.y.to_pt() as f32,
|
||||
frame.size.w.to_pt() as f32,
|
||||
frame.size.h.to_pt() as f32,
|
||||
)
|
||||
.unwrap(),
|
||||
&paint,
|
||||
ts,
|
||||
None,
|
||||
);
|
||||
let mut background = sk::Paint::default();
|
||||
background.set_color(sk::Color::WHITE);
|
||||
|
||||
for (pos, element) in frame.elements() {
|
||||
let global = origin + pos;
|
||||
let x = global.x.to_pt() as f32;
|
||||
let y = global.y.to_pt() as f32;
|
||||
let ts = ts.pre_translate(x, y);
|
||||
match *element {
|
||||
Element::Text(ref text) => {
|
||||
draw_text(&mut canvas, ts, ctx.fonts.get(text.face_id), text);
|
||||
}
|
||||
Element::Geometry(ref geometry, paint) => {
|
||||
draw_geometry(&mut canvas, ts, geometry, paint);
|
||||
}
|
||||
Element::Image(id, size) => {
|
||||
draw_image(&mut canvas, ts, ctx.images.get(id), size);
|
||||
}
|
||||
Element::Link(_, s) => {
|
||||
let outline = Geometry::Rect(s);
|
||||
let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
|
||||
draw_geometry(&mut canvas, ts, &outline, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
let w = frame.size.w.to_pt() as f32;
|
||||
let h = frame.size.h.to_pt() as f32;
|
||||
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||
canvas.fill_rect(rect, &background, ts, None);
|
||||
|
||||
origin.y += frame.size.h + pad;
|
||||
draw_frame(&mut canvas, ts, &mask, ctx, frame);
|
||||
ts = ts.pre_translate(0.0, (frame.size.h + pad).to_pt() as f32);
|
||||
}
|
||||
|
||||
canvas
|
||||
}
|
||||
|
||||
fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Text) {
|
||||
fn draw_frame(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: &sk::ClipMask,
|
||||
ctx: &Context,
|
||||
frame: &Frame,
|
||||
) {
|
||||
let mut storage;
|
||||
let mask = if frame.clips {
|
||||
let w = frame.size.w.to_pt() as f32;
|
||||
let h = frame.size.h.to_pt() as f32;
|
||||
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||
let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap();
|
||||
storage = mask.clone();
|
||||
storage.intersect_path(&path, sk::FillRule::default(), false);
|
||||
&storage
|
||||
} else {
|
||||
mask
|
||||
};
|
||||
|
||||
for (pos, element) in &frame.elements {
|
||||
let x = pos.x.to_pt() as f32;
|
||||
let y = pos.y.to_pt() as f32;
|
||||
let ts = ts.pre_translate(x, y);
|
||||
|
||||
match *element {
|
||||
Element::Text(ref text) => {
|
||||
draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text);
|
||||
}
|
||||
Element::Geometry(ref geometry, paint) => {
|
||||
draw_geometry(canvas, ts, mask, geometry, paint);
|
||||
}
|
||||
Element::Image(id, size) => {
|
||||
draw_image(canvas, ts, mask, ctx.images.get(id), size);
|
||||
}
|
||||
Element::Link(_, s) => {
|
||||
let outline = Geometry::Rect(s);
|
||||
let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
|
||||
draw_geometry(canvas, ts, mask, &outline, paint);
|
||||
}
|
||||
Element::Frame(ref frame) => {
|
||||
draw_frame(canvas, ts, mask, ctx, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: &sk::ClipMask,
|
||||
face: &Face,
|
||||
text: &Text,
|
||||
) {
|
||||
let ttf = face.ttf();
|
||||
let size = text.size.to_pt() as f32;
|
||||
let units_per_em = ttf.units_per_em() as f32;
|
||||
@ -481,7 +509,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
|
||||
if let Some(fill) = &node.fill {
|
||||
let path = convert_usvg_path(&node.data);
|
||||
let (paint, fill_rule) = convert_usvg_fill(fill);
|
||||
canvas.fill_path(&path, &paint, fill_rule, ts, None);
|
||||
canvas.fill_path(&path, &paint, fill_rule, ts, Some(mask));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -497,7 +525,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
|
||||
let dx = (raster.x as f32) / (img.width() as f32) * size;
|
||||
let dy = (raster.y as f32) / (img.height() as f32) * size;
|
||||
let ts = ts.pre_translate(dx, -size - dy);
|
||||
draw_image(canvas, ts, &img, Size::new(w, h));
|
||||
draw_image(canvas, ts, mask, &img, Size::new(w, h));
|
||||
} else {
|
||||
// Otherwise, draw normal outline.
|
||||
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
|
||||
@ -507,7 +535,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
|
||||
let path = builder.0.finish().unwrap();
|
||||
let mut paint = convert_typst_paint(text.fill);
|
||||
paint.anti_alias = true;
|
||||
canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, None);
|
||||
canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask));
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,6 +546,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
|
||||
fn draw_geometry(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: &sk::ClipMask,
|
||||
geometry: &Geometry,
|
||||
paint: Paint,
|
||||
) {
|
||||
@ -529,11 +558,11 @@ fn draw_geometry(
|
||||
let w = width.to_pt() as f32;
|
||||
let h = height.to_pt() as f32;
|
||||
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||
canvas.fill_rect(rect, &paint, ts, None);
|
||||
canvas.fill_rect(rect, &paint, ts, Some(mask));
|
||||
}
|
||||
Geometry::Ellipse(size) => {
|
||||
let path = convert_typst_path(&geom::Path::ellipse(size));
|
||||
canvas.fill_path(&path, &paint, rule, ts, None);
|
||||
canvas.fill_path(&path, &paint, rule, ts, Some(mask));
|
||||
}
|
||||
Geometry::Line(target, thickness) => {
|
||||
let path = {
|
||||
@ -544,16 +573,22 @@ fn draw_geometry(
|
||||
|
||||
let mut stroke = sk::Stroke::default();
|
||||
stroke.width = thickness.to_pt() as f32;
|
||||
canvas.stroke_path(&path, &paint, &stroke, ts, None);
|
||||
canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask));
|
||||
}
|
||||
Geometry::Path(ref path) => {
|
||||
let path = convert_typst_path(path);
|
||||
canvas.fill_path(&path, &paint, rule, ts, None);
|
||||
canvas.fill_path(&path, &paint, rule, ts, Some(mask));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn draw_image(canvas: &mut sk::Pixmap, ts: sk::Transform, img: &Image, size: Size) {
|
||||
fn draw_image(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: &sk::ClipMask,
|
||||
img: &Image,
|
||||
size: Size,
|
||||
) {
|
||||
let mut pixmap = sk::Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
|
||||
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
|
||||
let Rgba([r, g, b, a]) = src;
|
||||
@ -575,7 +610,7 @@ fn draw_image(canvas: &mut sk::Pixmap, ts: sk::Transform, img: &Image, size: Siz
|
||||
);
|
||||
|
||||
let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height).unwrap();
|
||||
canvas.fill_rect(rect, &paint, ts, None);
|
||||
canvas.fill_rect(rect, &paint, ts, Some(mask));
|
||||
}
|
||||
|
||||
fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> {
|
||||
|