mirror of
https://github.com/typst/typst
synced 2025-07-11 22:52:53 +08:00
Support images in HTML export (#6578)
This commit is contained in:
parent
a45c3388a6
commit
3aa7e861e7
@ -1,11 +1,72 @@
|
|||||||
//! Conversion from Typst data types into CSS data types.
|
//! Conversion from Typst data types into CSS data types.
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display, Write};
|
||||||
|
|
||||||
use typst_library::layout::Length;
|
use ecow::EcoString;
|
||||||
|
use typst_library::html::{attr, HtmlElem};
|
||||||
|
use typst_library::layout::{Length, Rel};
|
||||||
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
|
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
/// Additional methods for [`HtmlElem`].
|
||||||
|
pub trait HtmlElemExt {
|
||||||
|
/// Adds the styles to an element if the property list is non-empty.
|
||||||
|
fn with_styles(self, properties: Properties) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlElemExt for HtmlElem {
|
||||||
|
/// Adds CSS styles to an element.
|
||||||
|
fn with_styles(self, properties: Properties) -> Self {
|
||||||
|
if let Some(value) = properties.into_inline_styles() {
|
||||||
|
self.with_attr(attr::style, value)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of CSS properties with values.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Properties(EcoString);
|
||||||
|
|
||||||
|
impl Properties {
|
||||||
|
/// Creates an empty list.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new property to the list.
|
||||||
|
pub fn push(&mut self, property: &str, value: impl Display) {
|
||||||
|
if !self.0.is_empty() {
|
||||||
|
self.0.push_str("; ");
|
||||||
|
}
|
||||||
|
write!(&mut self.0, "{property}: {value}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new property in builder-style.
|
||||||
|
#[expect(unused)]
|
||||||
|
pub fn with(mut self, property: &str, value: impl Display) -> Self {
|
||||||
|
self.push(property, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns this into a string suitable for use as an inline `style`
|
||||||
|
/// attribute.
|
||||||
|
pub fn into_inline_styles(self) -> Option<EcoString> {
|
||||||
|
(!self.0.is_empty()).then_some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rel(rel: Rel) -> impl Display {
|
||||||
|
typst_utils::display(move |f| match (rel.abs.is_zero(), rel.rel.is_zero()) {
|
||||||
|
(false, false) => {
|
||||||
|
write!(f, "calc({}% + {})", rel.rel.get(), length(rel.abs))
|
||||||
|
}
|
||||||
|
(true, false) => write!(f, "{}%", rel.rel.get()),
|
||||||
|
(_, true) => write!(f, "{}", length(rel.abs)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn length(length: Length) -> impl Display {
|
pub fn length(length: Length) -> impl Display {
|
||||||
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
|
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
|
||||||
(false, false) => {
|
(false, false) => {
|
||||||
|
@ -3,12 +3,12 @@ use std::num::NonZeroUsize;
|
|||||||
use ecow::{eco_format, EcoVec};
|
use ecow::{eco_format, EcoVec};
|
||||||
use typst_library::diag::warning;
|
use typst_library::diag::warning;
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Content, NativeElement, NativeRuleMap, ShowFn, StyleChain, Target,
|
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
|
||||||
};
|
};
|
||||||
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
|
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
|
||||||
use typst_library::introspection::{Counter, Locator};
|
use typst_library::introspection::{Counter, Locator};
|
||||||
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
||||||
use typst_library::layout::OuterVAlignment;
|
use typst_library::layout::{OuterVAlignment, Sizing};
|
||||||
use typst_library::model::{
|
use typst_library::model::{
|
||||||
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
||||||
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
|
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
|
||||||
@ -18,6 +18,9 @@ use typst_library::text::{
|
|||||||
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
||||||
SubElem, SuperElem, UnderlineElem,
|
SubElem, SuperElem, UnderlineElem,
|
||||||
};
|
};
|
||||||
|
use typst_library::visualize::ImageElem;
|
||||||
|
|
||||||
|
use crate::css::{self, HtmlElemExt};
|
||||||
|
|
||||||
/// Register show rules for the [HTML target](Target::Html).
|
/// Register show rules for the [HTML target](Target::Html).
|
||||||
pub fn register(rules: &mut NativeRuleMap) {
|
pub fn register(rules: &mut NativeRuleMap) {
|
||||||
@ -47,6 +50,9 @@ pub fn register(rules: &mut NativeRuleMap) {
|
|||||||
rules.register(Html, HIGHLIGHT_RULE);
|
rules.register(Html, HIGHLIGHT_RULE);
|
||||||
rules.register(Html, RAW_RULE);
|
rules.register(Html, RAW_RULE);
|
||||||
rules.register(Html, RAW_LINE_RULE);
|
rules.register(Html, RAW_LINE_RULE);
|
||||||
|
|
||||||
|
// Visualize.
|
||||||
|
rules.register(Html, IMAGE_RULE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
||||||
@ -338,7 +344,7 @@ fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content {
|
|||||||
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
||||||
let cell = cell.body.clone();
|
let cell = cell.body.clone();
|
||||||
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
|
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
|
||||||
let mut attrs = HtmlAttrs::default();
|
let mut attrs = HtmlAttrs::new();
|
||||||
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
|
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
|
||||||
if let Some(colspan) = span(cell.colspan.get(styles)) {
|
if let Some(colspan) = span(cell.colspan.get(styles)) {
|
||||||
attrs.push(attr::colspan, colspan);
|
attrs.push(attr::colspan, colspan);
|
||||||
@ -409,3 +415,36 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
||||||
|
|
||||||
|
const IMAGE_RULE: ShowFn<ImageElem> = |elem, engine, styles| {
|
||||||
|
let image = elem.decode(engine, styles)?;
|
||||||
|
|
||||||
|
let mut attrs = HtmlAttrs::new();
|
||||||
|
attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image));
|
||||||
|
|
||||||
|
if let Some(alt) = elem.alt.get_cloned(styles) {
|
||||||
|
attrs.push(attr::alt, alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut inline = css::Properties::new();
|
||||||
|
|
||||||
|
// TODO: Exclude in semantic profile.
|
||||||
|
if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) {
|
||||||
|
inline.push("image-rendering", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Exclude in semantic profile?
|
||||||
|
match elem.width.get(styles) {
|
||||||
|
Smart::Auto => {}
|
||||||
|
Smart::Custom(rel) => inline.push("width", css::rel(rel)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Exclude in semantic profile?
|
||||||
|
match elem.height.get(styles) {
|
||||||
|
Sizing::Auto => {}
|
||||||
|
Sizing::Rel(rel) => inline.push("height", css::rel(rel)),
|
||||||
|
Sizing::Fr(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack())
|
||||||
|
};
|
||||||
|
@ -1,18 +1,11 @@
|
|||||||
use std::ffi::OsStr;
|
use typst_library::diag::SourceResult;
|
||||||
|
|
||||||
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
|
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
||||||
};
|
};
|
||||||
use typst_library::loading::DataSource;
|
use typst_library::visualize::{Curve, Image, ImageElem, ImageFit};
|
||||||
use typst_library::text::families;
|
|
||||||
use typst_library::visualize::{
|
|
||||||
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
|
|
||||||
RasterImage, SvgImage, VectorFormat,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Layout the image.
|
/// Layout the image.
|
||||||
#[typst_macros::time(span = elem.span())]
|
#[typst_macros::time(span = elem.span())]
|
||||||
@ -23,53 +16,7 @@ pub fn layout_image(
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
region: Region,
|
region: Region,
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
let span = elem.span();
|
let image = elem.decode(engine, styles)?;
|
||||||
|
|
||||||
// Take the format that was explicitly defined, or parse the extension,
|
|
||||||
// or try to detect the format.
|
|
||||||
let Derived { source, derived: loaded } = &elem.source;
|
|
||||||
let format = match elem.format.get(styles) {
|
|
||||||
Smart::Custom(v) => v,
|
|
||||||
Smart::Auto => determine_format(source, &loaded.data).at(span)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Warn the user if the image contains a foreign object. Not perfect
|
|
||||||
// because the svg could also be encoded, but that's an edge case.
|
|
||||||
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
|
||||||
let has_foreign_object =
|
|
||||||
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
|
|
||||||
|
|
||||||
if has_foreign_object {
|
|
||||||
engine.sink.warn(warning!(
|
|
||||||
span,
|
|
||||||
"image contains foreign object";
|
|
||||||
hint: "SVG images with foreign objects might render incorrectly in typst";
|
|
||||||
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the image itself.
|
|
||||||
let kind = match format {
|
|
||||||
ImageFormat::Raster(format) => ImageKind::Raster(
|
|
||||||
RasterImage::new(
|
|
||||||
loaded.data.clone(),
|
|
||||||
format,
|
|
||||||
elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
|
|
||||||
)
|
|
||||||
.at(span)?,
|
|
||||||
),
|
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
|
|
||||||
SvgImage::with_fonts(
|
|
||||||
loaded.data.clone(),
|
|
||||||
engine.world,
|
|
||||||
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.within(loaded)?,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles));
|
|
||||||
|
|
||||||
// Determine the image's pixel aspect ratio.
|
// Determine the image's pixel aspect ratio.
|
||||||
let pxw = image.width();
|
let pxw = image.width();
|
||||||
@ -122,7 +69,7 @@ pub fn layout_image(
|
|||||||
// the frame to the target size, center aligning the image in the
|
// the frame to the target size, center aligning the image in the
|
||||||
// process.
|
// process.
|
||||||
let mut frame = Frame::soft(fitted);
|
let mut frame = Frame::soft(fitted);
|
||||||
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
|
frame.push(Point::zero(), FrameItem::Image(image, fitted, elem.span()));
|
||||||
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
||||||
|
|
||||||
// Create a clipping group if only part of the image should be visible.
|
// Create a clipping group if only part of the image should be visible.
|
||||||
@ -132,25 +79,3 @@ pub fn layout_image(
|
|||||||
|
|
||||||
Ok(frame)
|
Ok(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to determine the image format based on the data.
|
|
||||||
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
|
|
||||||
if let DataSource::Path(path) = source {
|
|
||||||
let ext = std::path::Path::new(path.as_str())
|
|
||||||
.extension()
|
|
||||||
.and_then(OsStr::to_str)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
match ext.as_str() {
|
|
||||||
"png" => return Ok(ExchangeFormat::Png.into()),
|
|
||||||
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
|
|
||||||
"gif" => return Ok(ExchangeFormat::Gif.into()),
|
|
||||||
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
|
|
||||||
"webp" => return Ok(ExchangeFormat::Webp.into()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
|
|
||||||
}
|
|
||||||
|
@ -165,6 +165,11 @@ cast! {
|
|||||||
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
|
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
|
||||||
|
|
||||||
impl HtmlAttrs {
|
impl HtmlAttrs {
|
||||||
|
/// Creates an empty attribute list.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Add an attribute.
|
/// Add an attribute.
|
||||||
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||||
self.0.push((attr, value.into()));
|
self.0.push((attr, value.into()));
|
||||||
|
@ -8,6 +8,7 @@ pub use self::raster::{
|
|||||||
};
|
};
|
||||||
pub use self::svg::SvgImage;
|
pub use self::svg::SvgImage;
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -15,14 +16,16 @@ use ecow::EcoString;
|
|||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
|
||||||
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart,
|
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart,
|
||||||
|
StyleChain,
|
||||||
};
|
};
|
||||||
use crate::layout::{Length, Rel, Sizing};
|
use crate::layout::{Length, Rel, Sizing};
|
||||||
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
|
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
|
||||||
use crate::model::Figurable;
|
use crate::model::Figurable;
|
||||||
use crate::text::LocalName;
|
use crate::text::{families, LocalName};
|
||||||
|
|
||||||
/// A raster or vector graphic.
|
/// A raster or vector graphic.
|
||||||
///
|
///
|
||||||
@ -217,6 +220,81 @@ impl ImageElem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Packed<ImageElem> {
|
||||||
|
/// Decodes the image.
|
||||||
|
pub fn decode(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Image> {
|
||||||
|
let span = self.span();
|
||||||
|
let loaded = &self.source.derived;
|
||||||
|
let format = self.determine_format(styles).at(span)?;
|
||||||
|
|
||||||
|
// Warn the user if the image contains a foreign object. Not perfect
|
||||||
|
// because the svg could also be encoded, but that's an edge case.
|
||||||
|
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||||
|
let has_foreign_object =
|
||||||
|
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
|
||||||
|
|
||||||
|
if has_foreign_object {
|
||||||
|
engine.sink.warn(warning!(
|
||||||
|
span,
|
||||||
|
"image contains foreign object";
|
||||||
|
hint: "SVG images with foreign objects might render incorrectly in typst";
|
||||||
|
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the image itself.
|
||||||
|
let kind = match format {
|
||||||
|
ImageFormat::Raster(format) => ImageKind::Raster(
|
||||||
|
RasterImage::new(
|
||||||
|
loaded.data.clone(),
|
||||||
|
format,
|
||||||
|
self.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
|
||||||
|
)
|
||||||
|
.at(span)?,
|
||||||
|
),
|
||||||
|
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
|
||||||
|
SvgImage::with_fonts(
|
||||||
|
loaded.data.clone(),
|
||||||
|
engine.world,
|
||||||
|
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.within(loaded)?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to determine the image format based on the format that was
|
||||||
|
/// explicitly defined, or else the extension, or else the data.
|
||||||
|
fn determine_format(&self, styles: StyleChain) -> StrResult<ImageFormat> {
|
||||||
|
if let Smart::Custom(v) = self.format.get(styles) {
|
||||||
|
return Ok(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Derived { source, derived: loaded } = &self.source;
|
||||||
|
if let DataSource::Path(path) = source {
|
||||||
|
let ext = std::path::Path::new(path.as_str())
|
||||||
|
.extension()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
match ext.as_str() {
|
||||||
|
"png" => return Ok(ExchangeFormat::Png.into()),
|
||||||
|
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
|
||||||
|
"gif" => return Ok(ExchangeFormat::Gif.into()),
|
||||||
|
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
|
||||||
|
"webp" => return Ok(ExchangeFormat::Webp.into()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ImageFormat::detect(&loaded.data).ok_or("unknown image format")?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl LocalName for Packed<ImageElem> {
|
impl LocalName for Packed<ImageElem> {
|
||||||
const KEY: &'static str = "figure";
|
const KEY: &'static str = "figure";
|
||||||
}
|
}
|
||||||
|
@ -18,21 +18,27 @@ impl SVGRenderer {
|
|||||||
self.xml.write_attribute("width", &size.x.to_pt());
|
self.xml.write_attribute("width", &size.x.to_pt());
|
||||||
self.xml.write_attribute("height", &size.y.to_pt());
|
self.xml.write_attribute("height", &size.y.to_pt());
|
||||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||||
match image.scaling() {
|
if let Some(value) = convert_image_scaling(image.scaling()) {
|
||||||
Smart::Auto => {}
|
self.xml
|
||||||
Smart::Custom(ImageScaling::Smooth) => {
|
.write_attribute("style", &format_args!("image-rendering: {value}"))
|
||||||
// This is still experimental and not implemented in all major browsers.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
|
||||||
self.xml.write_attribute("style", "image-rendering: smooth")
|
|
||||||
}
|
|
||||||
Smart::Custom(ImageScaling::Pixelated) => {
|
|
||||||
self.xml.write_attribute("style", "image-rendering: pixelated")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an image scaling to a CSS `image-rendering` propery value.
|
||||||
|
pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> Option<&'static str> {
|
||||||
|
match scaling {
|
||||||
|
Smart::Auto => None,
|
||||||
|
Smart::Custom(ImageScaling::Smooth) => {
|
||||||
|
// This is still experimental and not implemented in all major browsers.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
||||||
|
Some("smooth")
|
||||||
|
}
|
||||||
|
Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Encode an image into a data URL. The format of the URL is
|
/// Encode an image into a data URL. The format of the URL is
|
||||||
/// `data:image/{format};base64,`.
|
/// `data:image/{format};base64,`.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
|
@ -5,6 +5,8 @@ mod paint;
|
|||||||
mod shape;
|
mod shape;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
|
pub use image::{convert_image_scaling, convert_image_to_base64_url};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Display, Formatter, Write};
|
use std::fmt::{self, Display, Formatter, Write};
|
||||||
|
|
||||||
|
8
tests/ref/html/image-jpg-html-base64.html
Normal file
8
tests/ref/html/image-jpg-html-base64.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAA3ADcAAD/4QBiRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAEAAAITAAMAAAABAAEAAAAAAAAAAADcAAAAAQAAANwAAAAB/8AACwgAUAAwAQERAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/aAAgBAQAAPwD5/ooooooooooooor7+ooor4Bor7+ooor4Booor7+or4Booor7+or4Bor7+ooor4Bor7+ooor4Bor7+ooor4Bor7+ooor/2Q==" alt="The letter F"></body>
|
||||||
|
</html>
|
10
tests/ref/html/image-scaling-methods.html
Normal file
10
tests/ref/html/image-scaling-methods.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="display: flex; flex-direction: row; gap: 4pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="width: 28.346456692913385pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="image-rendering: smooth; width: 28.346456692913385pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="image-rendering: pixelated; width: 28.346456692913385pt"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -9,6 +9,9 @@
|
|||||||
#set page(height: 60pt)
|
#set page(height: 60pt)
|
||||||
#image("/assets/images/tiger.jpg")
|
#image("/assets/images/tiger.jpg")
|
||||||
|
|
||||||
|
--- image-jpg-html-base64 html ---
|
||||||
|
#image("/assets/images/f2t.jpg", alt: "The letter F")
|
||||||
|
|
||||||
--- image-sizing ---
|
--- image-sizing ---
|
||||||
// Test configuring the size and fitting behaviour of images.
|
// Test configuring the size and fitting behaviour of images.
|
||||||
|
|
||||||
@ -128,7 +131,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
width: 1cm,
|
width: 1cm,
|
||||||
)
|
)
|
||||||
|
|
||||||
--- image-scaling-methods ---
|
--- image-scaling-methods render html ---
|
||||||
#let img(scaling) = image(
|
#let img(scaling) = image(
|
||||||
bytes((
|
bytes((
|
||||||
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
@ -144,14 +147,26 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
scaling: scaling,
|
scaling: scaling,
|
||||||
)
|
)
|
||||||
|
|
||||||
#stack(
|
#let images = (
|
||||||
dir: ltr,
|
|
||||||
spacing: 4pt,
|
|
||||||
img(auto),
|
img(auto),
|
||||||
img("smooth"),
|
img("smooth"),
|
||||||
img("pixelated"),
|
img("pixelated"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#context if target() == "html" {
|
||||||
|
// TODO: Remove this once `stack` is supported in HTML export.
|
||||||
|
html.div(
|
||||||
|
style: "display: flex; flex-direction: row; gap: 4pt",
|
||||||
|
images.join(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stack(
|
||||||
|
dir: ltr,
|
||||||
|
spacing: 4pt,
|
||||||
|
..images,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
--- image-natural-dpi-sizing ---
|
--- image-natural-dpi-sizing ---
|
||||||
// Test that images aren't upscaled.
|
// Test that images aren't upscaled.
|
||||||
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
||||||
|
Loading…
x
Reference in New Issue
Block a user