Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add video/multimedia support #31

Merged
merged 15 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)/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
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().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(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.permissions().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 {
awehrfritz marked this conversation as resolved.
Show resolved Hide resolved
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
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.
reknih marked this conversation as resolved.
Show resolved Hide resolved
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
8 changes: 7 additions & 1 deletion 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 All @@ -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,
Expand All @@ -131,6 +135,7 @@ pub mod writers {
ExponentialFunction, PostScriptFunction, SampledFunction, StitchingFunction,
};
pub use object::{NameTree, NameTreeEntries, NumberTree, NumberTreeEntries};
pub use renditions::{Rendition, MediaClip, MediaPlayParams, MediaPermissions};
pub use structure::{
Catalog, ClassMap, Destination, DeveloperExtension, DocumentInfo, MarkInfo,
MarkedRef, Metadata, Names, ObjectRef, Outline, OutlineItem, Page, PageLabel,
Expand Down Expand Up @@ -166,6 +171,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
Loading