Decode image (#1810)

This commit is contained in:
Beiri22 2023-08-05 13:58:28 +02:00 committed by GitHub
parent 49282626e9
commit e3115336bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 157 additions and 20 deletions

View File

@ -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:

View File

@ -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()),

View File

@ -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

View File

@ -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%)