mirror of
https://github.com/typst/typst
synced 2025-08-03 01:37:54 +08:00
feat: [WIP] text decoration attributes
This commit is contained in:
parent
df6a4ba7f4
commit
b2d9bd2fb4
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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());
|
||||||
|
@ -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>,
|
||||||
|
@ -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) {
|
||||||
|
@ -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())?;
|
||||||
|
7
tests/suite/pdftags/deco.typ
Normal file
7
tests/suite/pdftags/deco.typ
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
--- deco-tags-underline pdftags ---
|
||||||
|
#show: underline.with(stroke: red)
|
||||||
|
|
||||||
|
red underlined text
|
||||||
|
red underlined text
|
||||||
|
|
||||||
|
red underlined text
|
Loading…
x
Reference in New Issue
Block a user