mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Add alt text to image function and PDF (#823)
This commit is contained in:
parent
4524539c2b
commit
2a682f0008
@ -32,7 +32,7 @@ pub struct ImageElem {
|
|||||||
let Spanned { v: path, span } =
|
let Spanned { v: path, span } =
|
||||||
args.expect::<Spanned<EcoString>>("path to image file")?;
|
args.expect::<Spanned<EcoString>>("path to image file")?;
|
||||||
let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into();
|
let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into();
|
||||||
let _ = load(vm.world(), &path, None).at(span)?;
|
let _ = load(vm.world(), &path, None, None).at(span)?;
|
||||||
path
|
path
|
||||||
)]
|
)]
|
||||||
pub path: EcoString,
|
pub path: EcoString,
|
||||||
@ -43,6 +43,9 @@ pub struct ImageElem {
|
|||||||
/// The height of the image.
|
/// The height of the image.
|
||||||
pub height: Smart<Rel<Length>>,
|
pub height: Smart<Rel<Length>>,
|
||||||
|
|
||||||
|
/// A text describing the image.
|
||||||
|
pub alt: Option<EcoString>,
|
||||||
|
|
||||||
/// How the image should adjust itself to a given area.
|
/// How the image should adjust itself to a given area.
|
||||||
#[default(ImageFit::Cover)]
|
#[default(ImageFit::Cover)]
|
||||||
pub fit: ImageFit,
|
pub fit: ImageFit,
|
||||||
@ -57,7 +60,8 @@ impl Layout for ImageElem {
|
|||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let first = families(styles).next();
|
let first = families(styles).next();
|
||||||
let fallback_family = first.as_ref().map(|f| f.as_str());
|
let fallback_family = first.as_ref().map(|f| f.as_str());
|
||||||
let image = load(vt.world, &self.path(), fallback_family).unwrap();
|
let image =
|
||||||
|
load(vt.world, &self.path(), fallback_family, self.alt(styles)).unwrap();
|
||||||
let sizing = Axes::new(self.width(styles), self.height(styles));
|
let sizing = Axes::new(self.width(styles), self.height(styles));
|
||||||
let region = sizing
|
let region = sizing
|
||||||
.zip(regions.base())
|
.zip(regions.base())
|
||||||
@ -163,6 +167,7 @@ fn load(
|
|||||||
world: Tracked<dyn World>,
|
world: Tracked<dyn World>,
|
||||||
full: &str,
|
full: &str,
|
||||||
fallback_family: Option<&str>,
|
fallback_family: Option<&str>,
|
||||||
|
alt: Option<EcoString>,
|
||||||
) -> StrResult<Image> {
|
) -> StrResult<Image> {
|
||||||
let full = Path::new(full);
|
let full = Path::new(full);
|
||||||
let buffer = world.file(full)?;
|
let buffer = world.file(full)?;
|
||||||
@ -174,5 +179,5 @@ fn load(
|
|||||||
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
|
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
|
||||||
_ => return Err("unknown image format".into()),
|
_ => return Err("unknown image format".into()),
|
||||||
};
|
};
|
||||||
Image::with_fonts(buffer, format, world, fallback_family)
|
Image::with_fonts(buffer, format, world, fallback_family, alt)
|
||||||
}
|
}
|
||||||
|
@ -483,7 +483,21 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size)
|
|||||||
let h = size.y.to_f32();
|
let h = size.y.to_f32();
|
||||||
ctx.content.save_state();
|
ctx.content.save_state();
|
||||||
ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]);
|
ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]);
|
||||||
ctx.content.x_object(Name(name.as_bytes()));
|
|
||||||
|
if let Some(alt) = image.alt() {
|
||||||
|
let mut image_span =
|
||||||
|
ctx.content.begin_marked_content_with_properties(Name(b"Span"));
|
||||||
|
let mut image_alt = image_span.properties_direct();
|
||||||
|
image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes()));
|
||||||
|
image_alt.finish();
|
||||||
|
image_span.finish();
|
||||||
|
|
||||||
|
ctx.content.x_object(Name(name.as_bytes()));
|
||||||
|
ctx.content.end_marked_content();
|
||||||
|
} else {
|
||||||
|
ctx.content.x_object(Name(name.as_bytes()));
|
||||||
|
}
|
||||||
|
|
||||||
ctx.content.restore_state();
|
ctx.content.restore_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +238,7 @@ fn render_bitmap_glyph(
|
|||||||
let size = text.size.to_f32();
|
let size = text.size.to_f32();
|
||||||
let ppem = size * ts.sy;
|
let ppem = size * ts.sy;
|
||||||
let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
|
let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
|
||||||
let image = Image::new(raster.data.into(), raster.format.into()).ok()?;
|
let image = Image::new(raster.data.into(), raster.format.into(), None).ok()?;
|
||||||
|
|
||||||
// FIXME: Vertical alignment isn't quite right for Apple Color Emoji,
|
// FIXME: Vertical alignment isn't quite right for Apple Color Emoji,
|
||||||
// and maybe also for Noto Color Emoji. And: Is the size calculation
|
// and maybe also for Noto Color Emoji. And: Is the size calculation
|
||||||
|
78
src/image.rs
78
src/image.rs
@ -17,25 +17,30 @@ use crate::World;
|
|||||||
///
|
///
|
||||||
/// Values of this type are cheap to clone and hash.
|
/// Values of this type are cheap to clone and hash.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Image(Arc<Repr>);
|
pub struct Image {
|
||||||
|
|
||||||
/// The internal representation.
|
|
||||||
struct Repr {
|
|
||||||
/// The raw, undecoded image data.
|
/// The raw, undecoded image data.
|
||||||
data: Buffer,
|
data: Buffer,
|
||||||
/// The format of the encoded `buffer`.
|
/// The format of the encoded `buffer`.
|
||||||
format: ImageFormat,
|
format: ImageFormat,
|
||||||
/// The decoded image.
|
/// The decoded image.
|
||||||
decoded: DecodedImage,
|
decoded: Arc<DecodedImage>,
|
||||||
|
/// A text describing the image.
|
||||||
|
alt: Option<EcoString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
/// Create an image from a buffer and a format.
|
/// Create an image from a buffer and a format.
|
||||||
pub fn new(data: Buffer, format: ImageFormat) -> StrResult<Self> {
|
pub fn new(
|
||||||
match format {
|
data: Buffer,
|
||||||
ImageFormat::Raster(format) => decode_raster(data, format),
|
format: ImageFormat,
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => decode_svg(data),
|
alt: Option<EcoString>,
|
||||||
}
|
) -> StrResult<Self> {
|
||||||
|
let decoded = match format {
|
||||||
|
ImageFormat::Raster(format) => decode_raster(&data, format)?,
|
||||||
|
ImageFormat::Vector(VectorFormat::Svg) => decode_svg(&data)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { data, format, decoded, alt })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a font-dependant image from a buffer and a format.
|
/// Create a font-dependant image from a buffer and a format.
|
||||||
@ -44,28 +49,31 @@ impl Image {
|
|||||||
format: ImageFormat,
|
format: ImageFormat,
|
||||||
world: Tracked<dyn World>,
|
world: Tracked<dyn World>,
|
||||||
fallback_family: Option<&str>,
|
fallback_family: Option<&str>,
|
||||||
|
alt: Option<EcoString>,
|
||||||
) -> StrResult<Self> {
|
) -> StrResult<Self> {
|
||||||
match format {
|
let decoded = match format {
|
||||||
ImageFormat::Raster(format) => decode_raster(data, format),
|
ImageFormat::Raster(format) => decode_raster(&data, format)?,
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => {
|
ImageFormat::Vector(VectorFormat::Svg) => {
|
||||||
decode_svg_with_fonts(data, world, fallback_family)
|
decode_svg_with_fonts(&data, world, fallback_family)?
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(Self { data, format, decoded, alt })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The raw image data.
|
/// The raw image data.
|
||||||
pub fn data(&self) -> &Buffer {
|
pub fn data(&self) -> &Buffer {
|
||||||
&self.0.data
|
&self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format of the image.
|
/// The format of the image.
|
||||||
pub fn format(&self) -> ImageFormat {
|
pub fn format(&self) -> ImageFormat {
|
||||||
self.0.format
|
self.format
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The decoded version of the image.
|
/// The decoded version of the image.
|
||||||
pub fn decoded(&self) -> &DecodedImage {
|
pub fn decoded(&self) -> &DecodedImage {
|
||||||
&self.0.decoded
|
&self.decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The width of the image in pixels.
|
/// The width of the image in pixels.
|
||||||
@ -77,6 +85,11 @@ impl Image {
|
|||||||
pub fn height(&self) -> u32 {
|
pub fn height(&self) -> u32 {
|
||||||
self.decoded().height()
|
self.decoded().height()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A text describing the image.
|
||||||
|
pub fn alt(&self) -> Option<&str> {
|
||||||
|
self.alt.as_deref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Image {
|
impl Debug for Image {
|
||||||
@ -85,6 +98,7 @@ impl Debug for Image {
|
|||||||
.field("format", &self.format())
|
.field("format", &self.format())
|
||||||
.field("width", &self.width())
|
.field("width", &self.width())
|
||||||
.field("height", &self.height())
|
.field("height", &self.height())
|
||||||
|
.field("alt", &self.alt())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,38 +197,30 @@ impl DecodedImage {
|
|||||||
|
|
||||||
/// Decode a raster image.
|
/// Decode a raster image.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn decode_raster(data: Buffer, format: RasterFormat) -> StrResult<Image> {
|
fn decode_raster(data: &Buffer, format: RasterFormat) -> StrResult<Arc<DecodedImage>> {
|
||||||
let cursor = io::Cursor::new(&data);
|
let cursor = io::Cursor::new(&data);
|
||||||
let reader = image::io::Reader::with_format(cursor, format.into());
|
let reader = image::io::Reader::with_format(cursor, format.into());
|
||||||
let dynamic = reader.decode().map_err(format_image_error)?;
|
let dynamic = reader.decode().map_err(format_image_error)?;
|
||||||
Ok(Image(Arc::new(Repr {
|
Ok(Arc::new(DecodedImage::Raster(dynamic, format)))
|
||||||
data,
|
|
||||||
format: ImageFormat::Raster(format),
|
|
||||||
decoded: DecodedImage::Raster(dynamic, format),
|
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode an SVG image.
|
/// Decode an SVG image.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn decode_svg(data: Buffer) -> StrResult<Image> {
|
fn decode_svg(data: &Buffer) -> StrResult<Arc<DecodedImage>> {
|
||||||
let opts = usvg::Options::default();
|
let opts = usvg::Options::default();
|
||||||
let tree = usvg::Tree::from_data(&data, &opts.to_ref()).map_err(format_usvg_error)?;
|
let tree = usvg::Tree::from_data(data, &opts.to_ref()).map_err(format_usvg_error)?;
|
||||||
Ok(Image(Arc::new(Repr {
|
Ok(Arc::new(DecodedImage::Svg(tree)))
|
||||||
data,
|
|
||||||
format: ImageFormat::Vector(VectorFormat::Svg),
|
|
||||||
decoded: DecodedImage::Svg(tree),
|
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode an SVG image with access to fonts.
|
/// Decode an SVG image with access to fonts.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn decode_svg_with_fonts(
|
fn decode_svg_with_fonts(
|
||||||
data: Buffer,
|
data: &Buffer,
|
||||||
world: Tracked<dyn World>,
|
world: Tracked<dyn World>,
|
||||||
fallback_family: Option<&str>,
|
fallback_family: Option<&str>,
|
||||||
) -> StrResult<Image> {
|
) -> StrResult<Arc<DecodedImage>> {
|
||||||
// Parse XML.
|
// Parse XML.
|
||||||
let xml = std::str::from_utf8(&data)
|
let xml = std::str::from_utf8(data)
|
||||||
.map_err(|_| format_usvg_error(usvg::Error::NotAnUtf8Str))?;
|
.map_err(|_| format_usvg_error(usvg::Error::NotAnUtf8Str))?;
|
||||||
let document = roxmltree::Document::parse(xml)
|
let document = roxmltree::Document::parse(xml)
|
||||||
.map_err(|err| format_xml_like_error("svg", err))?;
|
.map_err(|err| format_xml_like_error("svg", err))?;
|
||||||
@ -239,11 +245,7 @@ fn decode_svg_with_fonts(
|
|||||||
let tree =
|
let tree =
|
||||||
usvg::Tree::from_xmltree(&document, &opts.to_ref()).map_err(format_usvg_error)?;
|
usvg::Tree::from_xmltree(&document, &opts.to_ref()).map_err(format_usvg_error)?;
|
||||||
|
|
||||||
Ok(Image(Arc::new(Repr {
|
Ok(Arc::new(DecodedImage::Svg(tree)))
|
||||||
data,
|
|
||||||
format: ImageFormat::Vector(VectorFormat::Svg),
|
|
||||||
decoded: DecodedImage::Svg(tree),
|
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discover and load the fonts referenced by an SVG.
|
/// Discover and load the fonts referenced by an SVG.
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
#image("/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
|
#image("/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
|
||||||
|
|
||||||
// Make sure the bounding-box of the image is correct.
|
// Make sure the bounding-box of the image is correct.
|
||||||
#align(bottom + right, image("/tiger.jpg", width: 40pt))
|
#align(bottom + right, image("/tiger.jpg", width: 40pt, alt: "A tiger"))
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test all three fit modes.
|
// Test all three fit modes.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user