summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-library/src/compute/data.rs11
-rw-r--r--crates/typst-library/src/visualize/image.rs109
-rw-r--r--crates/typst/src/image.rs39
-rw-r--r--tests/ref/visualize/image.pngbin172120 -> 400573 bytes
-rw-r--r--tests/typ/visualize/image.typ20
5 files changed, 158 insertions, 21 deletions
diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs
index 4a7c53cc..48e35846 100644
--- a/crates/typst-library/src/compute/data.rs
+++ b/crates/typst-library/src/compute/data.rs
@@ -57,7 +57,7 @@ pub enum Encoding {
Utf8,
}
-/// A value that can be read from a value.
+/// A value that can be read from a file.
pub enum Readable {
/// A decoded string.
Str(Str),
@@ -75,6 +75,15 @@ cast! {
v: Bytes => Self::Bytes(v),
}
+impl From<Readable> for Bytes {
+ fn from(value: Readable) -> Self {
+ match value {
+ Readable::Bytes(v) => v,
+ Readable::Str(v) => v.as_bytes().into(),
+ }
+ }
+}
+
/// Reads structured data from a CSV file.
///
/// The CSV file will be read and parsed into a 2-dimensional array of strings:
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
index 514861e5..7c42ef3c 100644
--- a/crates/typst-library/src/visualize/image.rs
+++ b/crates/typst-library/src/visualize/image.rs
@@ -1,9 +1,10 @@
use std::ffi::OsStr;
use std::path::Path;
-use typst::eval::Bytes;
+use typst::geom::Smart;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
+use crate::compute::Readable;
use crate::meta::{Figurable, LocalName};
use crate::prelude::*;
use crate::text::families;
@@ -32,6 +33,10 @@ use crate::text::families;
/// Display: Image
/// Category: visualize
#[element(Layout, LocalName, Figurable)]
+#[scope(
+ scope.define("decode", image_decode_func());
+ scope
+)]
pub struct ImageElem {
/// Path to an image file.
#[required]
@@ -47,8 +52,11 @@ pub struct ImageElem {
/// The raw file data.
#[internal]
#[required]
- #[parse(data)]
- pub data: Bytes,
+ #[parse(Readable::Bytes(data))]
+ pub data: Readable,
+
+ /// The image's format. Detected automatically by default.
+ pub format: Smart<ImageFormat>,
/// The width of the image.
pub width: Smart<Rel<Length>>,
@@ -64,6 +72,61 @@ pub struct ImageElem {
pub fit: ImageFit,
}
+/// Decode a raster of vector graphic from bytes or a string.
+///
+/// ## Example { #example }
+/// ```example
+/// #let original = read("diagram.svg")
+/// #let changed = original.replace(
+/// "#2B80FF", // blue
+/// green.hex(),
+/// )
+///
+/// #image.decode(original)
+/// #image.decode(changed)
+/// ```
+///
+/// Display: Decode Image
+/// Category: visualize
+#[func]
+pub fn image_decode(
+ /// The data to decode as an image. Can be a string for SVGs.
+ data: Readable,
+ /// The image's format. Detected automatically by default.
+ #[named]
+ format: Option<Smart<ImageFormat>>,
+ /// The width of the image.
+ #[named]
+ width: Option<Smart<Rel<Length>>>,
+ /// The height of the image.
+ #[named]
+ height: Option<Smart<Rel<Length>>>,
+ /// A text describing the image.
+ #[named]
+ alt: Option<Option<EcoString>>,
+ /// How the image should adjust itself to a given area.
+ #[named]
+ fit: Option<ImageFit>,
+) -> StrResult<Content> {
+ let mut elem = ImageElem::new(EcoString::new(), data);
+ if let Some(format) = format {
+ elem.push_format(format);
+ }
+ if let Some(width) = width {
+ elem.push_width(width);
+ }
+ if let Some(height) = height {
+ elem.push_height(height);
+ }
+ if let Some(alt) = alt {
+ elem.push_alt(alt);
+ }
+ if let Some(fit) = fit {
+ elem.push_fit(fit);
+ }
+ Ok(elem.pack())
+}
+
impl Layout for ImageElem {
#[tracing::instrument(name = "ImageElem::layout", skip_all)]
fn layout(
@@ -72,22 +135,36 @@ impl Layout for ImageElem {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
- let ext = Path::new(self.path().as_str())
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default()
- .to_lowercase();
-
- let format = match ext.as_str() {
- "png" => ImageFormat::Raster(RasterFormat::Png),
- "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
- "gif" => ImageFormat::Raster(RasterFormat::Gif),
- "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
- _ => bail!(self.span(), "unknown image format"),
+ // Take the format that was explicitly defined, or parse the extention,
+ // or try to detect the format.
+ let data = self.data();
+ let format = match self.format(styles) {
+ Smart::Custom(v) => v,
+ Smart::Auto => {
+ let ext = Path::new(self.path().as_str())
+ .extension()
+ .and_then(OsStr::to_str)
+ .unwrap_or_default()
+ .to_lowercase();
+
+ match ext.as_str() {
+ "png" => ImageFormat::Raster(RasterFormat::Png),
+ "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
+ "gif" => ImageFormat::Raster(RasterFormat::Gif),
+ "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
+ _ => match &data {
+ Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
+ Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
+ Some(f) => ImageFormat::Raster(f),
+ None => bail!(self.span(), "unknown image format"),
+ },
+ },
+ }
+ }
};
let image = Image::with_fonts(
- self.data(),
+ data.into(),
format,
vt.world,
families(styles).next().as_ref().map(|f| f.as_str()),
diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs
index 1b62a5ac..05672e2a 100644
--- a/crates/typst/src/image.rs
+++ b/crates/typst/src/image.rs
@@ -12,10 +12,11 @@ use image::codecs::gif::GifDecoder;
use image::codecs::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder;
use image::io::Limits;
-use image::{ImageDecoder, ImageResult};
+use image::{guess_format, ImageDecoder, ImageResult};
+use typst_macros::{cast, Cast};
use usvg::{TreeParsing, TreeTextToPath};
-use crate::diag::{format_xml_like_error, StrResult};
+use crate::diag::{bail, format_xml_like_error, StrResult};
use crate::eval::Bytes;
use crate::font::Font;
use crate::geom::Axes;
@@ -156,8 +157,18 @@ pub enum ImageFormat {
Vector(VectorFormat),
}
+cast! {
+ ImageFormat,
+ self => match self {
+ Self::Raster(v) => v.into_value(),
+ Self::Vector(v) => v.into_value()
+ },
+ v: RasterFormat => Self::Raster(v),
+ v: VectorFormat => Self::Vector(v),
+}
+
/// A raster graphics format.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum RasterFormat {
/// Raster format for illustrations and transparent graphics.
Png,
@@ -168,12 +179,19 @@ pub enum RasterFormat {
}
/// A vector graphics format.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VectorFormat {
/// The vector graphics format of the web.
Svg,
}
+impl RasterFormat {
+ /// Try to detect the format of data in a buffer.
+ pub fn detect(data: &[u8]) -> Option<Self> {
+ guess_format(data).ok().and_then(|format| format.try_into().ok())
+ }
+}
+
impl From<RasterFormat> for image::ImageFormat {
fn from(format: RasterFormat) -> Self {
match format {
@@ -184,6 +202,19 @@ impl From<RasterFormat> for image::ImageFormat {
}
}
+impl TryFrom<image::ImageFormat> for RasterFormat {
+ type Error = EcoString;
+
+ fn try_from(format: image::ImageFormat) -> StrResult<Self> {
+ Ok(match format {
+ image::ImageFormat::Png => RasterFormat::Png,
+ image::ImageFormat::Jpeg => RasterFormat::Jpg,
+ image::ImageFormat::Gif => RasterFormat::Gif,
+ _ => bail!("Format not yet supported."),
+ })
+ }
+}
+
impl From<ttf_parser::RasterImageFormat> for RasterFormat {
fn from(format: ttf_parser::RasterImageFormat) -> Self {
match format {
diff --git a/tests/ref/visualize/image.png b/tests/ref/visualize/image.png
index 90aa9b48..ec53fa98 100644
--- a/tests/ref/visualize/image.png
+++ b/tests/ref/visualize/image.png
Binary files differ
diff --git a/tests/typ/visualize/image.typ b/tests/typ/visualize/image.typ
index e3bcc64f..60ce4f68 100644
--- a/tests/typ/visualize/image.typ
+++ b/tests/typ/visualize/image.typ
@@ -60,3 +60,23 @@ A #box(image("/files/tiger.jpg", height: 1cm, width: 80%)) B
---
// Error: 2-25 failed to parse svg: found closing tag 'g' instead of 'style' in line 4
#image("/files/bad.svg")
+
+---
+// Test parsing from svg data
+#image.decode(`<svg xmlns="http://www.w3.org/2000/svg" height="140" width="500"><ellipse cx="200" cy="80" rx="100" ry="50" style="fill:yellow;stroke:purple;stroke-width:2" /></svg>`.text, format: "svg")
+
+---
+// Error: 2-168 failed to parse svg: missing root node
+#image.decode(`<svg height="140" width="500"><ellipse cx="200" cy="80" rx="100" ry="50" style="fill:yellow;stroke:purple;stroke-width:2" /></svg>`.text, format: "svg")
+
+---
+// Test format auto detect
+#image.decode(read("/files/tiger.jpg", encoding: none), width: 80%)
+
+---
+// Test format manual
+#image.decode(read("/files/tiger.jpg", encoding: none), format: "jpg", width: 80%)
+
+---
+// Error: 2-83 failed to decode image
+#image.decode(read("/files/tiger.jpg", encoding: none), format: "png", width: 80%)