Add alt text to image function and PDF (#823)

This commit is contained in:
Martin Haug 2023-04-20 11:23:03 +02:00 committed by GitHub
parent 4524539c2b
commit 2a682f0008
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 65 additions and 44 deletions

View File

@ -32,7 +32,7 @@ pub struct ImageElem {
let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?;
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
)]
pub path: EcoString,
@ -43,6 +43,9 @@ pub struct ImageElem {
/// The height of the image.
pub height: Smart<Rel<Length>>,
/// A text describing the image.
pub alt: Option<EcoString>,
/// How the image should adjust itself to a given area.
#[default(ImageFit::Cover)]
pub fit: ImageFit,
@ -57,7 +60,8 @@ impl Layout for ImageElem {
) -> SourceResult<Fragment> {
let first = families(styles).next();
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 region = sizing
.zip(regions.base())
@ -163,6 +167,7 @@ fn load(
world: Tracked<dyn World>,
full: &str,
fallback_family: Option<&str>,
alt: Option<EcoString>,
) -> StrResult<Image> {
let full = Path::new(full);
let buffer = world.file(full)?;
@ -174,5 +179,5 @@ fn load(
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => return Err("unknown image format".into()),
};
Image::with_fonts(buffer, format, world, fallback_family)
Image::with_fonts(buffer, format, world, fallback_family, alt)
}

View File

@ -483,7 +483,21 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size)
let h = size.y.to_f32();
ctx.content.save_state();
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();
}

View File

@ -238,7 +238,7 @@ fn render_bitmap_glyph(
let size = text.size.to_f32();
let ppem = size * ts.sy;
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,
// and maybe also for Noto Color Emoji. And: Is the size calculation

View File

@ -17,25 +17,30 @@ use crate::World;
///
/// Values of this type are cheap to clone and hash.
#[derive(Clone)]
pub struct Image(Arc<Repr>);
/// The internal representation.
struct Repr {
pub struct Image {
/// The raw, undecoded image data.
data: Buffer,
/// The format of the encoded `buffer`.
format: ImageFormat,
/// The decoded image.
decoded: DecodedImage,
decoded: Arc<DecodedImage>,
/// A text describing the image.
alt: Option<EcoString>,
}
impl Image {
/// Create an image from a buffer and a format.
pub fn new(data: Buffer, format: ImageFormat) -> StrResult<Self> {
match format {
ImageFormat::Raster(format) => decode_raster(data, format),
ImageFormat::Vector(VectorFormat::Svg) => decode_svg(data),
}
pub fn new(
data: Buffer,
format: ImageFormat,
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.
@ -44,28 +49,31 @@ impl Image {
format: ImageFormat,
world: Tracked<dyn World>,
fallback_family: Option<&str>,
alt: Option<EcoString>,
) -> StrResult<Self> {
match format {
ImageFormat::Raster(format) => decode_raster(data, format),
let decoded = match format {
ImageFormat::Raster(format) => decode_raster(&data, format)?,
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.
pub fn data(&self) -> &Buffer {
&self.0.data
&self.data
}
/// The format of the image.
pub fn format(&self) -> ImageFormat {
self.0.format
self.format
}
/// The decoded version of the image.
pub fn decoded(&self) -> &DecodedImage {
&self.0.decoded
&self.decoded
}
/// The width of the image in pixels.
@ -77,6 +85,11 @@ impl Image {
pub fn height(&self) -> u32 {
self.decoded().height()
}
/// A text describing the image.
pub fn alt(&self) -> Option<&str> {
self.alt.as_deref()
}
}
impl Debug for Image {
@ -85,6 +98,7 @@ impl Debug for Image {
.field("format", &self.format())
.field("width", &self.width())
.field("height", &self.height())
.field("alt", &self.alt())
.finish()
}
}
@ -183,38 +197,30 @@ impl DecodedImage {
/// Decode a raster image.
#[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 reader = image::io::Reader::with_format(cursor, format.into());
let dynamic = reader.decode().map_err(format_image_error)?;
Ok(Image(Arc::new(Repr {
data,
format: ImageFormat::Raster(format),
decoded: DecodedImage::Raster(dynamic, format),
})))
Ok(Arc::new(DecodedImage::Raster(dynamic, format)))
}
/// Decode an SVG image.
#[comemo::memoize]
fn decode_svg(data: Buffer) -> StrResult<Image> {
fn decode_svg(data: &Buffer) -> StrResult<Arc<DecodedImage>> {
let opts = usvg::Options::default();
let tree = usvg::Tree::from_data(&data, &opts.to_ref()).map_err(format_usvg_error)?;
Ok(Image(Arc::new(Repr {
data,
format: ImageFormat::Vector(VectorFormat::Svg),
decoded: DecodedImage::Svg(tree),
})))
let tree = usvg::Tree::from_data(data, &opts.to_ref()).map_err(format_usvg_error)?;
Ok(Arc::new(DecodedImage::Svg(tree)))
}
/// Decode an SVG image with access to fonts.
#[comemo::memoize]
fn decode_svg_with_fonts(
data: Buffer,
data: &Buffer,
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> StrResult<Image> {
) -> StrResult<Arc<DecodedImage>> {
// Parse XML.
let xml = std::str::from_utf8(&data)
let xml = std::str::from_utf8(data)
.map_err(|_| format_usvg_error(usvg::Error::NotAnUtf8Str))?;
let document = roxmltree::Document::parse(xml)
.map_err(|err| format_xml_like_error("svg", err))?;
@ -239,11 +245,7 @@ fn decode_svg_with_fonts(
let tree =
usvg::Tree::from_xmltree(&document, &opts.to_ref()).map_err(format_usvg_error)?;
Ok(Image(Arc::new(Repr {
data,
format: ImageFormat::Vector(VectorFormat::Svg),
decoded: DecodedImage::Svg(tree),
})))
Ok(Arc::new(DecodedImage::Svg(tree)))
}
/// Discover and load the fonts referenced by an SVG.

View File

@ -21,7 +21,7 @@
#image("/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
// 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.