diff --git a/examples/bear-1280x720.jpg b/examples/bear-1280x720.jpg new file mode 100644 index 0000000..f6dbc70 Binary files /dev/null and b/examples/bear-1280x720.jpg differ diff --git a/examples/bear-1280x720.mp4 b/examples/bear-1280x720.mp4 new file mode 100644 index 0000000..b424a0f Binary files /dev/null and b/examples/bear-1280x720.mp4 differ diff --git a/examples/video.rs b/examples/video.rs new file mode 100644 index 0000000..2f7f63c --- /dev/null +++ b/examples/video.rs @@ -0,0 +1,168 @@ +//! This example demonstrates how to link/embed videos. + +use image::ColorType; +use pdf_writer::types::{ + ActionType, AnnotationType, MediaClipType, RenditionOperation, RenditionType, + TempFileType, +}; +use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect, Ref, Str, TextStr}; + +fn get_bbox(page: &Rect, mut w: f32, mut h: f32) -> Rect { + // Limit the width and height of the object to the page size, retaining the + // aspect ratio. + if w > (page.x2 - page.x1) { + let f = (page.x2 - page.x1) / w; + w *= f; + h *= f; + } + if h > (page.y2 - page.y1) { + let f = (page.y2 - page.y1) / h; + w *= f; + h *= f; + } + + // Return a bounding box for the object centered on the page. + Rect::new( + (page.x2 - w) / 2.0, + (page.y2 - h) / 2.0, + (page.x2 + w) / 2.0, + (page.y2 + h) / 2.0, + ) +} + +fn main() -> std::io::Result<()> { + let embedded = true; + + // Start writing. + let mut pdf = Pdf::new(); + + // Define some indirect reference ids we'll use. + let catalog_id = Ref::new(1); + let page_tree_id = Ref::new(2); + let page_id = Ref::new(3); + let annotation_id = Ref::new(4); + let video_file_id = Ref::new(5); + let form_xobject_id = Ref::new(6); + let image_id = Ref::new(7); + let image_name = Name(b"Im1"); + + // Set up the page tree. For more details see `hello.rs`. + pdf.catalog(catalog_id).pages(page_tree_id); + pdf.pages(page_tree_id).kids([page_id]).count(1); + + // Specify one A4 landscape page. + let mut page = pdf.page(page_id); + let a4_landscape = Rect::new(0.0, 0.0, 842.0, 595.0); + page.media_box(a4_landscape); + page.parent(page_tree_id); + page.annotations([annotation_id]); + page.finish(); + + // Decode the image. + // Image extracte from video file using ffmpeg: + // ffmpeg -i bear-1280x720.mp4 -vf "select=eq(n\,0)" -q:v 3 bear-1280x720.jpg + let data = std::fs::read("examples/bear-1280x720.jpg").unwrap(); + let dynamic = image::load_from_memory(&data).unwrap(); + assert!(dynamic.color() == ColorType::Rgb8); + + // Write the stream for the image we want to embed. + let mut image = pdf.image_xobject(image_id, &data); + image.filter(Filter::DctDecode); + image.width(dynamic.width() as i32); + image.height(dynamic.height() as i32); + image.color_space().device_rgb(); + image.bits_per_component(8); + image.finish(); + + // Get a centered and fitted bounding box for the screen annotation and image. + let bbox = get_bbox(&a4_landscape, dynamic.width() as f32, dynamic.height() as f32); + + // Place and size the image in a content stream. + // + // By default, PDF XObjects always have a size of 1x1 user units (and 1 user + // unit is one 1pt if you don't change that). To position and size them, you + // have to change the current transformation matrix, which is structured as + // [scale_x, skew_x, skew_y, scale_y, translate_x, translate_y]. Also, + // remember that the PDF coordinate system starts at the bottom left! When + // you have other elements after the image, it's also important to save & + // restore the state so that they are not affected by the transformation. + let mut content = Content::new(); + content.save_state(); + content.transform([ + (bbox.x2 - bbox.x1), + 0.0, + 0.0, + (bbox.y2 - bbox.y1), + bbox.x1, + bbox.y1, + ]); + content.x_object(image_name); + content.restore_state(); + let content_data = content.finish(); + + // Create a form XObject with the image for the appearance stream in the + // screen annotation. + let mut form_xobject = pdf.form_xobject(form_xobject_id, &content_data); + form_xobject.bbox(bbox); + form_xobject.resources().x_objects().pair(image_name, image_id); + form_xobject.finish(); + + // Video file + // Downloaded from the Chromium sources at: + // https://github.com/chromium/chromium/blob/main/media/test/data/bear-1280x720.mp4 + // Get the absolute path and file name. + let file_path = std::fs::canonicalize("examples/bear-1280x720.mp4").unwrap(); + let file_name = file_path.file_name().unwrap(); + + if embedded { + // Read video file and add to pdf as embedded file. + let data = std::fs::read(&file_path).unwrap(); + pdf.embedded_file(video_file_id, &data); + } + + // Create a screen annotation and set the appearance stream. + let mut annotation = pdf.annotation(annotation_id); + annotation.subtype(AnnotationType::Screen); + annotation.rect(bbox); + annotation.page(page_id); + annotation.appearance().normal().stream(form_xobject_id); + + // Write a rendition action for the screen annotation. + let mut action = annotation.action(); + action.action_type(ActionType::Rendition); + action.operation(RenditionOperation::Play); + action.annotation(annotation_id); + + // Write a media rendition for the action. + let mut rendition = action.rendition(); + rendition.subtype(RenditionType::Media); + + // Write the media clip data for the media rendition. + let mut media_clip = rendition.media_clip(); + media_clip.subtype(MediaClipType::Data); + if embedded { + media_clip + .data() + .path(Str(file_name.as_encoded_bytes())) + .embedded_file(video_file_id); + } else { + // FIXME: Is there a more elegant way to assemble the URL? + let file_url = &[b"file://", file_path.as_os_str().as_encoded_bytes()].concat(); + media_clip.data().file_system(Name(b"URL")).path(Str(file_url)); + } + media_clip.data_type(Str(b"video/mp4")); + media_clip.permissions().temp_file(TempFileType::Access); + media_clip.alt_texts([TextStr(""), TextStr("default text")]); + media_clip.finish(); + + // Add controls for the media player. + rendition.media_play_params().controls(true); + + // Finish off a few things. + rendition.finish(); + action.finish(); + annotation.finish(); + + // Write the thing to a file. + std::fs::write("target/video.pdf", pdf.finish()) +} diff --git a/src/actions.rs b/src/actions.rs index 74be322..14ca559 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -66,7 +66,7 @@ impl<'a> Action<'a> { } /// Write the `/JS` attribute to set the script of this action as a text - /// string. Only permissible for JavaScript actions. + /// string. Only permissible for JavaScript and Rendition actions. pub fn js_string(&mut self, script: TextStr) -> &mut Self { self.pair(Name(b"JS"), script); self @@ -75,7 +75,8 @@ impl<'a> Action<'a> { /// Write the `/JS` attribute to set the script of this action as a text /// stream. The indirect reference shall point to a stream containing valid /// ECMAScript. The stream must have `PdfDocEncoding` or be in Unicode, - /// starting with `U+FEFF`. Only permissible for JavaScript actions. + /// starting with `U+FEFF`. Only permissible for JavaScript and Rendition + /// actions. pub fn js_stream(&mut self, script: Ref) -> &mut Self { self.pair(Name(b"JS"), script); self @@ -94,10 +95,49 @@ impl<'a> Action<'a> { self.pair(Name(b"Flags"), flags.bits() as i32); self } + + /// Write the `/OP` attribute to set the operation to perform when the + /// action is triggered. + pub fn operation(&mut self, op: RenditionOperation) -> &mut Self { + self.pair(Name(b"OP"), op as i32); + self + } + + /// Write the `/AN` attribute to provide a reference to the screen + /// annotation for the operation. Required if OP is present. + pub fn annotation(&mut self, id: Ref) -> &mut Self { + self.pair(Name(b"AN"), id); + self + } + + /// Start writing the `/R` dictionary. Only permissible for the subtype + /// `Rendition`. + pub fn rendition(&mut self) -> Rendition<'_> { + self.insert(Name(b"R")).start() + } } deref!('a, Action<'a> => Dict<'a>, dict); +/// The operation to perform when a rendition action is triggered. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum RenditionOperation { + /// Play the rendition specified by /R, and associating it with the + /// annotation. If a rendition is already associated with the annotation, it + /// shall be stopped, and the new rendition shall be associated with the + /// annotation. + Play = 0, + /// Stop any rendition being played in association with the annotation. + Stop = 1, + /// Pause any rendition being played in association with the annotation. + Pause = 2, + /// Resume any rendition being played in association with the annotation. + Resume = 3, + /// Play the rendition specified by /R, and associating it with the + /// annotation, or resume if a rendition is already associated. + PlayOrResume = 4, +} + /// Writer for a _fields array_. /// /// This struct is created by [`Action::fields`]. @@ -146,6 +186,8 @@ pub enum ActionType { /// [JavaScript for Acrobat API Reference](https://opensource.adobe.com/dc-acrobat-sdk-docs/acrobatsdk/pdfs/acrobatsdk_jsapiref.pdf) /// and ISO 21757. JavaScript, + /// A rendition action to control the playing of multimedia content. PDF 1.5+. + Rendition, } impl ActionType { @@ -159,6 +201,7 @@ impl ActionType { Self::ResetForm => Name(b"ResetForm"), Self::ImportData => Name(b"ImportData"), Self::JavaScript => Name(b"JavaScript"), + Self::Rendition => Name(b"Rendition"), } } } diff --git a/src/annotations.rs b/src/annotations.rs index 22f3d0c..98e6dd3 100644 --- a/src/annotations.rs +++ b/src/annotations.rs @@ -35,6 +35,14 @@ impl<'a> Annotation<'a> { self } + /// Write the `/P` attribute. This provides an indirect reference to the + /// page object with which this annotation is associated. Required for the + /// subtype `Screen` associated with rendition actions. PDF 1.3+. + pub fn page(&mut self, id: Ref) -> &mut Self { + self.pair(Name(b"P"), id); + self + } + /// Write the `/NM` attribute. This uniquely identifies the annotation on the /// page. PDF 1.3+. pub fn name(&mut self, text: TextStr) -> &mut Self { @@ -55,6 +63,21 @@ impl<'a> Annotation<'a> { self } + /// Start writing the `/AP` dictionary to set how the annotation shall + /// be presented visually. If this dictionary contains sub dictionaries, + /// [`Self::appearance_state`] must be set. PDF 1.2+. + pub fn appearance(&mut self) -> Appearance<'_> { + self.insert(Name(b"AP")).start() + } + + /// Write the `/AS` attribute to set the annotation's current appearance + /// state from the `/AP` subdictionary. Must be set if [`Self::appearance`] + /// has one or more subdictionaries. PDF 1.2+. + pub fn appearance_state(&mut self, name: Name) -> &mut Self { + self.pair(Name(b"AS"), name); + self + } + /// Write the `/Border` attribute. This describes the look of the border /// around the annotation, including width and horizontal and vertical /// border radii. The function may also receive a dash pattern which @@ -226,6 +249,8 @@ pub enum AnnotationType { FileAttachment, /// A widget annotation. PDF 1.2+. Widget, + /// A screen annotation. PDF 1.5+. + Screen, } impl AnnotationType { @@ -242,6 +267,7 @@ impl AnnotationType { Self::StrikeOut => Name(b"StrikeOut"), Self::FileAttachment => Name(b"FileAttachment"), Self::Widget => Name(b"Widget"), + Self::Screen => Name(b"Screen"), } } } @@ -600,6 +626,63 @@ impl HighlightEffect { } } +/// Writer for an _appearance dictionary_. +/// +/// This struct is created by [`Annotation::appearance`]. +pub struct Appearance<'a> { + dict: Dict<'a>, +} + +writer!(Appearance: |obj| Self { dict: obj.dict() }); + +impl<'a> Appearance<'a> { + /// Start writing the `/N` stream or dictionary to set the annotation's + /// normal appearance. + pub fn normal(&mut self) -> AppearanceEntry<'_> { + self.insert(Name(b"N")).start() + } + + /// Start writing the `/R` stream or dictionary to set the annotation's + /// rollover (hover) appearance. + pub fn rollover(&mut self) -> AppearanceEntry<'_> { + self.insert(Name(b"R")).start() + } + + /// Start writing the `/D` stream or dictionary to set the annotation's + /// alternate (down) appearance. + pub fn alternate(&mut self) -> AppearanceEntry<'_> { + self.insert(Name(b"D")).start() + } +} + +deref!('a, Appearance<'a> => Dict<'a>, dict); + +/// Writer for an _appearance stream_ or an _appearance subdictionary_. +/// +/// This struct is created by [`Appearance::normal`], [`Appearance::rollover`] +/// and [`Appearance::alternate`]. +pub struct AppearanceEntry<'a> { + obj: Obj<'a>, +} + +writer!(AppearanceEntry: |obj| Self { obj }); + +impl<'a> AppearanceEntry<'a> { + /// Write an indirect reference to a [`FormXObject`] containing the + /// appearance stream. + pub fn stream(self, id: Ref) { + self.obj.primitive(id); + } + + /// Start writing an appearance subdictionary containing indirect references + /// to [`FormXObject`]s for each appearance state. + pub fn streams(self) -> TypedDict<'a, Ref> { + self.obj.dict().typed() + } +} + +deref!('a, AppearanceEntry<'a> => Obj<'a>, obj); + /// Writer for an _border style dictionary_. /// /// This struct is created by [`Annotation::border_style`]. diff --git a/src/files.rs b/src/files.rs index fbeb85b..66ada08 100644 --- a/src/files.rs +++ b/src/files.rs @@ -3,7 +3,7 @@ use super::*; /// Writer for a _file specification dictionary_. /// /// This struct is created by [`Annotation::file_spec`], -/// [`Reference::file_spec`], and [`Action::file_spec`]. +/// [`Reference::file_spec`], [`Rendition::data`], and [`Action::file_spec`]. pub struct FileSpec<'a> { dict: Dict<'a>, } diff --git a/src/lib.rs b/src/lib.rs index a95f6bb..d235408 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,7 @@ mod font; mod forms; mod functions; mod object; +mod renditions; mod renumber; mod structure; mod transitions; @@ -107,7 +108,10 @@ mod xobject; pub mod writers { use super::*; pub use actions::{Action, AdditionalActions, Fields}; - pub use annotations::{Annotation, AppearanceCharacteristics, BorderStyle, IconFit}; + pub use annotations::{ + Annotation, Appearance, AppearanceCharacteristics, AppearanceEntry, BorderStyle, + IconFit, + }; pub use attributes::{ Attributes, FieldAttributes, LayoutAttributes, ListAttributes, TableAttributes, UserProperty, @@ -131,6 +135,7 @@ pub mod writers { ExponentialFunction, PostScriptFunction, SampledFunction, StitchingFunction, }; pub use object::{NameTree, NameTreeEntries, NumberTree, NumberTreeEntries}; + pub use renditions::{MediaClip, MediaPermissions, MediaPlayParams, Rendition}; pub use structure::{ Catalog, ClassMap, Destination, DeveloperExtension, DocumentInfo, MarkInfo, MarkedRef, Metadata, Names, ObjectRef, Outline, OutlineItem, Page, PageLabel, @@ -143,7 +148,7 @@ pub mod writers { /// Types used by specific PDF structures. pub mod types { use super::*; - pub use actions::{ActionType, FormActionFlags}; + pub use actions::{ActionType, FormActionFlags, RenditionOperation}; pub use annotations::{ AnnotationFlags, AnnotationIcon, AnnotationType, BorderType, HighlightEffect, IconScale, IconScaleType, TextPosition, @@ -166,6 +171,7 @@ pub mod types { CheckBoxState, ChoiceOptions, FieldFlags, FieldType, Quadding, RadioState, }; pub use functions::{InterpolationOrder, PostScriptOp}; + pub use renditions::{MediaClipType, RenditionType, TempFileType}; pub use structure::{ Direction, NumberingStyle, OutlineItemFlags, PageLayout, PageMode, StructRole, TabOrder, TrappingStatus, diff --git a/src/renditions.rs b/src/renditions.rs new file mode 100644 index 0000000..44fa518 --- /dev/null +++ b/src/renditions.rs @@ -0,0 +1,234 @@ +use super::*; + +/// Writer for an _rendition dictionary_. +/// +/// This struct is created by [`Action::rendition`]. +pub struct Rendition<'a> { + dict: Dict<'a>, +} + +writer!(Rendition: |obj| { + let mut dict = obj.dict(); + dict.pair(Name(b"Type"), Name(b"Rendition")); + Self { dict } +}); + +impl<'a> Rendition<'a> { + /// Write the `/S` attribute to set the rendition type. + pub fn subtype(&mut self, kind: RenditionType) -> &mut Self { + self.pair(Name(b"S"), kind.to_name()); + self + } + + /// Write the `/N` attribute. Specify the name of the rendition for use in a + /// user interface and for name tree lookup by JavaScript actions. + pub fn name(&mut self, text: TextStr) -> &mut Self { + self.pair(Name(b"N"), text); + self + } + + /// Start writing the `/C`, i.e. media clip, dictionary which specifies what + /// media should be played. Only permissible for Media Renditions. + pub fn media_clip(&mut self) -> MediaClip<'_> { + self.insert(Name(b"C")).start() + } + + /// Start writing the `/P`, i.e. media play parameters, dictionary which + /// specifies how the media should be played. Only permissible for Media + /// Renditions. + pub fn media_play_params(&mut self) -> MediaPlayParams<'_> { + self.insert(Name(b"P")).start() + } +} + +deref!('a, Rendition<'a> => Dict<'a>, dict); + +/// Writer for an _media clip dictionary_. +/// +/// This struct is created by [`Rendition::media_clip`]. +/// +/// ## Note on reader compatibility +/// +/// Different PDF readers may have support for different media codecs and +/// container formats. +/// +/// For example, [Adobe's documentation][1] states that Adobe Acrobat can play +/// videos in MP4, MOV, M4V, 3GP, and 3G2 containers using the H.264 codec. +/// +/// Other readers may depend on the media libraries installed on the system. KDE +/// Okular, for example, uses the Phonon library to support a range of media +/// formats. +/// +/// Yet other viewers do not support media clips at all. At the time of writing, +/// this includes the popular Pdfium library used by Google Chrome and Microsoft +/// Edge, `pdf.js` used by Firefox, mupdf, and Quartz, the PDF viewer on Apple +/// platforms. +/// +/// [1]: https://helpx.adobe.com/acrobat/using/playing-video-audio-multimedia-formats.html#supported_video_audio_and_interactive_formats +pub struct MediaClip<'a> { + dict: Dict<'a>, +} + +writer!(MediaClip: |obj| { + let mut dict = obj.dict(); + dict.pair(Name(b"Type"), Name(b"MediaClip")); + Self { dict } +}); + +impl<'a> MediaClip<'a> { + /// Write the `/S` attribute to set the media clip type. + pub fn subtype(&mut self, kind: MediaClipType) -> &mut Self { + self.pair(Name(b"S"), kind.to_name()); + self + } + + /// Write the `/N` attribute. Specifies the name of the media clip, for use + /// in the user interface. + pub fn name(&mut self, text: TextStr) -> &mut Self { + self.pair(Name(b"N"), text); + self + } + + /// Start writing the `/D` dictionary specifying the media data. + pub fn data(&mut self) -> FileSpec<'_> { + self.insert(Name(b"D")).start() + } + + /// Write the `/CT` attribute identifying the type of data in `/D`, i.e. the + /// MIME type. + pub fn data_type(&mut self, tf: Str) -> &mut Self { + self.pair(Name(b"CT"), tf); + self + } + + /// Start writing the `/P`, i.e. media permissions, dictionary. + pub fn permissions(&mut self) -> MediaPermissions<'_> { + self.insert(Name(b"P")).start() + } + + /// Write the `/Alt` attribute, listing alternate text descriptions which + /// are specified as a multi-language text array. A multi-language text + /// array shall contain pairs of strings. + pub fn alt_texts<'b>( + &mut self, + texts: impl IntoIterator>, + ) -> &mut Self { + self.insert(Name(b"Alt")).array().items(texts); + self + } +} + +deref!('a, MediaClip<'a> => Dict<'a>, dict); + +/// Writer for an _media play parameters dictionary_. +/// +/// This struct is created by [`Rendition::media_play_params`]. +pub struct MediaPlayParams<'a> { + dict: Dict<'a>, +} + +writer!(MediaPlayParams: |obj| { + let mut dict = obj.dict(); + dict.pair(Name(b"Type"), Name(b"MediaPlayParams")); + Self { dict } +}); + +impl<'a> MediaPlayParams<'a> { + /// Write the `/C` attribute inside a `/BE` dictionary specifying whether to + /// display a player-specific controls. + /// + /// This avoids implementing the "must honour" (MH) or "best effort" (BE) + /// dictionaries for MediaPlayParams, as the required boiler-plate code + /// would be high, and its usefulness low. + pub fn controls(&mut self, c: bool) -> &mut Self { + self.insert(Name(b"BE")).dict().pair(Name(b"C"), c); + self + } +} + +deref!('a, MediaPlayParams<'a> => Dict<'a>, dict); + +/// Writer for an _media permissions dictionary_. +/// +/// This struct is created by [`Rendition::permissions`]. +pub struct MediaPermissions<'a> { + dict: Dict<'a>, +} + +writer!(MediaPermissions: |obj| { + let mut dict = obj.dict(); + dict.pair(Name(b"Type"), Name(b"MediaPermissions")); + Self { dict } +}); + +impl<'a> MediaPermissions<'a> { + /// Write the `/TF` attribute to control permissions to write a temporary file. + pub fn temp_file(&mut self, tf: TempFileType) -> &mut Self { + self.pair(Name(b"TF"), tf.to_str()); + self + } +} + +deref!('a, MediaPermissions<'a> => Dict<'a>, dict); + +/// The circumstances under which it is acceptable to write a temporary file in +/// order to play a media clip. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TempFileType { + /// Never allowed. + Never, + /// Allowed only if the document permissions allow content extraction. + Extract, + /// Allowed only if the document permissions allow content extraction, + /// including for accessibility purposes. + Access, + /// Always allowed. + Always, +} + +impl TempFileType { + pub(crate) fn to_str(self) -> Str<'static> { + match self { + Self::Never => Str(b"TEMPNEVER"), + Self::Extract => Str(b"TEMPEXTRACT"), + Self::Access => Str(b"TEMPACCESS"), + Self::Always => Str(b"TEMPALWAYS"), + } + } +} + +/// Type of rendition objects. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum RenditionType { + /// Media Rendition. + Media, + /// Selector Rendition. + Selector, +} + +impl RenditionType { + pub(crate) fn to_name(self) -> Name<'static> { + match self { + Self::Media => Name(b"MR"), + Self::Selector => Name(b"SR"), + } + } +} + +/// Type of media clip objects. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum MediaClipType { + /// Media Clip Data. + Data, + /// Media Clip Section. + Section, +} + +impl MediaClipType { + pub(crate) fn to_name(self) -> Name<'static> { + match self { + Self::Data => Name(b"MCD"), + Self::Section => Name(b"MCS"), + } + } +}