diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 886350e22..f4f79205f 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; use ecow::{EcoVec, eco_format}; use krilla::configure::{Configuration, ValidationError, Validator}; @@ -19,7 +19,7 @@ use typst_library::layout::{ Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, }; use typst_library::model::HeadingElem; -use typst_library::text::{Font, Lang}; +use typst_library::text::Font; use typst_library::visualize::{Geometry, Paint}; use typst_syntax::Span; @@ -257,8 +257,6 @@ pub(crate) struct GlobalContext<'a> { pub(crate) options: &'a PdfOptions<'a>, /// Mapping between locations in the document and named destinations. pub(crate) loc_to_names: HashMap, - /// The languages used throughout the document. - pub(crate) languages: BTreeMap, pub(crate) page_index_converter: PageIndexConverter, /// Tagged PDF context. pub(crate) tags: Tags, @@ -279,7 +277,6 @@ impl<'a> GlobalContext<'a> { loc_to_names, image_to_spans: HashMap::new(), image_spans: HashSet::new(), - languages: BTreeMap::new(), page_index_converter, tags: Tags::new(), diff --git a/crates/typst-pdf/src/metadata.rs b/crates/typst-pdf/src/metadata.rs index d51cb5335..3421fb5d5 100644 --- a/crates/typst-pdf/src/metadata.rs +++ b/crates/typst-pdf/src/metadata.rs @@ -1,17 +1,19 @@ use ecow::EcoString; use krilla::metadata::{Metadata, TextDirection}; -use typst_library::foundations::{Datetime, Smart}; +use typst_library::foundations::{Datetime, Smart, StyleChain}; use typst_library::layout::Dir; -use typst_library::text::Lang; +use typst_library::text::TextElem; use crate::convert::GlobalContext; pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata { let creator = format!("Typst {}", env!("CARGO_PKG_VERSION")); - let lang = gc.languages.iter().max_by_key(|&(_, &count)| count).map(|(&l, _)| l); + // Always write a language, PDF/UA-1 implicitly requires a document language + // so the metadata and outline entries have an applicable language. + let lang = gc.tags.doc_lang.unwrap_or(StyleChain::default().get(TextElem::lang)); - let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + let dir = if lang.dir() == Dir::RTL { TextDirection::RightToLeft } else { TextDirection::LeftToRight @@ -20,11 +22,8 @@ pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata { let mut metadata = Metadata::new() .creator(creator) .keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect()) - .authors(gc.document.info.author.iter().map(EcoString::to_string).collect()); - - if let Some(lang) = lang { - metadata = metadata.language(lang.as_str().to_string()); - } + .authors(gc.document.info.author.iter().map(EcoString::to_string).collect()) + .language(lang.as_str().to_string()); if let Some(title) = &gc.document.info.title { metadata = metadata.title(title.to_string()); diff --git a/crates/typst-pdf/src/tags/list.rs b/crates/typst-pdf/src/tags/list.rs index 38bbe8378..59cb62bc3 100644 --- a/crates/typst-pdf/src/tags/list.rs +++ b/crates/typst-pdf/src/tags/list.rs @@ -1,6 +1,6 @@ use krilla::tagging::{ListNumbering, Tag, TagKind}; -use crate::tags::TagNode; +use crate::tags::{GroupContents, TagNode}; #[derive(Clone, Debug)] pub(crate) struct ListCtx { @@ -10,8 +10,8 @@ pub(crate) struct ListCtx { #[derive(Clone, Debug)] struct ListItem { - label: Vec, - body: Option>, + label: TagNode, + body: Option, sub_list: Option, } @@ -20,11 +20,15 @@ impl ListCtx { Self { numbering, items: Vec::new() } } - pub(crate) fn push_label(&mut self, nodes: Vec) { - self.items.push(ListItem { label: nodes, body: None, sub_list: None }); + pub(crate) fn push_label(&mut self, contents: GroupContents) { + self.items.push(ListItem { + label: TagNode::group(Tag::Lbl, contents), + body: None, + sub_list: None, + }); } - pub(crate) fn push_body(&mut self, mut nodes: Vec) { + pub(crate) fn push_body(&mut self, mut contents: GroupContents) { let item = self.items.last_mut().expect("ListItemLabel"); // Nested lists are expected to have the following structure: @@ -60,40 +64,43 @@ impl ListCtx { // ``` // // So move the nested list out of the list item. - if let [_, TagNode::Group(TagKind::L(_), _)] = nodes.as_slice() { - item.sub_list = nodes.pop(); + if let [_, TagNode::Group(group)] = contents.nodes.as_slice() + && let TagKind::L(_) = group.tag + { + item.sub_list = contents.nodes.pop(); } - item.body = Some(nodes); + item.body = Some(TagNode::group(Tag::LBody, contents)); } - pub(crate) fn push_bib_entry(&mut self, nodes: Vec) { - let nodes = vec![TagNode::group(Tag::BibEntry, nodes)]; + pub(crate) fn push_bib_entry(&mut self, contents: GroupContents) { + let nodes = vec![TagNode::group(Tag::BibEntry, contents)]; // Bibliography lists cannot be nested, but may be missing labels. + let body = TagNode::virtual_group(Tag::LBody, nodes); if let Some(item) = self.items.last_mut().filter(|item| item.body.is_none()) { - item.body = Some(nodes); + item.body = Some(body); } else { self.items.push(ListItem { - label: Vec::new(), - body: Some(nodes), + label: TagNode::empty_group(Tag::Lbl), + body: Some(body), sub_list: None, }); } } - pub(crate) fn build_list(self, mut nodes: Vec) -> TagNode { + pub(crate) fn build_list(self, mut contents: GroupContents) -> TagNode { for item in self.items.into_iter() { - nodes.push(TagNode::group( + contents.nodes.push(TagNode::virtual_group( Tag::LI, vec![ - TagNode::group(Tag::Lbl, item.label), - TagNode::group(Tag::LBody, item.body.unwrap_or_default()), + item.label, + item.body.unwrap_or_else(|| TagNode::empty_group(Tag::LBody)), ], )); if let Some(sub_list) = item.sub_list { - nodes.push(sub_list); + contents.nodes.push(sub_list); } } - TagNode::group(Tag::L(self.numbering), nodes) + TagNode::group(Tag::L(self.numbering), contents) } } diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index f6e8398e6..3c45bc250 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -10,7 +10,7 @@ use krilla::page::Page; use krilla::surface::Surface; use krilla::tagging::{ ArtifactType, BBox, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag, - TagGroup, TagKind, TagTree, + TagKind, TagTree, }; use typst_library::diag::{SourceResult, bail}; use typst_library::foundations::{Content, LinkMarker, Packed}; @@ -23,7 +23,7 @@ use typst_library::model::{ TermsElem, }; use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind}; -use typst_library::text::RawElem; +use typst_library::text::{Lang, RawElem}; use typst_library::visualize::ImageElem; use typst_syntax::Span; @@ -220,7 +220,9 @@ fn push_stack( } } - gc.tags.stack.push(StackEntry { loc, span, kind, nodes: Vec::new() }); + gc.tags + .stack + .push(StackEntry { loc, span, lang: None, kind, nodes: Vec::new() }); Ok(()) } @@ -324,6 +326,7 @@ pub(crate) fn handle_end( broken_entries.push(StackEntry { loc: entry.loc, span: entry.span, + lang: None, kind, nodes: Vec::new(), }); @@ -341,73 +344,77 @@ pub(crate) fn handle_end( } fn pop_stack(gc: &mut GlobalContext, entry: StackEntry) { + // Try to propagate the tag language to the parent tag, or the document. + // If successfull omit the language attribute on this tag. + let lang = entry.lang.and_then(|lang| { + let parent_lang = (gc.tags.stack.last_mut()) + .map(|e| &mut e.lang) + .unwrap_or(&mut gc.tags.doc_lang); + if parent_lang.is_none_or(|l| l == lang) { + *parent_lang = Some(lang); + return None; + } + Some(lang) + }); + + let contents = GroupContents { span: entry.span, lang, nodes: entry.nodes }; let node = match entry.kind { - StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes), - StackEntryKind::Outline(ctx) => ctx.build_outline(entry.nodes), + StackEntryKind::Standard(tag) => TagNode::group(tag, contents), + StackEntryKind::Outline(ctx) => ctx.build_outline(contents), StackEntryKind::OutlineEntry(outline_entry) => { - let Some((outline_ctx, outline_nodes)) = gc.tags.stack.parent_outline() - else { - // PDF/UA compliance of the structure hierarchy is checked - // elsewhere. While this doesn't make a lot of sense, just - // avoid crashing here. - gc.tags.push(TagNode::group(Tag::TOCI, entry.nodes)); - return; - }; + // FIXME(accessibility): disallow usage of `outline.entry` outside of `outline` + let (outline_ctx, outline_nodes) = (gc.tags.stack.parent_outline()) + .expect("outline entries may only exist within an outline"); - outline_ctx.insert(outline_nodes, outline_entry, entry.nodes); + outline_ctx.insert(outline_nodes, outline_entry, contents); return; } - StackEntryKind::Table(ctx) => ctx.build_table(entry.nodes), + StackEntryKind::Table(ctx) => ctx.build_table(contents), StackEntryKind::TableCell(cell) => { - let Some(table_ctx) = gc.tags.stack.parent_table() else { - // PDF/UA compliance of the structure hierarchy is checked - // elsewhere. While this doesn't make a lot of sense, just - // avoid crashing here. - let tag = Tag::TD.with_location(Some(cell.span().into_raw())); - gc.tags.push(TagNode::group(tag, entry.nodes)); - return; - }; + // FIXME(accessibility): disallow usage of `table.cell` and `grid.cell` outside of table/grid + let table_ctx = (gc.tags.stack.parent_table()) + .expect("table cells may only exist within a table"); - table_ctx.insert(&cell, entry.nodes); + table_ctx.insert(&cell, contents); return; } - StackEntryKind::List(list) => list.build_list(entry.nodes), + StackEntryKind::List(list) => list.build_list(contents), StackEntryKind::ListItemLabel => { let list_ctx = gc.tags.stack.parent_list().expect("parent list"); - list_ctx.push_label(entry.nodes); + list_ctx.push_label(contents); return; } StackEntryKind::ListItemBody => { let list_ctx = gc.tags.stack.parent_list().expect("parent list"); - list_ctx.push_body(entry.nodes); + list_ctx.push_body(contents); return; } StackEntryKind::BibEntry => { let list_ctx = gc.tags.stack.parent_list().expect("parent list"); - list_ctx.push_bib_entry(entry.nodes); + list_ctx.push_bib_entry(contents); return; } StackEntryKind::Figure(ctx) => { let tag = Tag::Figure(ctx.alt).with_bbox(ctx.bbox.get()); - TagNode::group(tag, entry.nodes) + TagNode::group(tag, contents) } StackEntryKind::Formula(ctx) => { let tag = Tag::Formula(ctx.alt).with_bbox(ctx.bbox.get()); - TagNode::group(tag, entry.nodes) + TagNode::group(tag, contents) } StackEntryKind::Link(_, link) => { let alt = link.alt.as_ref().map(EcoString::to_string); let tag = Tag::Link.with_alt_text(alt); - let mut node = TagNode::group(tag, entry.nodes); + let mut node = TagNode::group(tag, contents); // Wrap link in reference tag, if it's not a url. if let Destination::Position(_) | Destination::Location(_) = link.dest { - node = TagNode::group(Tag::Reference, vec![node]); + node = TagNode::virtual_group(Tag::Reference, vec![node]); } node } StackEntryKind::FootnoteRef(decl_loc) => { // transparently insert all children. - gc.tags.extend(entry.nodes); + gc.tags.extend(contents.nodes); let ctx = gc.tags.footnotes.entry(decl_loc).or_insert(FootnoteCtx::new()); @@ -421,16 +428,16 @@ fn pop_stack(gc: &mut GlobalContext, entry: StackEntry) { StackEntryKind::FootnoteEntry(footnote_loc) => { // Store footnotes separately so they can be inserted directly after // the footnote reference in the reading order. - let tag = TagNode::group(Tag::Note, entry.nodes); + let tag = TagNode::group(Tag::Note, contents); let ctx = gc.tags.footnotes.entry(footnote_loc).or_insert(FootnoteCtx::new()); ctx.entry = Some(tag); return; } StackEntryKind::Code(desc) => { - let code = TagNode::group(Tag::Code, entry.nodes); + let code = TagNode::group(Tag::Code, contents); if desc.is_some() { - let desc = TagNode::group(Tag::Span.with_alt_text(desc), Vec::new()); - TagNode::group(Tag::NonStruct, vec![desc, code]) + let desc = TagNode::empty_group(Tag::Span.with_alt_text(desc)); + TagNode::virtual_group(Tag::NonStruct, vec![desc, code]) } else { code } @@ -505,6 +512,8 @@ pub(crate) fn update_bbox( } pub(crate) struct Tags { + /// The language of the first text item that has been encountered. + pub(crate) doc_lang: Option, /// The intermediary stack of nested tag groups. pub(crate) stack: TagStack, /// A list of placeholders corresponding to a [`TagNode::Placeholder`]. @@ -528,6 +537,7 @@ pub(crate) struct Tags { impl Tags { pub(crate) fn new() -> Self { Self { + doc_lang: None, stack: TagStack::new(), placeholders: Placeholders(Vec::new()), footnotes: HashMap::new(), @@ -564,15 +574,37 @@ impl Tags { TagTree::from(children) } + /// Try to set the language of a parent tag, or the entire document. + /// If the language couldn't be set and is different from the existing one, + /// this will return `Some`, and the language should be specified on the + /// marked content directly. + pub(crate) fn try_set_lang(&mut self, lang: Lang) -> Option { + // Discard languages within artifacts. + if self.in_artifact.is_some() { + return None; + } + if self.doc_lang.is_none_or(|l| l == lang) { + self.doc_lang = Some(lang); + return None; + } + if let Some(last) = self.stack.last_mut() + && last.lang.is_none_or(|l| l == lang) + { + last.lang = Some(lang); + return None; + } + Some(lang) + } + /// Resolves [`Placeholder`] nodes. fn resolve_node(&mut self, node: TagNode) -> Node { match node { - TagNode::Group(tag, nodes) => { + TagNode::Group(TagGroup { tag, nodes }) => { let children = nodes .into_iter() .map(|node| self.resolve_node(node)) .collect::>(); - Node::Group(TagGroup::with_children(tag, children)) + Node::Group(krilla::tagging::TagGroup::with_children(tag, children)) } TagNode::Leaf(identifier) => Node::Leaf(identifier), TagNode::Placeholder(placeholder) => self.placeholders.take(placeholder), @@ -762,6 +794,7 @@ pub(crate) struct LinkId(u32); pub(crate) struct StackEntry { pub(crate) loc: Location, pub(crate) span: Span, + pub(crate) lang: Option, pub(crate) kind: StackEntryKind, pub(crate) nodes: Vec, } @@ -995,7 +1028,7 @@ impl BBoxCtx { #[derive(Debug, Clone, PartialEq)] pub(crate) enum TagNode { - Group(TagKind, Vec), + Group(TagGroup), Leaf(Identifier), /// Allows inserting a placeholder into the tag tree. /// Currently used for [`krilla::page::Page::add_tagged_annotation`]. @@ -1004,9 +1037,38 @@ pub(crate) enum TagNode { } impl TagNode { - pub fn group(tag: impl Into, children: Vec) -> Self { - TagNode::Group(tag.into(), children) + pub fn group(tag: impl Into, contents: GroupContents) -> Self { + let lang = contents.lang.map(|l| l.as_str().to_string()); + let tag = tag + .into() + .with_lang(lang) + .with_location(Some(contents.span.into_raw())); + TagNode::Group(TagGroup { tag, nodes: contents.nodes }) } + + /// A tag group not directly related to a typst element, generated to + /// accomodate the tag structure. + pub fn virtual_group(tag: impl Into, nodes: Vec) -> Self { + let tag = tag.into(); + TagNode::Group(TagGroup { tag, nodes }) + } + + pub fn empty_group(tag: impl Into) -> Self { + Self::virtual_group(tag, Vec::new()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TagGroup { + tag: TagKind, + nodes: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GroupContents { + span: Span, + lang: Option, + nodes: Vec, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/typst-pdf/src/tags/outline.rs b/crates/typst-pdf/src/tags/outline.rs index 4ce5e57da..3c4ac6087 100644 --- a/crates/typst-pdf/src/tags/outline.rs +++ b/crates/typst-pdf/src/tags/outline.rs @@ -2,7 +2,7 @@ use krilla::tagging::Tag; use typst_library::foundations::Packed; use typst_library::model::OutlineEntry; -use crate::tags::TagNode; +use crate::tags::{GroupContents, TagNode}; #[derive(Clone, Debug)] pub(crate) struct OutlineCtx { @@ -18,7 +18,7 @@ impl OutlineCtx { &mut self, outline_nodes: &mut Vec, entry: Packed, - nodes: Vec, + contents: GroupContents, ) { let expected_len = entry.level.get() - 1; if self.stack.len() < expected_len { @@ -29,7 +29,7 @@ impl OutlineCtx { } } - let section_entry = TagNode::group(Tag::TOCI, nodes); + let section_entry = TagNode::group(Tag::TOCI, contents); self.push(outline_nodes, section_entry); } @@ -45,11 +45,11 @@ impl OutlineCtx { } } - pub(crate) fn build_outline(mut self, mut outline_nodes: Vec) -> TagNode { + pub(crate) fn build_outline(mut self, mut contents: GroupContents) -> TagNode { while !self.stack.is_empty() { - self.finish_section(&mut outline_nodes); + self.finish_section(&mut contents.nodes); } - TagNode::group(Tag::TOC, outline_nodes) + TagNode::group(Tag::TOC, contents) } } @@ -68,6 +68,6 @@ impl OutlineSection { } fn into_tag(self) -> TagNode { - TagNode::group(Tag::TOC, self.entries) + TagNode::virtual_group(Tag::TOC, self.entries) } } diff --git a/crates/typst-pdf/src/tags/table.rs b/crates/typst-pdf/src/tags/table.rs index c2f273130..057503e45 100644 --- a/crates/typst-pdf/src/tags/table.rs +++ b/crates/typst-pdf/src/tags/table.rs @@ -7,10 +7,9 @@ use smallvec::SmallVec; use typst_library::foundations::{Packed, Smart}; use typst_library::model::TableCell; use typst_library::pdf::{TableCellKind, TableHeaderScope}; -use typst_syntax::Span; use crate::tags::util::PropertyValCopied; -use crate::tags::{BBoxCtx, TableId, TagNode}; +use crate::tags::{BBoxCtx, GroupContents, TableId, TagNode}; #[derive(Clone, Debug)] pub(crate) struct TableCtx { @@ -64,7 +63,7 @@ impl TableCtx { } } - pub(crate) fn insert(&mut self, cell: &Packed, nodes: Vec) { + pub(crate) fn insert(&mut self, cell: &Packed, contents: GroupContents) { let x = cell.x.val().unwrap_or_else(|| unreachable!()); let y = cell.y.val().unwrap_or_else(|| unreachable!()); let rowspan = cell.rowspan.val(); @@ -98,16 +97,15 @@ impl TableCtx { colspan: colspan.try_into().unwrap_or(NonZeroU32::MAX), kind, headers: SmallVec::new(), - nodes, - span: cell.span(), + contents, }); } - pub(crate) fn build_table(mut self, mut nodes: Vec) -> TagNode { + pub(crate) fn build_table(mut self, mut contents: GroupContents) -> TagNode { // Table layouting ensures that there are no overlapping cells, and that // any gaps left by the user are filled with empty cells. if self.rows.is_empty() { - return TagNode::group(Tag::Table.with_summary(self.summary), nodes); + return TagNode::group(Tag::Table.with_summary(self.summary), contents); } let height = self.rows.len(); let width = self.rows[0].len(); @@ -174,7 +172,7 @@ impl TableCtx { let cell = cell.into_cell()?; let rowspan = (cell.rowspan.get() != 1).then_some(cell.rowspan); let colspan = (cell.colspan.get() != 1).then_some(cell.colspan); - let tag = match cell.unwrap_kind() { + let tag: TagKind = match cell.unwrap_kind() { TableCellKind::Header(_, scope) => { let id = table_cell_id(self.id, cell.x, cell.y); let scope = table_header_scope(scope); @@ -183,25 +181,23 @@ impl TableCtx { .with_headers(Some(cell.headers)) .with_row_span(rowspan) .with_col_span(colspan) - .with_location(Some(cell.span.into_raw())) .into() } TableCellKind::Footer | TableCellKind::Data => Tag::TD .with_headers(Some(cell.headers)) .with_row_span(rowspan) .with_col_span(colspan) - .with_location(Some(cell.span.into_raw())) .into(), }; - Some(TagNode::Group(tag, cell.nodes)) + Some(TagNode::group(tag, cell.contents)) }) .collect(); - let row = TagNode::group(Tag::TR, row_nodes); + let row = TagNode::virtual_group(Tag::TR, row_nodes); // Push the `TR` tags directly. if !gen_row_groups { - nodes.push(row); + contents.nodes.push(row); continue; } @@ -212,7 +208,8 @@ impl TableCtx { TableCellKind::Footer => Tag::TFoot.into(), TableCellKind::Data => Tag::TBody.into(), }; - nodes.push(TagNode::Group(tag, std::mem::take(&mut row_chunk))); + let chunk_nodes = std::mem::take(&mut row_chunk); + contents.nodes.push(TagNode::virtual_group(tag, chunk_nodes)); chunk_kind = row_kind; } @@ -225,14 +222,11 @@ impl TableCtx { TableCellKind::Footer => Tag::TFoot.into(), TableCellKind::Data => Tag::TBody.into(), }; - nodes.push(TagNode::Group(tag, row_chunk)); + contents.nodes.push(TagNode::virtual_group(tag, row_chunk)); } - let tag = Tag::Table - .with_summary(self.summary) - .with_bbox(self.bbox.get()) - .into(); - TagNode::Group(tag, nodes) + let tag = Tag::Table.with_summary(self.summary).with_bbox(self.bbox.get()); + TagNode::group(tag, contents) } fn resolve_cell_headers( @@ -297,8 +291,7 @@ struct TableCtxCell { colspan: NonZeroU32, kind: Smart, headers: SmallVec<[TagId; 1]>, - nodes: Vec, - span: Span, + contents: GroupContents, } impl TableCtxCell { diff --git a/crates/typst-pdf/src/text.rs b/crates/typst-pdf/src/text.rs index 293a6ff83..f5f8b2782 100644 --- a/crates/typst-pdf/src/text.rs +++ b/crates/typst-pdf/src/text.rs @@ -7,7 +7,7 @@ use krilla::tagging::SpanTag; use krilla::text::GlyphId; use typst_library::diag::{SourceResult, bail}; use typst_library::layout::Size; -use typst_library::text::{Font, Glyph, TextItem}; +use typst_library::text::{Font, Glyph, Lang, TextItem}; use typst_library::visualize::FillRule; use typst_syntax::Span; @@ -22,11 +22,11 @@ pub(crate) fn handle_text( surface: &mut Surface, gc: &mut GlobalContext, ) -> SourceResult<()> { - *gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); - + let lang = gc.tags.try_set_lang(t.lang); + 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()); + let mut handle = tags::start_span(gc, surface, SpanTag::empty().with_lang(lang)); let surface = handle.surface(); let font = convert_font(gc, t.font.clone())?; diff --git a/tests/ref/pdftags/quote-dir-align.yml b/tests/ref/pdftags/quote-dir-align.yml index ded5274f9..8c56e21d8 100644 --- a/tests/ref/pdftags/quote-dir-align.yml +++ b/tests/ref/pdftags/quote-dir-align.yml @@ -3,6 +3,7 @@ - Content: page=0 mcid=0 - Content: page=0 mcid=1 - Tag: BlockQuote + /Lang: "ar" /K: - Content: page=0 mcid=2 - Content: page=0 mcid=3 diff --git a/tests/ref/pdftags/quote-dir-author-pos.yml b/tests/ref/pdftags/quote-dir-author-pos.yml index 549c77926..767b17a34 100644 --- a/tests/ref/pdftags/quote-dir-author-pos.yml +++ b/tests/ref/pdftags/quote-dir-author-pos.yml @@ -7,6 +7,7 @@ - Content: page=0 mcid=2 - Content: page=0 mcid=3 - Tag: InlineQuote + /Lang: "ar" /K: - Tag: P /K: