Skip to content

Commit

Permalink
Add video/multimedia support (#31)
Browse files Browse the repository at this point in the history
Co-authored-by: tingerrr <[email protected]>
  • Loading branch information
awehrfritz and tingerrr authored Feb 16, 2024
1 parent d817e80 commit dd53e58
Show file tree
Hide file tree
Showing 8 changed files with 539 additions and 5 deletions.
Binary file added examples/bear-1280x720.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/bear-1280x720.mp4
Binary file not shown.
168 changes: 168 additions & 0 deletions examples/video.rs
Original file line number Diff line number Diff line change
@@ -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())
}
47 changes: 45 additions & 2 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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`].
Expand Down Expand Up @@ -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 {
Expand All @@ -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"),
}
}
}
Expand Down
83 changes: 83 additions & 0 deletions src/annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -226,6 +249,8 @@ pub enum AnnotationType {
FileAttachment,
/// A widget annotation. PDF 1.2+.
Widget,
/// A screen annotation. PDF 1.5+.
Screen,
}

impl AnnotationType {
Expand All @@ -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"),
}
}
}
Expand Down Expand Up @@ -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`].
Expand Down
2 changes: 1 addition & 1 deletion src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand Down
Loading

0 comments on commit dd53e58

Please sign in to comment.