Repository: fschutt/printpdf (language: rust)

Issue:
feature request: bookmarks
Hi, 

Thanks for the awesome library. One feature that I would really like is bookmarks in the pdf that you can put on a page. This bookmark shows up on the left of the page in firefox and is accessible on the kindle very easily with the `goto` menu. (The kindle is my use case.)

I do not know how simple this would be to implement, but it doesn't seem that hard.

Thanks so much.

for reference, here is how the feature looks in firefox (on the left there is a list of pages) 
![image](https://user-images.githubusercontent.com/58830309/101397344-90c6d180-389a-11eb-9acc-9e5896b465d5.png)

And if it helps, my use case is making a code renderer that can render source code for a project in a pdf file so it can be read on a kindle, so I would like it to be able to jump around quickly with bookmarks.

Edit:

Just saw this is in the TODOs. I could try to implement this if you could give me a little background, since I know very little about pdfs. I think there could be a function for a page called `set_bookmark` that just sets a bookmark on a page and accepts a `&str` for the name of the bookmark.


Discussion / hints:
After some investigation I found this is how you do it: 
```pdf
509 0 obj
<<
/Parent 5 0 R
/Next 524 0 R
/Title (ir\056zig\056pdf)
/A 508 0 R
/Prev 505 0 R
>>
```
It seems like this is a doubly linked list with a prev and next (I think the object numbers, I dont really know anything about pdfs, this was just examining it with a text editor.  `/A` is actually the page it is referring (marked) to.)

Hopefully this gives more insight.
Yeah, it's probably not that hard to implement, but I've not done it since it wasn't important for me. If you want to implement it, you'd probably want to start like this: https://github.com/fschutt/printpdf/blob/1b0aa7af98a0f9b082a580463156c6a548226788/src/types/pdf_layer.rs#L124-L133

I.e. `.add_bookmark(page: &PdfPageId, text: &str)`

The only difficulty is that you need to create the bookmark after the page that you're referencing has been created, plus you'd need access to the internal PDF object ID. Maybe instead of writing the bookmarks directly, just store all bookmarks as a HashMap and defer writing them until the document is being saved - because you can't really reference the PDF page until the document is actually written. Otherwise you're going to have a problem trying to reference a page whose PDF object ID hasn't yet been determined.

You need to insert a [Reference](https://docs.rs/lopdf/0.26.0/lopdf/enum.Object.html#variant.Reference) to reference a page, otherwise refer to the Adobe PDF spec, it probably has some info on creating bookmarks.
Thanks. Would the object I would want to use be `Object::Dictionary(Dictionary)` from LoPdf to implement this feature? 
 You were right, the spec did have a feature for this ![spec pic](https://share.olind.xyz/f.php?h=3vpuTta1&p=1).

Edit: 

This feature is turning out harder than I thought. The main confusion point for me is where I insert the information. It does not seem like the font setting, because the font is a `/Tf name size`, while this requires a whole new object (Dictionary) like this (listing the first, last, and how many bookmarks)
```
5 0 obj
<<
/Count 49
/First 6 0 R
/Last 1559 0 R
>>
endobj
```
plus an individual object for each bookmark
```
6 0 obj
<<
/Parent 5 0 R
/Title (astgen\056zig\056pdf)
/A 4 0 R
/Next 73 0 R
>>
endobj
```
If you could catch me up to speed on where exactly and what functions I should use to insert these "objects" that would be awesome!
Dictionaries can be built from a `Vec<(String, Object)>`, if you pass that to lopdf, it'll create a dictionary. See here:

https://github.com/fschutt/printpdf/blob/1b0aa7af98a0f9b082a580463156c6a548226788/src/types/pdf_document.rs#L327-L332

(I've also noticed that `Use0` is technically not a valid value, should be `UseNone` instead). But basically you need to add a new `("Outlines", Reference(outline_dictionary))` to that `/Catalog`, after the `PageMode`

![image](https://user-images.githubusercontent.com/12084016/101476560-4010b080-394e-11eb-8f0b-b040f4833da9.png)

![image](https://user-images.githubusercontent.com/12084016/101476328-e01a0a00-394d-11eb-968c-f8b95f3ffcf3.png)

You need to "reserve" a new object ID for the catalog with `document.inner_doc.new_object_id()`, because it has to be an indirect reference, not a direct one. If you're not sure about how the final PDF should look like, one technique I like to do is to download PDFExplorer (see README) and open an existing PDF that already has working bookmarks, that way you know where to insert them.

Fix patch:
diff --git a/examples/bookmarks.rs b/examples/bookmarks.rs
index 97ea4ca..ec0b9a6 100644
--- a/examples/bookmarks.rs
+++ b/examples/bookmarks.rs
@@ -5,18 +5,14 @@ use std::fs::File;
 use std::io::BufWriter;
 
 fn main() {
-    // To prevent empty documents, you must specify at least one page with one layer
-    // You can later on add more pages with the add_page() function
-    // You also have to specify the title of the PDF and the document creator
     let (doc, page1, _) = PdfDocument::new("printpdf page test", Mm(210.0), Mm(297.0), "Layer 1");
     doc.add_bookmark("This is a bookmark", page1);
 
-    // You can add more pages and layers to the PDF.
-    // Just make sure you don't lose the references, otherwise, you can't add things to the layer anymore
     let (page2, _) = doc.add_page(Mm(297.0), Mm(210.0), "Page 2, Layer 1");
     let _ = doc.get_page(page2).add_layer("Layer 3");
+    doc.add_bookmark("This is another bookmark", page2);
 
-    // If this is successful, you should see a PDF with two blank A4 pages
+    // If this is successful, you should see a PDF with two blank A4 pages and 2 bookmarks
     doc.save(&mut BufWriter::new(
         File::create("test_bookmark.pdf").unwrap(),
     ))
diff --git a/src/types/pdf_document.rs b/src/types/pdf_document.rs
index 9433234..a5d16c0 100644
--- a/src/types/pdf_document.rs
+++ b/src/types/pdf_document.rs
@@ -340,6 +340,7 @@ impl PdfDocumentReference {
         let pages_id = doc.inner_doc.new_object_id();
         let bookmarks_id = doc.inner_doc.new_object_id();
         let mut bookmarks_list = LoDictionary::from_iter(vec![
+            ("Type", "Outlines".into()),
             ("Count", Integer(doc.bookmarks.len() as i64)),
             /* First and Last will be filled in once they are created from the pages */
         ]);
@@ -533,14 +534,15 @@ impl PdfDocumentReference {
                 lopdf::Stream::new(lopdf::Dictionary::new(), layer_streams_merged_vec)
                     .with_compression(false);
             let page_content_id = doc.inner_doc.add_object(merged_layer_stream);
+
+            p.set("Contents", Reference(page_content_id));
+            let page_obj = doc.inner_doc.add_object(p);
             if doc.bookmarks.len() > 0 {
                 if let Some(_) = doc.bookmarks.get(&idx) {
-                    page_id_to_obj.insert(idx, page_content_id);
+                    page_id_to_obj.insert(idx, page_obj);
                 }
             }
-
-            p.set("Contents", Reference(page_content_id));
-            page_ids.push(Reference(doc.inner_doc.add_object(p)))
+            page_ids.push(Reference(page_obj))
         }
 
         if doc.bookmarks.len() > 0 {
@@ -552,51 +554,58 @@ impl PdfDocumentReference {
                     .inner_doc
                     .add_object(Dictionary(LoDictionary::from_iter(vec![
                         ("Parent", Reference(bookmarks_id)),
-                        ("Title", title.into()),
+                        ("Title", String(title.into(), Literal)),
                         (
                             "Dest",
                             Array(vec![
                                 Reference(page_id_to_obj.get(&page_index).unwrap().to_owned()),
-                                "Fit".into(),
+                                "XYZ".into(),
+                                Null,
+                                Null,
+                                Null,
                             ]),
                         ),
                     ])));
                 bookmarks_list.set("First", Reference(obj_ref));
                 bookmarks_list.set("Last", Reference(obj_ref));
             } else {
-                for (i, (page_index, b_name)) in doc.bookmarks.iter().enumerate() {
+                let mut sorted_bmarks: Vec<(&usize, &std::string::String)> = doc.bookmarks.iter().collect();
+                sorted_bmarks.sort();
+                for (i, (page_index, b_name)) in sorted_bmarks.iter().enumerate() {
                     let dest = (
                         "Dest",
                         Array(vec![
                             Reference(page_id_to_obj.get(page_index).unwrap().to_owned()),
-                            "Fit".into(),
+                                "XYZ".into(),
+                                Null,
+                                Null,
+                                Null,
                         ]),
                     );
-                    dbg!(i, len);
                     doc.inner_doc
                         .add_object(Dictionary(LoDictionary::from_iter(if i == 0 {
-                            bookmarks_list.set("First", Reference((doc.inner_doc.max_id, 0)));
+                            bookmarks_list.set("First", Reference((doc.inner_doc.max_id + 1, 0)));
                             vec![
                                 ("Parent", Reference(bookmarks_id)),
-                                ("Title", b_name.to_owned().into()),
+                                ("Title", String(b_name.to_owned().to_owned().into(), Literal)),
+                                ("Next", Reference((doc.inner_doc.max_id + 2, 0))),
                                 dest,
-                                ("Next", Reference((doc.inner_doc.max_id + 1, 0))),
                             ]
                         } else if i == len - 1 {
                             bookmarks_list.set("Last", Reference((doc.inner_doc.max_id + 1, 0)));
                             vec![
                                 ("Parent", Reference(bookmarks_id)),
-                                ("Title", b_name.to_owned().into()),
+                                ("Title", String(b_name.to_owned().to_owned().into(), Literal)),
+                                ("Prev", Reference((doc.inner_doc.max_id, 0))),
                                 dest,
-                                ("Prev", Reference((doc.inner_doc.max_id - 1, 0))),
                             ]
                         } else {
                             vec![
                                 ("Parent", Reference(bookmarks_id)),
-                                ("Title", b_name.to_owned().into()),
+                                ("Title", String(b_name.to_owned().to_owned().into(), Literal)),
+                                ("Prev", Reference((doc.inner_doc.max_id, 0))),
+                                ("Next", Reference((doc.inner_doc.max_id + 2, 0))),
                                 dest,
-                                ("Prev", Reference((doc.inner_doc.max_id - 1, 0))),
-                                ("Next", Reference((doc.inner_doc.max_id + 1, 0))),
                             ]
                         })));
                 }


Test patch: