feat: underline, overline, and strike text decoration attributes

Artifact content ids are no longer added to the tag tree. This greatly
simplifies the grouping of text items into span tags with text decoration
attributes.
Artifact IDs are dummy ids anyway and more of an implementation detail
of how krilla writes marked content/artifacts.
This commit is contained in:
Tobias Schmitz 2025-08-01 16:02:58 +02:00
parent df6a4ba7f4
commit b312b45b19
No known key found for this signature in database
16 changed files with 415 additions and 207 deletions

View File

@ -5,11 +5,10 @@ use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::pdf::PdfDocument;
use krilla::surface::Surface;
use krilla::tagging::SpanTag;
use krilla_svg::{SurfaceExt, SvgSettings};
use typst_library::diag::{SourceResult, bail};
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::{
ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage,
};
@ -35,11 +34,9 @@ pub(crate) fn handle_image(
gc.image_spans.insert(span);
tags::update_bbox(gc, fc, || Rect::from_pos_size(Point::zero(), size));
let mut handle =
tags::start_span(gc, surface, SpanTag::empty().with_alt_text(image.alt()));
let mut handle = tags::image(gc, fc, surface, image, size);
let surface = handle.surface();
match image.kind() {
ImageKind::Raster(raster) => {
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 ecow::eco_format;
use krilla::configure::Validator;
use serde::{Deserialize, Serialize};
use typst_library::diag::{SourceResult, StrResult, bail};
use typst_library::foundations::Smart;
@ -67,6 +68,14 @@ pub struct PdfOptions<'a> {
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.
#[derive(Clone)]
pub struct PdfStandards {

View File

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

View File

@ -3,25 +3,32 @@ use std::collections::HashMap;
use std::slice::SliceIndex;
use krilla::geom as kg;
use krilla::tagging::{BBox, Node, TagKind, TagTree};
use typst_library::foundations::{LinkMarker, Packed};
use krilla::tagging::{
BBox, Identifier, LineHeight, NaiveRgbColor, Node, Tag, TagKind, TagTree,
TextDecorationType,
};
use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::{Content, LinkMarker, Packed};
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::pdf::ArtifactKind;
use typst_library::text::Lang;
use typst_syntax::Span;
use crate::PdfOptions;
use crate::convert::FrameContext;
use crate::tags::list::ListCtx;
use crate::tags::outline::OutlineCtx;
use crate::tags::table::TableCtx;
use crate::tags::{Placeholder, TagGroup, TagNode};
use crate::tags::{Placeholder, TagNode};
use crate::util::AbsExt;
pub struct Tags {
/// The language of the first text item that has been encountered.
pub doc_lang: Option<Lang>,
/// The current set of text attributes.
pub text_attrs: TextAttrs,
/// The intermediary stack of nested tag groups.
pub stack: TagStack,
/// A list of placeholders corresponding to a [`TagNode::Placeholder`].
@ -39,13 +46,14 @@ pub struct Tags {
table_id: TableId,
/// The output.
pub tree: Vec<TagNode>,
tree: Vec<TagNode>,
}
impl Tags {
pub fn new() -> Self {
Self {
doc_lang: None,
text_attrs: TextAttrs::new(),
stack: TagStack::new(),
placeholders: Placeholders(Vec::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>) {
if let Some(entry) = self.stack.last_mut() {
entry.nodes.extend(nodes);
@ -77,11 +102,11 @@ impl Tags {
pub fn build_tree(&mut self) -> TagTree {
assert!(self.stack.items.is_empty(), "tags weren't properly closed");
let children = std::mem::take(&mut self.tree)
.into_iter()
.map(|node| self.resolve_node(node))
.collect::<Vec<_>>();
TagTree::from(children)
let mut nodes = Vec::new();
for child in std::mem::take(&mut self.tree) {
self.resolve_node(&mut nodes, child);
}
TagTree::from(nodes)
}
/// 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
/// marked content directly.
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) {
self.doc_lang = Some(lang);
return None;
@ -106,23 +127,46 @@ impl Tags {
Some(lang)
}
/// Resolves [`Placeholder`] nodes.
fn resolve_node(&mut self, node: TagNode) -> Node {
/// Resolves nodes into an accumulator.
fn resolve_node(&mut self, accum: &mut Vec<Node>, node: TagNode) {
match node {
TagNode::Group(TagGroup { tag, nodes }) => {
let children = nodes
.into_iter()
.map(|node| self.resolve_node(node))
.collect::<Vec<_>>();
Node::Group(krilla::tagging::TagGroup::with_children(tag, children))
TagNode::Group(group) => {
let mut nodes = Vec::new();
for child in group.nodes {
self.resolve_node(&mut nodes, child);
}
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) => {
let node = (self.footnotes.remove(&loc))
.and_then(|ctx| ctx.entry)
.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,123 @@ 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_deco(
&mut self,
options: &PdfOptions,
elem: &Content,
kind: TextDecoKind,
stroke: TextDecoStroke,
) -> SourceResult<()> {
let deco = TextDeco { kind, stroke };
// 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!(
elem.span(),
"{validator} error: cannot combine underline, overline, and or strike"
);
}
let loc = elem.location().unwrap();
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)]
pub struct TagStack {
items: Vec<StackEntry>,

View File

@ -5,12 +5,13 @@ use krilla::configure::Validator;
use krilla::page::Page;
use krilla::surface::Surface;
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::foundations::{Content, LinkMarker};
use typst_library::foundations::{Content, LinkMarker, Smart};
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::model::{
Destination, EnumElem, FigureCaption, FigureElem, FootnoteEntry, HeadingElem,
@ -18,8 +19,10 @@ use typst_library::model::{
TermsElem,
};
use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind};
use typst_library::text::{Lang, RawElem};
use typst_library::visualize::ImageElem;
use typst_library::text::{
Lang, OverlineElem, RawElem, StrikeElem, TextItem, UnderlineElem,
};
use typst_library::visualize::{Image, ImageElem, Paint, Shape, Stroke};
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext};
@ -27,7 +30,7 @@ use crate::link::LinkAnnotation;
use crate::tags::list::ListCtx;
use crate::tags::outline::OutlineCtx;
use crate::tags::table::TableCtx;
use crate::tags::util::{PropertyOptRef, PropertyValCopied};
use crate::tags::util::{PropertyOptRef, PropertyValCloned, PropertyValCopied};
pub use context::*;
@ -45,6 +48,9 @@ pub enum TagNode {
/// Currently used for [`krilla::page::Page::add_tagged_annotation`].
Placeholder(Placeholder),
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 {
@ -108,7 +114,7 @@ pub fn handle_start(
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 {
PdfMarkerTagKind::OutlineBody => {
push_stack(gc, elem, StackEntryKind::Outline(OutlineCtx::new()))?;
@ -239,16 +245,50 @@ pub fn handle_start(
});
push_stack(gc, elem, StackEntryKind::Code(desc))?;
return Ok(());
} else if let Some(underline) = elem.to_packed::<UnderlineElem>() {
let kind = TextDecoKind::Underline;
let stroke = deco_stroke(underline.stroke.val_cloned());
gc.tags.text_attrs.push_deco(gc.options, elem, kind, stroke)?;
return Ok(());
} else if let Some(overline) = elem.to_packed::<OverlineElem>() {
let kind = TextDecoKind::Overline;
let stroke = deco_stroke(overline.stroke.val_cloned());
gc.tags.text_attrs.push_deco(gc.options, elem, kind, stroke)?;
return Ok(());
} else if let Some(strike) = elem.to_packed::<StrikeElem>() {
let kind = TextDecoKind::Strike;
let stroke = deco_stroke(strike.stroke.val_cloned());
gc.tags.text_attrs.push_deco(gc.options, elem, kind, stroke)?;
return Ok(());
} else {
return Ok(());
};
tag.set_location(Some(elem.span().into_raw()));
push_stack(gc, elem, StackEntryKind::Standard(tag))?;
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(
gc: &mut GlobalContext,
elem: &Content,
@ -280,8 +320,7 @@ fn push_artifact(
) {
let loc = elem.location().expect("elem to be locatable");
let ty = artifact_type(kind);
let id = surface.start_tagged(ContentTag::Artifact(ty));
gc.tags.push(TagNode::Leaf(id));
surface.start_tagged(ContentTag::Artifact(ty));
gc.tags.in_artifact = Some((loc, kind));
}
@ -307,6 +346,10 @@ pub fn handle_end(
return Ok(());
}
if gc.tags.text_attrs.pop_deco(loc) {
return Ok(());
}
// Search for an improperly nested starting tag, that is being closed.
let Some(idx) = (gc.tags.stack.iter().enumerate())
.rev()
@ -318,11 +361,10 @@ pub fn handle_end(
// There are overlapping tags in the tag tree. Figure whether breaking
// 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 non_breakable_span = Span::detached();
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;
}
@ -333,12 +375,12 @@ pub fn handle_end(
}
}
if !is_breakable {
let validator = gc.options.standards.config.validator();
if is_pdf_ua {
let ua1 = validator.as_str();
if gc.options.is_pdf_ua() {
let validator = gc.options.standards.config.validator();
let validator = validator.as_str();
bail!(
non_breakable_span,
"{ua1} error: invalid semantic structure, \
"{validator} error: invalid semantic structure, \
this element's tag would be split up";
hint: "maybe this is caused by a `parbreak`, `colbreak`, or `pagebreak`"
);
@ -503,8 +545,7 @@ pub fn page_start(gc: &mut GlobalContext, surface: &mut Surface) {
if let Some((_, kind)) = gc.tags.in_artifact {
let ty = artifact_type(kind);
let id = surface.start_tagged(ContentTag::Artifact(ty));
gc.tags.push(TagNode::Leaf(id));
surface.start_tagged(ContentTag::Artifact(ty));
}
}
@ -543,7 +584,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,
fc: &FrameContext,
compute_bbox: impl FnOnce() -> Rect,
@ -577,48 +677,30 @@ 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>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
content: ContentTag,
) -> TagHandle<'a, 'b> {
if gc.options.disable_tags {
return TagHandle { surface, started: false };
}
let content = if gc.tags.in_artifact.is_some() {
if gc.tags.in_artifact.is_some() {
return TagHandle { surface, started: false };
} else if let Some(StackEntryKind::Table(_)) = gc.tags.stack.last().map(|e| &e.kind) {
// TODO: handle this more like other artifacts
// Mark any direct child of a table as an aritfact. Any real content
// will be wrapped inside a `TableCell`.
ContentTag::Artifact(ArtifactType::Other)
// Don't store artifact content ids, they will be omitted anyway when
// serializing the tag tree.
surface.start_tagged(ContentTag::Artifact(ArtifactType::Other));
TagHandle { surface, started: true }
} else {
content
};
let id = surface.start_tagged(content);
gc.tags.push(TagNode::Leaf(id));
TagHandle { surface, started: true }
let artifact = matches!(content, ContentTag::Artifact(_));
let id = surface.start_tagged(content);
if !artifact {
gc.tags.push(TagNode::Leaf(id));
}
TagHandle { surface, started: true }
}
}
fn artifact_type(kind: ArtifactKind) -> ArtifactType {

View File

@ -3,11 +3,10 @@ use std::sync::Arc;
use bytemuck::TransparentWrapper;
use krilla::surface::{Location, Surface};
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, Lang, TextItem};
use typst_library::text::{Font, Glyph, TextItem};
use typst_library::visualize::FillRule;
use typst_syntax::Span;
@ -22,11 +21,7 @@ pub(crate) fn handle_text(
surface: &mut Surface,
gc: &mut GlobalContext,
) -> SourceResult<()> {
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().with_lang(lang));
let mut handle = tags::text(gc, fc, surface, t);
let surface = handle.surface();
let font = convert_font(gc, t.font.clone())?;

View File

@ -0,0 +1,13 @@
- Tag: P
/K:
- Tag: Span
/TextDecorationColor: #ff4136
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=0
- Tag: Span
/TextDecorationColor: #0074d9
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=1
- Content: page=0 mcid=2

View File

@ -0,0 +1,13 @@
- Tag: P
/K:
- Tag: Span
/TextDecorationThickness: 2.000
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=0
- Tag: Span
/TextDecorationThickness: 1.000
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=1
- Content: page=0 mcid=2

View File

@ -0,0 +1,14 @@
- Tag: P
/K:
- Tag: Span
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=0
- Tag: Span
/TextDecorationType: Overline
/K:
- Content: page=0 mcid=1
- Tag: Span
/TextDecorationType: LineThrough
/K:
- Content: page=0 mcid=2

View File

@ -0,0 +1,15 @@
- Tag: P
/K:
- Tag: Span
/TextDecorationColor: #ff4136
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=0
- Content: page=0 mcid=1
- Tag: P
/K:
- Tag: Span
/TextDecorationColor: #ff4136
/TextDecorationType: Underline
/K:
- Content: page=0 mcid=2

View File

@ -36,11 +36,6 @@
right: 90.250
bottom: 71.820
/K:
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Tag: TBody
/K:
- Tag: TR
@ -88,13 +83,6 @@
right: 97.635
bottom: 199.833
/K:
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Tag: TBody
/K:
- Tag: TR

View File

@ -1,80 +1,5 @@
- Tag: Table
/K:
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Tag: THead
/K:
- Tag: TR

View File

@ -6,14 +6,6 @@
right: 76.350
bottom: 60.240
/K:
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Tag: THead
/K:
- Tag: TR

View File

@ -6,14 +6,6 @@
right: 85.360
bottom: 60.240
/K:
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Tag: TR
/K:
- Tag: TH

View File

@ -6,17 +6,6 @@
right: 76.350
bottom: 96.820
/K:
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Artifact
- Tag: THead
/K:
- Tag: TR

View File

@ -0,0 +1,26 @@
--- deco-tags-underline pdftags ---
#show: underline.with(stroke: red)
// The content in this paragraph is grouped into one span tag with the
// corresponding text attributes.
red underlined text
red underlined text
red underlined text
--- deco-tags-different-color pdftags ---
#show: underline.with(stroke: red)
red underlined text
#show: underline.with(stroke: blue)
blue underlined text
--- deco-tags-different-stroke-thickness pdftags ---
#show: underline.with(stroke: 2pt)
red underlined text
#show: underline.with(stroke: 1pt)
blue underlined text
--- deco-tags-different-type pdftags ---
#underline[underlined]\
#overline[overlined]\
#strike[striked]\