feat: [WIP] text decoration attributes

This commit is contained in:
Tobias Schmitz 2025-08-01 16:02:58 +02:00
parent df6a4ba7f4
commit b2d9bd2fb4
No known key found for this signature in database
7 changed files with 353 additions and 82 deletions

View File

@ -5,11 +5,10 @@ use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::pdf::PdfDocument; use krilla::pdf::PdfDocument;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::SpanTag;
use krilla_svg::{SurfaceExt, SvgSettings}; use krilla_svg::{SurfaceExt, SvgSettings};
use typst_library::diag::{SourceResult, bail}; use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Angle, Point, Ratio, Rect, Size, Transform}; use typst_library::layout::{Abs, Angle, Ratio, Size, Transform};
use typst_library::visualize::{ use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage, ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage,
}; };
@ -35,11 +34,9 @@ pub(crate) fn handle_image(
gc.image_spans.insert(span); gc.image_spans.insert(span);
tags::update_bbox(gc, fc, || Rect::from_pos_size(Point::zero(), size)); let mut handle = tags::image(gc, fc, surface, image, size);
let mut handle =
tags::start_span(gc, surface, SpanTag::empty().with_alt_text(image.alt()));
let surface = handle.surface(); let surface = handle.surface();
match image.kind() { match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
let (exif_transform, new_size) = exif_transform(raster, size); let (exif_transform, new_size) = exif_transform(raster, size);

View File

@ -18,6 +18,7 @@ pub use self::metadata::{Timestamp, Timezone};
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use ecow::eco_format; use ecow::eco_format;
use krilla::configure::Validator;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst_library::diag::{SourceResult, StrResult, bail}; use typst_library::diag::{SourceResult, StrResult, bail};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
@ -67,6 +68,14 @@ pub struct PdfOptions<'a> {
pub disable_tags: bool, pub disable_tags: bool,
} }
impl PdfOptions<'_> {
/// Whether the current export mode is PDF/UA-1, and in the future maybe
/// PDF/UA-2.
pub fn is_pdf_ua(&self) -> bool {
self.standards.config.validator() == Validator::UA1
}
}
/// Encapsulates a list of compatible PDF standards. /// Encapsulates a list of compatible PDF standards.
#[derive(Clone)] #[derive(Clone)]
pub struct PdfStandards { pub struct PdfStandards {

View File

@ -1,7 +1,6 @@
use krilla::geom::{Path, PathBuilder, Rect}; use krilla::geom::{Path, PathBuilder, Rect};
use krilla::surface::Surface; use krilla::surface::Surface;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::pdf::ArtifactKind;
use typst_library::visualize::{Geometry, Shape}; use typst_library::visualize::{Geometry, Shape};
use typst_syntax::Span; use typst_syntax::Span;
@ -17,9 +16,7 @@ pub(crate) fn handle_shape(
gc: &mut GlobalContext, gc: &mut GlobalContext,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
tags::update_bbox(gc, fc, || shape.geometry.bbox()); let mut handle = tags::shape(gc, fc, surface, shape);
let mut handle = tags::start_artifact(gc, surface, ArtifactKind::Other);
let surface = handle.surface(); let surface = handle.surface();
surface.set_location(span.into_raw()); surface.set_location(span.into_raw());

View File

@ -3,25 +3,32 @@ use std::collections::HashMap;
use std::slice::SliceIndex; use std::slice::SliceIndex;
use krilla::geom as kg; use krilla::geom as kg;
use krilla::tagging::{BBox, Node, TagKind, TagTree}; use krilla::tagging::{
BBox, Identifier, LineHeight, NaiveRgbColor, Node, Tag, TagKind, TagTree,
TextDecorationType,
};
use typst_library::diag::{StrResult, bail};
use typst_library::foundations::{LinkMarker, Packed}; use typst_library::foundations::{LinkMarker, Packed};
use typst_library::introspection::Location; use typst_library::introspection::Location;
use typst_library::layout::{Abs, Point, Rect}; use typst_library::layout::{Abs, Length, Point, Rect};
use typst_library::model::{OutlineEntry, TableCell}; use typst_library::model::{OutlineEntry, TableCell};
use typst_library::pdf::ArtifactKind; use typst_library::pdf::ArtifactKind;
use typst_library::text::Lang; use typst_library::text::Lang;
use typst_syntax::Span; use typst_syntax::Span;
use crate::PdfOptions;
use crate::convert::FrameContext; use crate::convert::FrameContext;
use crate::tags::list::ListCtx; use crate::tags::list::ListCtx;
use crate::tags::outline::OutlineCtx; use crate::tags::outline::OutlineCtx;
use crate::tags::table::TableCtx; use crate::tags::table::TableCtx;
use crate::tags::{Placeholder, TagGroup, TagNode}; use crate::tags::{Placeholder, TagNode};
use crate::util::AbsExt; use crate::util::AbsExt;
pub struct Tags { pub struct Tags {
/// The language of the first text item that has been encountered. /// The language of the first text item that has been encountered.
pub doc_lang: Option<Lang>, pub doc_lang: Option<Lang>,
/// The current set of text attributes.
pub text_attrs: TextAttrs,
/// The intermediary stack of nested tag groups. /// The intermediary stack of nested tag groups.
pub stack: TagStack, pub stack: TagStack,
/// A list of placeholders corresponding to a [`TagNode::Placeholder`]. /// A list of placeholders corresponding to a [`TagNode::Placeholder`].
@ -39,13 +46,14 @@ pub struct Tags {
table_id: TableId, table_id: TableId,
/// The output. /// The output.
pub tree: Vec<TagNode>, tree: Vec<TagNode>,
} }
impl Tags { impl Tags {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
doc_lang: None, doc_lang: None,
text_attrs: TextAttrs::new(),
stack: TagStack::new(), stack: TagStack::new(),
placeholders: Placeholders(Vec::new()), placeholders: Placeholders(Vec::new()),
footnotes: HashMap::new(), footnotes: HashMap::new(),
@ -66,6 +74,23 @@ impl Tags {
} }
} }
pub fn push_text(&mut self, new_attrs: ResolvedTextAttrs, id: Identifier) {
// FIXME: Artifacts will force a split in the spans, and decoartions
// generate artifacts
let last_node = if let Some(entry) = self.stack.last_mut() {
entry.nodes.last_mut()
} else {
self.tree.last_mut()
};
if let Some(TagNode::Text(prev_attrs, nodes)) = last_node
&& *prev_attrs == new_attrs
{
nodes.push(id);
} else {
self.push(TagNode::Text(new_attrs, vec![id]));
}
}
pub fn extend(&mut self, nodes: impl IntoIterator<Item = TagNode>) { pub fn extend(&mut self, nodes: impl IntoIterator<Item = TagNode>) {
if let Some(entry) = self.stack.last_mut() { if let Some(entry) = self.stack.last_mut() {
entry.nodes.extend(nodes); entry.nodes.extend(nodes);
@ -77,11 +102,11 @@ impl Tags {
pub fn build_tree(&mut self) -> TagTree { pub fn build_tree(&mut self) -> TagTree {
assert!(self.stack.items.is_empty(), "tags weren't properly closed"); assert!(self.stack.items.is_empty(), "tags weren't properly closed");
let children = std::mem::take(&mut self.tree) let mut nodes = Vec::new();
.into_iter() for child in std::mem::take(&mut self.tree) {
.map(|node| self.resolve_node(node)) self.resolve_node(&mut nodes, child);
.collect::<Vec<_>>(); }
TagTree::from(children) TagTree::from(nodes)
} }
/// Try to set the language of a parent tag, or the entire document. /// Try to set the language of a parent tag, or the entire document.
@ -89,10 +114,6 @@ impl Tags {
/// this will return `Some`, and the language should be specified on the /// this will return `Some`, and the language should be specified on the
/// marked content directly. /// marked content directly.
pub fn try_set_lang(&mut self, lang: Lang) -> Option<Lang> { pub fn try_set_lang(&mut self, lang: Lang) -> Option<Lang> {
// Discard languages within artifacts.
if self.in_artifact.is_some() {
return None;
}
if self.doc_lang.is_none_or(|l| l == lang) { if self.doc_lang.is_none_or(|l| l == lang) {
self.doc_lang = Some(lang); self.doc_lang = Some(lang);
return None; return None;
@ -106,23 +127,46 @@ impl Tags {
Some(lang) Some(lang)
} }
/// Resolves [`Placeholder`] nodes. /// Resolves nodes into an accumulator.
fn resolve_node(&mut self, node: TagNode) -> Node { fn resolve_node(&mut self, accum: &mut Vec<Node>, node: TagNode) {
match node { match node {
TagNode::Group(TagGroup { tag, nodes }) => { TagNode::Group(group) => {
let children = nodes let mut nodes = Vec::new();
.into_iter() for child in group.nodes {
.map(|node| self.resolve_node(node)) self.resolve_node(&mut nodes, child);
.collect::<Vec<_>>(); }
Node::Group(krilla::tagging::TagGroup::with_children(tag, children)) let group = krilla::tagging::TagGroup::with_children(group.tag, nodes);
accum.push(Node::Group(group));
}
TagNode::Leaf(identifier) => {
accum.push(Node::Leaf(identifier));
}
TagNode::Placeholder(placeholder) => {
accum.push(self.placeholders.take(placeholder));
} }
TagNode::Leaf(identifier) => Node::Leaf(identifier),
TagNode::Placeholder(placeholder) => self.placeholders.take(placeholder),
TagNode::FootnoteEntry(loc) => { TagNode::FootnoteEntry(loc) => {
let node = (self.footnotes.remove(&loc)) let node = (self.footnotes.remove(&loc))
.and_then(|ctx| ctx.entry) .and_then(|ctx| ctx.entry)
.expect("footnote"); .expect("footnote");
self.resolve_node(node) self.resolve_node(accum, node)
}
TagNode::Text(attrs, ids) => {
let children = ids.into_iter().map(|id| Node::Leaf(id));
if attrs.is_empty() {
accum.extend(children);
} else {
let tag = Tag::Span
.with_line_height(attrs.lineheight)
.with_baseline_shift(attrs.baseline_shift)
.with_text_decoration_type(attrs.deco.map(|d| d.kind.to_krilla()))
.with_text_decoration_color(attrs.deco.and_then(|d| d.color))
.with_text_decoration_thickness(
attrs.deco.and_then(|d| d.thickness),
);
let group =
krilla::tagging::TagGroup::with_children(tag, children.collect());
accum.push(Node::Group(group));
}
} }
} }
} }
@ -143,6 +187,142 @@ impl Tags {
} }
} }
#[derive(Clone, Debug)]
pub struct TextAttrs {
lineheight: Option<LineHeight>,
baseline_shift: Option<f32>,
/// PDF can only represent one of the following attributes at a time.
/// Keep track of all of them, and depending if PDF/UA-1 is enforced, either
/// throw an error, or just use one of them.
decos: Vec<(Location, TextDeco)>,
}
impl TextAttrs {
pub fn new() -> Self {
Self {
lineheight: None,
baseline_shift: None,
decos: Vec::new(),
}
}
pub fn push_underline(
&mut self,
options: &PdfOptions,
loc: Location,
stroke: TextDecoStroke,
) -> StrResult<()> {
self.push_deco(options, loc, TextDeco { kind: TextDecoKind::Underline, stroke })
}
pub fn push_overline(
&mut self,
options: &PdfOptions,
loc: Location,
stroke: TextDecoStroke,
) -> StrResult<()> {
self.push_deco(options, loc, TextDeco { kind: TextDecoKind::Overline, stroke })
}
pub fn push_strike(
&mut self,
options: &PdfOptions,
loc: Location,
stroke: TextDecoStroke,
) -> StrResult<()> {
self.push_deco(options, loc, TextDeco { kind: TextDecoKind::Strike, stroke })
}
pub fn push_deco(
&mut self,
options: &PdfOptions,
loc: Location,
deco: TextDeco,
) -> StrResult<()> {
// TODO: can overlapping tags break this?
if self.decos.iter().any(|(_, d)| d.kind != deco.kind) {
let validator = options.standards.config.validator();
let validator = validator.as_str();
bail!("{validator} error: cannot combine underline, overline, and or strike");
}
self.decos.push((loc, deco));
Ok(())
}
/// Returns true if a decoration was removed.
pub fn pop_deco(&mut self, loc: Location) -> bool {
// TODO: Ideally we would just check the top of the stack, can
// overlapping tags even happen for decorations?
if let Some(i) = self.decos.iter().rposition(|(l, _)| *l != loc) {
self.decos.remove(i);
return true;
}
false
}
pub fn resolve(&self, em: Abs) -> ResolvedTextAttrs {
let deco = self.decos.last().map(|&(_, TextDeco { kind, stroke })| {
let thickness = stroke.thickness.map(|t| t.at(em).to_f32());
ResolvedTextDeco { kind, color: stroke.color, thickness }
});
ResolvedTextAttrs {
lineheight: self.lineheight,
baseline_shift: self.baseline_shift,
deco,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TextDeco {
kind: TextDecoKind,
stroke: TextDecoStroke,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextDecoKind {
Underline,
Overline,
Strike,
}
impl TextDecoKind {
fn to_krilla(self) -> TextDecorationType {
match self {
TextDecoKind::Underline => TextDecorationType::Underline,
TextDecoKind::Overline => TextDecorationType::Overline,
TextDecoKind::Strike => TextDecorationType::LineThrough,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct TextDecoStroke {
pub color: Option<NaiveRgbColor>,
pub thickness: Option<Length>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ResolvedTextAttrs {
lineheight: Option<LineHeight>,
baseline_shift: Option<f32>,
deco: Option<ResolvedTextDeco>,
}
impl ResolvedTextAttrs {
pub fn is_empty(&self) -> bool {
self.lineheight.is_none() && self.baseline_shift.is_none() && self.deco.is_none()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ResolvedTextDeco {
kind: TextDecoKind,
color: Option<NaiveRgbColor>,
thickness: Option<f32>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct TagStack { pub struct TagStack {
items: Vec<StackEntry>, items: Vec<StackEntry>,

View File

@ -5,12 +5,13 @@ use krilla::configure::Validator;
use krilla::page::Page; use krilla::page::Page;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::{ use krilla::tagging::{
ArtifactType, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag, TagKind, ArtifactType, ContentTag, Identifier, ListNumbering, NaiveRgbColor, Node, SpanTag,
Tag, TagKind,
}; };
use typst_library::diag::{SourceResult, bail}; use typst_library::diag::{At, SourceResult, bail};
use typst_library::foundations::{Content, LinkMarker}; use typst_library::foundations::{Content, LinkMarker, Smart};
use typst_library::introspection::Location; use typst_library::introspection::Location;
use typst_library::layout::{Rect, RepeatElem}; use typst_library::layout::{Point, Rect, RepeatElem, Size};
use typst_library::math::EquationElem; use typst_library::math::EquationElem;
use typst_library::model::{ use typst_library::model::{
Destination, EnumElem, FigureCaption, FigureElem, FootnoteEntry, HeadingElem, Destination, EnumElem, FigureCaption, FigureElem, FootnoteEntry, HeadingElem,
@ -18,8 +19,10 @@ use typst_library::model::{
TermsElem, TermsElem,
}; };
use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind}; use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind};
use typst_library::text::{Lang, RawElem}; use typst_library::text::{
use typst_library::visualize::ImageElem; Lang, OverlineElem, RawElem, StrikeElem, TextItem, UnderlineElem,
};
use typst_library::visualize::{Image, ImageElem, Paint, Shape, Stroke};
use typst_syntax::Span; use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
@ -27,7 +30,7 @@ use crate::link::LinkAnnotation;
use crate::tags::list::ListCtx; use crate::tags::list::ListCtx;
use crate::tags::outline::OutlineCtx; use crate::tags::outline::OutlineCtx;
use crate::tags::table::TableCtx; use crate::tags::table::TableCtx;
use crate::tags::util::{PropertyOptRef, PropertyValCopied}; use crate::tags::util::{PropertyOptRef, PropertyValCloned, PropertyValCopied};
pub use context::*; pub use context::*;
@ -45,6 +48,9 @@ pub enum TagNode {
/// Currently used for [`krilla::page::Page::add_tagged_annotation`]. /// Currently used for [`krilla::page::Page::add_tagged_annotation`].
Placeholder(Placeholder), Placeholder(Placeholder),
FootnoteEntry(Location), FootnoteEntry(Location),
/// If the attributes are non-empty this will resolve to a [`Tag::Span`],
/// otherwise the items are inserted directly.
Text(ResolvedTextAttrs, Vec<Identifier>),
} }
impl TagNode { impl TagNode {
@ -108,7 +114,7 @@ pub fn handle_start(
return Ok(()); return Ok(());
} }
let mut tag: TagKind = if let Some(tag) = elem.to_packed::<PdfMarkerTag>() { let tag = if let Some(tag) = elem.to_packed::<PdfMarkerTag>() {
match &tag.kind { match &tag.kind {
PdfMarkerTagKind::OutlineBody => { PdfMarkerTagKind::OutlineBody => {
push_stack(gc, elem, StackEntryKind::Outline(OutlineCtx::new()))?; push_stack(gc, elem, StackEntryKind::Outline(OutlineCtx::new()))?;
@ -239,16 +245,59 @@ pub fn handle_start(
}); });
push_stack(gc, elem, StackEntryKind::Code(desc))?; push_stack(gc, elem, StackEntryKind::Code(desc))?;
return Ok(()); return Ok(());
} else if let Some(underline) = elem.to_packed::<UnderlineElem>() {
let loc = elem.location().unwrap();
let stroke = deco_stroke(underline.stroke.val_cloned());
gc.tags
.text_attrs
.push_underline(gc.options, loc, stroke)
.at(elem.span())?;
return Ok(());
} else if let Some(overline) = elem.to_packed::<OverlineElem>() {
let loc = elem.location().unwrap();
let stroke = deco_stroke(overline.stroke.val_cloned());
gc.tags
.text_attrs
.push_overline(gc.options, loc, stroke)
.at(elem.span())?;
return Ok(());
} else if let Some(strike) = elem.to_packed::<StrikeElem>() {
let loc = elem.location().unwrap();
let stroke = deco_stroke(strike.stroke.val_cloned());
gc.tags
.text_attrs
.push_strike(gc.options, loc, stroke)
.at(elem.span())?;
return Ok(());
} else { } else {
return Ok(()); return Ok(());
}; };
tag.set_location(Some(elem.span().into_raw()));
push_stack(gc, elem, StackEntryKind::Standard(tag))?; push_stack(gc, elem, StackEntryKind::Standard(tag))?;
Ok(()) Ok(())
} }
fn deco_stroke(stroke: Smart<Stroke>) -> TextDecoStroke {
let Smart::Custom(stroke) = stroke else {
return TextDecoStroke::default();
};
let color = stroke.paint.custom().and_then(|paint| match paint {
Paint::Solid(color) => {
let c = color.to_rgb();
Some(NaiveRgbColor::new(c.red, c.green, c.blue))
}
// TODO: Don't fail silently, maybe make a best effort to convert a
// gradient to a single solid color?
Paint::Gradient(_) => None,
// TODO: Don't fail silently, maybe just error in PDF/UA mode?
Paint::Tiling(_) => None,
});
let thickness = stroke.thickness.custom();
TextDecoStroke { color, thickness }
}
fn push_stack( fn push_stack(
gc: &mut GlobalContext, gc: &mut GlobalContext,
elem: &Content, elem: &Content,
@ -307,6 +356,10 @@ pub fn handle_end(
return Ok(()); return Ok(());
} }
if gc.tags.text_attrs.pop_deco(loc) {
return Ok(());
}
// Search for an improperly nested starting tag, that is being closed. // Search for an improperly nested starting tag, that is being closed.
let Some(idx) = (gc.tags.stack.iter().enumerate()) let Some(idx) = (gc.tags.stack.iter().enumerate())
.rev() .rev()
@ -318,11 +371,10 @@ pub fn handle_end(
// There are overlapping tags in the tag tree. Figure whether breaking // There are overlapping tags in the tag tree. Figure whether breaking
// up the current tag stack is semantically ok. // up the current tag stack is semantically ok.
let is_pdf_ua = gc.options.standards.config.validator() == Validator::UA1;
let mut is_breakable = true; let mut is_breakable = true;
let mut non_breakable_span = Span::detached(); let mut non_breakable_span = Span::detached();
for e in gc.tags.stack[idx + 1..].iter() { for e in gc.tags.stack[idx + 1..].iter() {
if e.kind.is_breakable(is_pdf_ua) { if e.kind.is_breakable(gc.options.is_pdf_ua()) {
continue; continue;
} }
@ -333,12 +385,12 @@ pub fn handle_end(
} }
} }
if !is_breakable { if !is_breakable {
let validator = gc.options.standards.config.validator(); if gc.options.is_pdf_ua() {
if is_pdf_ua { let validator = gc.options.standards.config.validator();
let ua1 = validator.as_str(); let validator = validator.as_str();
bail!( bail!(
non_breakable_span, non_breakable_span,
"{ua1} error: invalid semantic structure, \ "{validator} error: invalid semantic structure, \
this element's tag would be split up"; this element's tag would be split up";
hint: "maybe this is caused by a `parbreak`, `colbreak`, or `pagebreak`" hint: "maybe this is caused by a `parbreak`, `colbreak`, or `pagebreak`"
); );
@ -543,7 +595,66 @@ pub fn add_link_annotations(
} }
} }
pub fn update_bbox( pub fn text<'a, 'b>(
gc: &mut GlobalContext,
fc: &FrameContext,
surface: &'b mut Surface<'a>,
text: &TextItem,
) -> TagHandle<'a, 'b> {
if gc.options.disable_tags {
return TagHandle { surface, started: false };
}
update_bbox(gc, fc, || text.bbox());
if gc.tags.in_artifact.is_some() {
return TagHandle { surface, started: false };
}
let attrs = gc.tags.text_attrs.resolve(text.size);
// Marked content
let lang = gc.tags.try_set_lang(text.lang);
let lang = lang.as_ref().map(Lang::as_str);
let content = ContentTag::Span(SpanTag::empty().with_lang(lang));
let id = surface.start_tagged(content);
gc.tags.push_text(attrs, id);
TagHandle { surface, started: true }
}
pub fn image<'a, 'b>(
gc: &mut GlobalContext,
fc: &FrameContext,
surface: &'b mut Surface<'a>,
image: &Image,
size: Size,
) -> TagHandle<'a, 'b> {
if gc.options.disable_tags {
return TagHandle { surface, started: false };
}
update_bbox(gc, fc, || Rect::from_pos_size(Point::zero(), size));
let content = ContentTag::Span(SpanTag::empty().with_alt_text(image.alt()));
start_content(gc, surface, content)
}
pub fn shape<'a, 'b>(
gc: &mut GlobalContext,
fc: &FrameContext,
surface: &'b mut Surface<'a>,
shape: &Shape,
) -> TagHandle<'a, 'b> {
if gc.options.disable_tags {
return TagHandle { surface, started: false };
}
update_bbox(gc, fc, || shape.geometry.bbox());
start_content(gc, surface, ContentTag::Artifact(ArtifactType::Other))
}
fn update_bbox(
gc: &mut GlobalContext, gc: &mut GlobalContext,
fc: &FrameContext, fc: &FrameContext,
compute_bbox: impl FnOnce() -> Rect, compute_bbox: impl FnOnce() -> Rect,
@ -577,36 +688,11 @@ impl<'a> TagHandle<'a, '_> {
} }
} }
/// Returns a [`TagHandle`] that automatically calls [`Surface::end_tagged`]
/// when dropped.
pub fn start_span<'a, 'b>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
span: SpanTag,
) -> TagHandle<'a, 'b> {
start_content(gc, surface, ContentTag::Span(span))
}
/// Returns a [`TagHandle`] that automatically calls [`Surface::end_tagged`]
/// when dropped.
pub fn start_artifact<'a, 'b>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
kind: ArtifactKind,
) -> TagHandle<'a, 'b> {
let ty = artifact_type(kind);
start_content(gc, surface, ContentTag::Artifact(ty))
}
fn start_content<'a, 'b>( fn start_content<'a, 'b>(
gc: &mut GlobalContext, gc: &mut GlobalContext,
surface: &'b mut Surface<'a>, surface: &'b mut Surface<'a>,
content: ContentTag, content: ContentTag,
) -> TagHandle<'a, 'b> { ) -> TagHandle<'a, 'b> {
if gc.options.disable_tags {
return TagHandle { surface, started: false };
}
let content = if gc.tags.in_artifact.is_some() { let content = if gc.tags.in_artifact.is_some() {
return TagHandle { surface, started: false }; return TagHandle { surface, started: false };
} else if let Some(StackEntryKind::Table(_)) = gc.tags.stack.last().map(|e| &e.kind) { } else if let Some(StackEntryKind::Table(_)) = gc.tags.stack.last().map(|e| &e.kind) {

View File

@ -3,11 +3,10 @@ use std::sync::Arc;
use bytemuck::TransparentWrapper; use bytemuck::TransparentWrapper;
use krilla::surface::{Location, Surface}; use krilla::surface::{Location, Surface};
use krilla::tagging::SpanTag;
use krilla::text::GlyphId; use krilla::text::GlyphId;
use typst_library::diag::{SourceResult, bail}; use typst_library::diag::{SourceResult, bail};
use typst_library::layout::Size; use typst_library::layout::Size;
use typst_library::text::{Font, Glyph, Lang, TextItem}; use typst_library::text::{Font, Glyph, TextItem};
use typst_library::visualize::FillRule; use typst_library::visualize::FillRule;
use typst_syntax::Span; use typst_syntax::Span;
@ -22,11 +21,7 @@ pub(crate) fn handle_text(
surface: &mut Surface, surface: &mut Surface,
gc: &mut GlobalContext, gc: &mut GlobalContext,
) -> SourceResult<()> { ) -> SourceResult<()> {
let lang = gc.tags.try_set_lang(t.lang); let mut handle = tags::text(gc, fc, surface, t);
let lang = lang.as_ref().map(Lang::as_str);
tags::update_bbox(gc, fc, || t.bbox());
let mut handle = tags::start_span(gc, surface, SpanTag::empty().with_lang(lang));
let surface = handle.surface(); let surface = handle.surface();
let font = convert_font(gc, t.font.clone())?; let font = convert_font(gc, t.font.clone())?;

View File

@ -0,0 +1,7 @@
--- deco-tags-underline pdftags ---
#show: underline.with(stroke: red)
red underlined text
red underlined text
red underlined text