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 } =
|
||||
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)
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
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.
|
||||
#[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.
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user