#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Document {
pub pages: Vec<Page>,
pub metadata: DocumentMetadata,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentMetadata {
pub format_version: Option<FormatVersion>,
pub created_ms: Option<i64>,
pub modified_ms: Option<i64>,
pub background_color: Option<Color>,
pub dark_mode_compatibility: Option<bool>,
pub page_dimensions: Option<(u32, u32)>,
#[cfg_attr(feature = "serde", serde(default))]
pub default_page_dimensions: Option<(u32, u32)>,
#[cfg_attr(feature = "serde", serde(default))]
pub page_mode: Option<u16>,
#[cfg_attr(feature = "serde", serde(default))]
pub orientation: Option<i32>,
pub flow_dimensions: Option<(u32, u32)>,
pub flow_page_padding: Option<(u32, u32)>,
pub page_ids: Vec<String>,
pub media_assets: Vec<MediaAsset>,
pub note_text: Option<RichTextBox>,
pub note_title: Option<RichTextBox>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FormatVersion(pub u16);
impl FormatVersion {
pub const INITIAL: Self = Self(2034);
pub const MINIMUM_SUPPORTED: Self = Self(4000);
pub const MATH_OBJECTS: Self = Self(5200);
pub const TABLE_AND_CODE_BLOCK_OBJECTS: Self = Self(5400);
pub const CURRENT: Self = Self(5500);
pub const fn raw(self) -> u16 {
self.0
}
pub const fn supports_math_objects(self) -> bool {
self.0 >= Self::MATH_OBJECTS.0
}
pub const fn supports_table_and_code_block_objects(self) -> bool {
self.0 >= Self::TABLE_AND_CODE_BLOCK_OBJECTS.0
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Page {
pub uuid: String,
pub width: u32,
pub height: u32,
pub content_bbox: BoundingBox,
pub background_color: Option<Color>,
pub template: Option<PageTemplate>,
#[cfg_attr(feature = "serde", serde(default))]
pub background: PageBackground,
pub strokes: Vec<Stroke>,
pub elements: Vec<PageElement>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MediaAsset {
pub name: String,
pub archive_id: Option<u32>,
pub mime_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum PageElement {
Image {
bbox: BoundingBox,
media_index: usize,
},
PlacedImage(PlacedImage),
TextBox(RichTextBox),
Shape(crate::NativeShape),
Line(crate::NativeLine),
}
impl PageElement {
pub const fn object_type(&self) -> ObjectType {
match self {
Self::Image { .. } | Self::PlacedImage(_) => ObjectType::Image,
Self::TextBox(_) => ObjectType::TextBox,
Self::Shape(_) => ObjectType::Shape,
Self::Line(_) => ObjectType::Line,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct PlacedImage {
pub bbox: BoundingBox,
pub rotation_degrees: Option<f64>,
pub media_id: Option<u32>,
pub media_index: Option<usize>,
pub crop_rect: Option<[i32; 4]>,
pub original_bbox: Option<BoundingBox>,
pub border_media_id: Option<u32>,
pub original_media_id: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ObjectType {
None,
Stroke,
TextBox,
Image,
Container,
Shape,
Line,
DeprecatedDummyStroke,
Voice,
Formula,
DeprecatedTable,
Web,
Painting,
StrokeDevelopmentVersion,
Video,
Link,
StrokeBrush,
Unknown,
Plot,
Math,
Table,
CodeBlock,
AttachedFile,
StrokeGroup,
Other(u32),
}
impl ObjectType {
pub const fn raw(self) -> u32 {
match self {
Self::None => 0,
Self::Stroke => 1,
Self::TextBox => 2,
Self::Image => 3,
Self::Container => 4,
Self::Shape => 7,
Self::Line => 8,
Self::DeprecatedDummyStroke => 9,
Self::Voice => 10,
Self::Formula => 11,
Self::DeprecatedTable => 12,
Self::Web => 13,
Self::Painting => 14,
Self::StrokeDevelopmentVersion => 15,
Self::Video => 16,
Self::Link => 17,
Self::StrokeBrush => 18,
Self::Unknown => 19,
Self::Plot => 20,
Self::Math => 21,
Self::Table => 22,
Self::CodeBlock => 23,
Self::AttachedFile => 24,
Self::StrokeGroup => 100,
Self::Other(raw) => raw,
}
}
pub const fn is_supported_by(self, version: FormatVersion) -> bool {
match self {
Self::Math => version.supports_math_objects(),
Self::Table | Self::CodeBlock => version.supports_table_and_code_block_objects(),
_ => true,
}
}
}
impl From<u32> for ObjectType {
fn from(raw: u32) -> Self {
match raw {
0 => Self::None,
1 => Self::Stroke,
2 => Self::TextBox,
3 => Self::Image,
4 => Self::Container,
7 => Self::Shape,
8 => Self::Line,
9 => Self::DeprecatedDummyStroke,
10 => Self::Voice,
11 => Self::Formula,
12 => Self::DeprecatedTable,
13 => Self::Web,
14 => Self::Painting,
15 => Self::StrokeDevelopmentVersion,
16 => Self::Video,
17 => Self::Link,
18 => Self::StrokeBrush,
19 => Self::Unknown,
20 => Self::Plot,
21 => Self::Math,
22 => Self::Table,
23 => Self::CodeBlock,
24 => Self::AttachedFile,
100 => Self::StrokeGroup,
raw => Self::Other(raw),
}
}
}
impl From<ObjectType> for u32 {
fn from(object_type: ObjectType) -> Self {
object_type.raw()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum TextAreaType {
Margin,
Free,
Path,
Other(u8),
}
impl From<u8> for TextAreaType {
fn from(raw: u8) -> Self {
match raw {
0 => Self::Margin,
1 => Self::Free,
2 => Self::Path,
raw => Self::Other(raw),
}
}
}
impl TextAreaType {
pub const fn raw(self) -> u8 {
match self {
Self::Margin => 0,
Self::Free => 1,
Self::Path => 2,
Self::Other(raw) => raw,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextBox {
pub text_area_type: Option<TextAreaType>,
pub bbox: BoundingBox,
pub rotation_degrees: Option<f64>,
pub text: String,
pub color: Option<Color>,
pub highlight_color: Option<Color>,
pub underline: bool,
pub font_size: Option<f32>,
pub runs: Vec<RichTextRun>,
pub spans: Vec<RichTextSpan>,
pub paragraphs: Vec<RichTextParagraph>,
pub object_spans: Vec<RichTextObjectSpan>,
pub text_sections: Vec<RichTextSection>,
pub margins: Option<[f32; 4]>,
pub gravity: Option<u8>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextObjectSpan {
pub object_type: ObjectType,
pub object_data: Vec<u8>,
pub content: Option<RichTextObjectContent>,
pub text_index_utf16: i32,
pub layout_option: ObjectSpanLayoutOption,
pub layout_constraint: ObjectSpanLayoutConstraint,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum RichTextObjectContent {
Image(Box<PlacedImage>),
Table(Box<RichTextTable>),
CodeBlock(Box<RichTextCodeBlock>),
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextTable {
pub style: crate::TableStyle,
pub bbox: BoundingBox,
pub rotation_degrees: Option<f64>,
pub column_widths: Vec<f32>,
pub rows: Vec<RichTextTableRow>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextTableRow {
pub max_height: Option<f32>,
pub min_height: Option<f32>,
pub metadata: crate::TableRecordMetadata,
pub index: u32,
pub height: f32,
pub cells: Vec<RichTextTableCell>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextTableCell {
pub border: Option<crate::TableBorder>,
pub metadata: crate::TableRecordMetadata,
pub column_index: u32,
pub row_span: u32,
pub column_span: u32,
pub background_color: u32,
pub has_own_background_color: bool,
pub bbox: BoundingBox,
pub vertical_alignment: u8,
pub content: RichTextBox,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextCodeBlock {
pub bbox: BoundingBox,
pub rotation_degrees: Option<f64>,
pub title: Option<RichTextBox>,
pub body: Option<RichTextBox>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ObjectSpanLayoutOption {
Block,
Inline,
BlockWithSmallMargin,
BlockWithMediumMargin,
Other(u32),
}
impl From<u32> for ObjectSpanLayoutOption {
fn from(raw: u32) -> Self {
match raw {
0 => Self::Block,
1 => Self::Inline,
2 => Self::BlockWithSmallMargin,
3 => Self::BlockWithMediumMargin,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ObjectSpanLayoutConstraint {
Normal,
OverPagesOverlapPadding,
OverPages,
Other(u32),
}
impl From<u32> for ObjectSpanLayoutConstraint {
fn from(raw: u32) -> Self {
match raw {
0 => Self::Normal,
1 => Self::OverPagesOverlapPadding,
2 => Self::OverPages,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextSection {
pub start_utf16: i32,
pub length_utf16: i32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextRun {
pub start: usize,
pub end: usize,
pub bold: bool,
pub italic: bool,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextSpan {
pub kind: RichTextSpanType,
pub start_utf16: u32,
pub end_utf16: u32,
pub expand: bool,
pub payload: Vec<u8>,
}
impl RichTextSpan {
pub fn boolean_value(&self) -> Option<bool> {
self.payload
.get(..2)
.map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]) == 1)
}
pub fn color_value(&self) -> Option<Color> {
let bytes = self.payload.get(..4)?;
Some(Color {
r: bytes[2],
g: bytes[1],
b: bytes[0],
})
}
pub fn font_size_value(&self) -> Option<f32> {
let bytes = self.payload.get(..4)?.try_into().ok()?;
Some(f32::from_le_bytes(bytes))
}
pub fn hyperlink_value(&self) -> Option<RichTextHyperlink> {
if self.kind != RichTextSpanType::Hyperlink {
return None;
}
let kind = HyperlinkType::from(payload_u32(&self.payload, 0)?);
let date_time_type = payload_u32(&self.payload, 4)?;
let length = usize::try_from(payload_u32(&self.payload, 8)?).ok()?;
let byte_length = length.checked_mul(2)?;
let bytes = self.payload.get(12..12_usize.checked_add(byte_length)?)?;
let custom_data = (!bytes.is_empty())
.then(|| {
bytes
.as_chunks::<2>()
.0
.iter()
.map(|unit| u16::from_le_bytes([unit[0], unit[1]]))
.collect::<Vec<_>>()
})
.map(|units| String::from_utf16(&units))
.transpose()
.ok()?;
Some(RichTextHyperlink {
kind,
date_time_type,
custom_data,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextHyperlink {
pub kind: HyperlinkType,
pub date_time_type: u32,
pub custom_data: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum HyperlinkType {
Unknown,
Email,
Telephone,
Url,
Date,
Address,
DateTime,
Formula,
File,
Custom,
Other(u32),
}
impl From<u32> for HyperlinkType {
fn from(raw: u32) -> Self {
match raw {
0 => Self::Unknown,
1 => Self::Email,
2 => Self::Telephone,
3 => Self::Url,
4 => Self::Date,
5 => Self::Address,
6 => Self::DateTime,
7 => Self::Formula,
8 => Self::File,
9 => Self::Custom,
raw => Self::Other(raw),
}
}
}
fn payload_u32(payload: &[u8], offset: usize) -> Option<u32> {
Some(u32::from_le_bytes(
payload
.get(offset..offset.checked_add(4)?)?
.try_into()
.ok()?,
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum RichTextSpanType {
None,
ForegroundColor,
FontSize,
FontName,
Bold,
Italic,
Underline,
Hyperlink,
ComposingBackgroundColor,
Composing,
BackgroundColor,
ComposingTag,
Timestamp,
Strikethrough,
Suggestion,
SpellCorrection,
Formula,
Other(u32),
}
impl From<u32> for RichTextSpanType {
fn from(raw: u32) -> Self {
match raw {
0 => Self::None,
1 => Self::ForegroundColor,
3 => Self::FontSize,
4 => Self::FontName,
5 => Self::Bold,
6 => Self::Italic,
7 => Self::Underline,
9 => Self::Hyperlink,
15 => Self::ComposingBackgroundColor,
16 => Self::Composing,
17 => Self::BackgroundColor,
18 => Self::ComposingTag,
19 => Self::Timestamp,
20 => Self::Strikethrough,
21 => Self::Suggestion,
22 => Self::SpellCorrection,
23 => Self::Formula,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RichTextParagraph {
pub kind: RichTextParagraphType,
pub start_paragraph: u32,
pub end_paragraph: u32,
pub payload: Vec<u8>,
}
impl RichTextParagraph {
pub fn alignment(&self) -> Option<ParagraphAlignment> {
if self.kind != RichTextParagraphType::Alignment {
return None;
}
Some(ParagraphAlignment::from(self.payload_u32(0)?))
}
pub fn indent(&self) -> Option<ParagraphIndent> {
if self.kind != RichTextParagraphType::IndentLevel {
return None;
}
Some(ParagraphIndent {
level: self.payload_u32(0)?,
direction: ParagraphDirection::from(self.payload_u32(4)?),
})
}
pub fn line_spacing(&self) -> Option<ParagraphLineSpacing> {
if self.kind != RichTextParagraphType::LineSpacing {
return None;
}
Some(ParagraphLineSpacing {
kind: LineSpacingType::from(self.payload_u32(0)?),
value: self.payload_f32(4)?,
})
}
pub fn bullet(&self) -> Option<ParagraphBullet> {
if self.kind != RichTextParagraphType::Bullet {
return None;
}
Some(ParagraphBullet {
kind: BulletType::from(self.payload_u32(0)?),
number: self.payload_u32(4)?,
checked: self.payload_u32(8)? != 0,
initial_number: self.payload_u32(12)?,
})
}
pub fn spacing(&self) -> Option<f32> {
matches!(
self.kind,
RichTextParagraphType::SpacingBefore | RichTextParagraphType::SpacingAfter
)
.then(|| self.payload_f32(0))?
}
pub fn predefined_style(&self) -> Option<ParagraphPredefinedStyle> {
if self.kind != RichTextParagraphType::PredefinedStyle {
return None;
}
Some(ParagraphPredefinedStyle {
style: PredefinedTextStyle::from(self.payload_u32(0)?),
following_style: PredefinedTextStyle::from(self.payload_u32(4)?),
})
}
pub fn is_parsed(&self) -> Option<bool> {
(self.kind == RichTextParagraphType::ParsingState)
.then(|| self.payload_u32(0).map(|value| value != 0))?
}
fn payload_u32(&self, offset: usize) -> Option<u32> {
let bytes = self.payload.get(offset..offset.checked_add(4)?)?;
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
fn payload_f32(&self, offset: usize) -> Option<f32> {
let bytes = self.payload.get(offset..offset.checked_add(4)?)?;
Some(f32::from_le_bytes(bytes.try_into().ok()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum RichTextParagraphType {
None,
Legacy,
IndentLevel,
Alignment,
LineSpacing,
Bullet,
ParsingState,
SpacingBefore,
SpacingAfter,
PredefinedStyle,
Other(u32),
}
impl From<u32> for RichTextParagraphType {
fn from(raw: u32) -> Self {
match raw {
0 => Self::None,
1 => Self::Legacy,
2 => Self::IndentLevel,
3 => Self::Alignment,
4 => Self::LineSpacing,
5 => Self::Bullet,
6 => Self::ParsingState,
8 => Self::SpacingBefore,
9 => Self::SpacingAfter,
10 => Self::PredefinedStyle,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ParagraphAlignment {
Left,
Right,
Center,
Both,
Other(u32),
}
impl From<u32> for ParagraphAlignment {
fn from(raw: u32) -> Self {
match raw {
0 => Self::Left,
1 => Self::Right,
2 => Self::Center,
3 => Self::Both,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ParagraphDirection {
None,
LeftToRight,
RightToLeft,
Other(u32),
}
impl From<u32> for ParagraphDirection {
fn from(raw: u32) -> Self {
match raw {
0 => Self::None,
1 => Self::LeftToRight,
2 => Self::RightToLeft,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParagraphIndent {
pub level: u32,
pub direction: ParagraphDirection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum LineSpacingType {
Pixels,
Percent,
Other(u32),
}
impl From<u32> for LineSpacingType {
fn from(raw: u32) -> Self {
match raw {
0 => Self::Pixels,
1 => Self::Percent,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParagraphLineSpacing {
pub kind: LineSpacingType,
pub value: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum BulletType {
None,
Arrow,
Checker,
Diamond,
Digit,
CircledDigit,
Alphabet,
RomanNumeral,
SolidCircle,
WhiteCircle,
UppercaseAlphabet,
BlackSquare,
WhiteSquare,
Other(u32),
}
impl From<u32> for BulletType {
fn from(raw: u32) -> Self {
match raw {
0 => Self::None,
1 => Self::Arrow,
2 => Self::Checker,
3 => Self::Diamond,
4 => Self::Digit,
5 => Self::CircledDigit,
6 => Self::Alphabet,
7 => Self::RomanNumeral,
8 => Self::SolidCircle,
9 => Self::WhiteCircle,
10 => Self::UppercaseAlphabet,
11 => Self::BlackSquare,
12 => Self::WhiteSquare,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParagraphBullet {
pub kind: BulletType,
pub number: u32,
pub checked: bool,
pub initial_number: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum PredefinedTextStyle {
Heading1,
Heading2,
Heading3,
Body1,
Other(u32),
}
impl From<u32> for PredefinedTextStyle {
fn from(raw: u32) -> Self {
match raw {
0 => Self::Heading1,
1 => Self::Heading2,
2 => Self::Heading3,
3 => Self::Body1,
raw => Self::Other(raw),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParagraphPredefinedStyle {
pub style: PredefinedTextStyle,
pub following_style: PredefinedTextStyle,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct PageBackground {
pub template_uri: Option<String>,
pub image_id: Option<u32>,
pub image_mode: Option<u32>,
pub width: Option<u32>,
pub rotation: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PageTemplate {
pub id: u32,
pub source: PageTemplateSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum PageTemplateSource {
BuiltIn,
CustomPdf {
page_index: u32,
},
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Stroke {
#[cfg_attr(feature = "serde", serde(default))]
pub rendering: Option<crate::StrokeRendering>,
pub bbox: BoundingBox,
pub points: Vec<Point>,
pub pressures: Vec<f64>,
pub timestamps: Vec<i64>,
pub tilts: Vec<f64>,
pub orientations: Vec<f64>,
pub color: Option<Color>,
pub pen_width: f32,
}
impl Stroke {
pub const fn object_type(&self) -> ObjectType {
ObjectType::Stroke
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BoundingBox {
pub x_min: f64,
pub y_min: f64,
pub x_max: f64,
pub y_max: f64,
}
impl Default for BoundingBox {
fn default() -> Self {
Self {
x_min: 0.0,
y_min: 0.0,
x_max: 0.0,
y_max: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::{
BulletType, FormatVersion, HyperlinkType, LineSpacingType, ObjectType, ParagraphAlignment,
ParagraphDirection, PredefinedTextStyle, RichTextParagraph, RichTextParagraphType,
RichTextSpan, RichTextSpanType,
};
fn paragraph(kind: RichTextParagraphType, values: &[u32]) -> RichTextParagraph {
RichTextParagraph {
kind,
start_paragraph: 0,
end_paragraph: 1,
payload: values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect(),
}
}
#[test]
fn object_type_ids_round_trip_without_losing_future_values() {
for raw in 0..=100 {
let object_type = ObjectType::from(raw);
assert_eq!(object_type.raw(), raw);
}
assert_eq!(ObjectType::from(19), ObjectType::Unknown);
assert_eq!(ObjectType::from(25), ObjectType::Other(25));
}
#[test]
fn version_gates_match_current_sdk_contract() {
assert!(!ObjectType::Math.is_supported_by(FormatVersion(5199)));
assert!(ObjectType::Math.is_supported_by(FormatVersion::MATH_OBJECTS));
assert!(!ObjectType::Table.is_supported_by(FormatVersion(5399)));
assert!(ObjectType::Table.is_supported_by(FormatVersion::TABLE_AND_CODE_BLOCK_OBJECTS));
assert!(ObjectType::Stroke.is_supported_by(FormatVersion::INITIAL));
}
#[test]
fn decodes_apk_hyperlink_payload() {
let target = "https://example.com/markdown-test";
let mut payload = Vec::new();
payload.extend_from_slice(&3_u32.to_le_bytes());
payload.extend_from_slice(&0_u32.to_le_bytes());
payload.extend_from_slice(&(target.encode_utf16().count() as u32).to_le_bytes());
payload.extend(target.encode_utf16().flat_map(|unit| unit.to_le_bytes()));
let span = RichTextSpan {
kind: RichTextSpanType::Hyperlink,
start_utf16: 0,
end_utf16: 12,
expand: false,
payload,
};
let hyperlink = span.hyperlink_value().unwrap();
assert_eq!(hyperlink.kind, HyperlinkType::Url);
assert_eq!(hyperlink.date_time_type, 0);
assert_eq!(hyperlink.custom_data.as_deref(), Some(target));
}
#[test]
fn decodes_apk_paragraph_payloads() {
assert_eq!(
paragraph(RichTextParagraphType::Alignment, &[2]).alignment(),
Some(ParagraphAlignment::Center)
);
let indent = paragraph(RichTextParagraphType::IndentLevel, &[3, 2])
.indent()
.unwrap();
assert_eq!(indent.level, 3);
assert_eq!(indent.direction, ParagraphDirection::RightToLeft);
let line_spacing = paragraph(RichTextParagraphType::LineSpacing, &[1, 1.6_f32.to_bits()])
.line_spacing()
.unwrap();
assert_eq!(line_spacing.kind, LineSpacingType::Percent);
assert_eq!(line_spacing.value, 1.6);
let bullet = paragraph(RichTextParagraphType::Bullet, &[4, 2, 1, 1])
.bullet()
.unwrap();
assert_eq!(bullet.kind, BulletType::Digit);
assert_eq!(bullet.number, 2);
assert!(bullet.checked);
assert_eq!(bullet.initial_number, 1);
assert_eq!(
paragraph(RichTextParagraphType::SpacingBefore, &[20.0_f32.to_bits()]).spacing(),
Some(20.0)
);
let style = paragraph(RichTextParagraphType::PredefinedStyle, &[0, 3])
.predefined_style()
.unwrap();
assert_eq!(style.style, PredefinedTextStyle::Heading1);
assert_eq!(style.following_style, PredefinedTextStyle::Body1);
assert_eq!(
paragraph(RichTextParagraphType::ParsingState, &[1]).is_parsed(),
Some(true)
);
}
}