Skip to content

Commit

Permalink
Add video/multimedia support
Browse files Browse the repository at this point in the history
  • Loading branch information
awehrfritz committed Feb 11, 2024
1 parent d817e80 commit ee99ce9
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 1 deletion.
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.
161 changes: 161 additions & 0 deletions examples/video.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! This example demonstrates how to link/embed videos.
use pdf_writer::types::{ActionType, AnnotationType, MediaClipType, RenditionType};
use pdf_writer::{Content, Finish, Pdf, Rect, Ref, Str, Name, Filter};
use image::ColorType;


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)/w;
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
let file_name = "examples/bear-1280x720.mp4";

if embedded {
// Read video file and add to pdf as embedded file.
let data = std::fs::read(file_name).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(form_xobject_id);

// Write a rendition action for the screen annotation.
let mut action = annotation.action();
action.action_type(ActionType::Rendition);
action.operation(0);
action.annotation(annotation_id);

// Write a media rendition for the action.
let mut rendition = action.rendition();
rendition.rendition_type(RenditionType::Media);

// Write the media clip data for the media rendition.
let mut media_clip = rendition.media_clip();
media_clip.media_clip_type(MediaClipType::Data);
if embedded {
media_clip.data_embedded(video_file_id);
} else {
// Get the absolute path to the video file.
let file_path = std::fs::canonicalize(file_name)?;
// 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_url(Str(file_url));
}
media_clip.data_type(Str(b"video/mp4"));
media_clip.temp_file(Str(b"TEMPACCESS"));
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())
}
23 changes: 23 additions & 0 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ 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: i32) -> &mut Self {
self.pair(Name(b"OP"), op);
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);
Expand Down Expand Up @@ -146,6 +166,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 +181,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
21 changes: 21 additions & 0 deletions src/annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ impl<'a> Annotation<'a> {
self
}

/// Write the `/P` attribute to provide a reference to the page object with
/// which this annotation is associated. Required and only permissible for
/// the subtype `Screen`. PDF 1.5+.
pub fn page(&mut self, id: Ref) -> &mut Self {
self.pair(Name(b"P"), id);
self
}

/// Write the `/AP` dictionary to provide the visual appearance for a screen
/// annotation. For now, this sets only the normal appearance as a reference
/// to a [`FormXObject`]. Only permissible for the subtype `Screen`.
/// PDF 1.5+.
pub fn appearance(&mut self, id: Ref) -> &mut Self {
self.insert(Name(b"AP")).dict()
.pair(Name(b"N"), 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 Down Expand Up @@ -226,6 +244,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 +262,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
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
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ mod forms;
mod functions;
mod object;
mod renumber;
mod renditions;
mod structure;
mod transitions;
mod xobject;
Expand Down Expand Up @@ -131,6 +132,7 @@ pub mod writers {
ExponentialFunction, PostScriptFunction, SampledFunction, StitchingFunction,
};
pub use object::{NameTree, NameTreeEntries, NumberTree, NumberTreeEntries};
pub use renditions::{Rendition, MediaClip, MediaPlayParams};
pub use structure::{
Catalog, ClassMap, Destination, DeveloperExtension, DocumentInfo, MarkInfo,
MarkedRef, Metadata, Names, ObjectRef, Outline, OutlineItem, Page, PageLabel,
Expand Down Expand Up @@ -166,6 +168,7 @@ pub mod types {
CheckBoxState, ChoiceOptions, FieldFlags, FieldType, Quadding, RadioState,
};
pub use functions::{InterpolationOrder, PostScriptOp};
pub use renditions::{RenditionType, MediaClipType};
pub use structure::{
Direction, NumberingStyle, OutlineItemFlags, PageLayout, PageMode, StructRole,
TabOrder, TrappingStatus,
Expand Down
Loading

0 comments on commit ee99ce9

Please sign in to comment.