mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Decode image (#1810)
This commit is contained in:
parent
49282626e9
commit
e3115336bf
@ -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:
|
||||
|
@ -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();
|
||||
// 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();
|
||||
|
||||
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"),
|
||||
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()),
|
||||
|
@ -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 {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 391 KiB |
@ -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%)
|
||||
|
Loading…
x
Reference in New Issue
Block a user