2021-12-21 14:44:46 +01:00
|
|
|
open Pdfutil
|
2023-04-18 17:39:51 +02:00
|
|
|
open Cpdferror
|
2021-12-21 14:44:46 +01:00
|
|
|
|
2021-12-31 17:47:28 +01:00
|
|
|
(* List annotations *)
|
2021-12-21 14:44:46 +01:00
|
|
|
let get_annotation_string encoding pdf annot =
|
|
|
|
match Pdf.lookup_direct pdf "/Contents" annot with
|
|
|
|
| Some (Pdf.String s) -> Cpdfmetadata.encode_output encoding s
|
|
|
|
| _ -> ""
|
|
|
|
|
|
|
|
let print_annotation encoding pdf num s =
|
|
|
|
let s = get_annotation_string encoding pdf s in
|
|
|
|
match s with
|
|
|
|
| "" -> ()
|
|
|
|
| s ->
|
|
|
|
flprint (Printf.sprintf "Page %d: " num);
|
|
|
|
flprint s;
|
|
|
|
flprint "\n"
|
|
|
|
|
|
|
|
let list_page_annotations encoding pdf num page =
|
|
|
|
match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
|
|
|
|
| Some (Pdf.Array annots) ->
|
|
|
|
iter (print_annotation encoding pdf num) (map (Pdf.direct pdf) annots)
|
|
|
|
| _ -> ()
|
|
|
|
|
2021-12-31 20:28:42 +01:00
|
|
|
let rewrite_destination calculate_pagenumber d =
|
|
|
|
match d with
|
|
|
|
| Pdf.Array (Pdf.Indirect i::r) ->
|
2023-01-13 08:40:37 +01:00
|
|
|
Pdf.Array (Pdf.Integer (calculate_pagenumber (Pdfdest.Fit (Pdfdest.PageObject i)))::r)
|
2021-12-31 20:28:42 +01:00
|
|
|
| x -> x
|
|
|
|
|
|
|
|
let rewrite_destinations pdf annot =
|
|
|
|
let refnums = Pdf.page_reference_numbers pdf in
|
|
|
|
let fastrefnums = hashtable_of_dictionary (combine refnums (indx refnums)) in
|
|
|
|
let calculate_pagenumber = Pdfpage.pagenumber_of_target ~fastrefnums pdf in
|
2023-01-13 08:40:37 +01:00
|
|
|
(* Deal with /P in annotation *)
|
|
|
|
let annot =
|
|
|
|
match Pdf.indirect_number pdf "/P" annot with
|
|
|
|
| Some i -> Pdf.add_dict_entry annot "/P" (Pdf.Integer (calculate_pagenumber (Pdfdest.Fit (Pdfdest.PageObject i))))
|
|
|
|
| None -> annot
|
|
|
|
in
|
2021-12-31 20:28:42 +01:00
|
|
|
(* Deal with /Dest in annotation *)
|
|
|
|
match Pdf.lookup_direct pdf "/Dest" annot with
|
|
|
|
| Some d -> Pdf.add_dict_entry annot "/Dest" (rewrite_destination calculate_pagenumber d)
|
|
|
|
| None ->
|
|
|
|
(* Deal with /A --> /D dest when /A --> /S = /GoTo *)
|
|
|
|
match Pdf.lookup_direct pdf "/A" annot with
|
|
|
|
| Some action ->
|
|
|
|
begin match Pdf.lookup_direct pdf "/D" action with
|
|
|
|
| Some d ->
|
|
|
|
Pdf.add_dict_entry
|
|
|
|
annot "/A" (Pdf.add_dict_entry action "/D" (rewrite_destination calculate_pagenumber d))
|
|
|
|
| None -> annot
|
|
|
|
end
|
|
|
|
| None -> annot
|
|
|
|
|
2023-01-13 06:51:43 +01:00
|
|
|
let extra = ref []
|
|
|
|
|
2023-04-21 14:49:56 +02:00
|
|
|
let serial = ref 0
|
2023-01-14 05:59:54 +01:00
|
|
|
|
|
|
|
let getserial () =
|
|
|
|
serial +=1; !serial
|
|
|
|
|
2023-01-16 03:06:59 +01:00
|
|
|
let objnum_to_serial_map = ref []
|
|
|
|
|
2021-12-21 14:44:46 +01:00
|
|
|
let annotations_json_page pdf page pagenum =
|
|
|
|
match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
|
|
|
|
| Some (Pdf.Array annots) ->
|
|
|
|
map
|
|
|
|
(fun annot ->
|
2023-01-16 03:06:59 +01:00
|
|
|
let serial = getserial () in
|
|
|
|
begin match annot with
|
|
|
|
| Pdf.Indirect i -> objnum_to_serial_map := (i, serial)::!objnum_to_serial_map
|
|
|
|
| _ -> Printf.eprintf "annotations must be indirect\n"
|
|
|
|
end;
|
|
|
|
let annot = Pdf.direct pdf annot in
|
|
|
|
let annot = rewrite_destinations pdf annot in
|
2023-01-16 03:19:48 +01:00
|
|
|
extra := annot::!extra;
|
2023-04-21 14:49:56 +02:00
|
|
|
`List
|
|
|
|
[`Int pagenum;
|
|
|
|
`Int serial;
|
|
|
|
Cpdfjson.json_of_object ~utf8:true ~clean_strings:true pdf (fun _ -> ())
|
|
|
|
~no_stream_data:false ~parse_content:false annot])
|
2023-01-16 03:06:59 +01:00
|
|
|
annots
|
2021-12-21 14:44:46 +01:00
|
|
|
| _ -> []
|
|
|
|
|
2023-04-21 14:49:56 +02:00
|
|
|
(* Rewrite any /Parent entries in /Popup annotations to have annot serial
|
|
|
|
number, not object number, and all /Popup entries in parent annotations
|
|
|
|
similarly. *)
|
2023-01-16 03:06:59 +01:00
|
|
|
let postprocess_json_pdf objnum_to_serial_map pdf obj =
|
2023-01-16 06:35:40 +01:00
|
|
|
let obj =
|
|
|
|
match obj with
|
|
|
|
| Pdf.Dictionary d ->
|
|
|
|
(* These things seem to be to do with digital signatures, which aren't
|
|
|
|
going to survive round-tripping of annotations anyway, and drag in
|
|
|
|
all sorts of extra objects we don't want, so we remove them. *)
|
|
|
|
let d = remove "/Lock" (remove "/V" d) in Pdf.Dictionary d
|
|
|
|
| _ -> obj
|
|
|
|
in
|
2023-01-16 03:06:59 +01:00
|
|
|
match obj with
|
|
|
|
| Pdf.Dictionary d ->
|
2023-01-16 04:37:25 +01:00
|
|
|
let obj =
|
2023-01-16 06:35:40 +01:00
|
|
|
begin match lookup "/Subtype" d, lookup "/Parent" d with
|
|
|
|
| Some (Pdf.Name "/Popup"), Some (Pdf.Indirect i) ->
|
|
|
|
begin match lookup i objnum_to_serial_map with
|
|
|
|
| Some s -> Pdf.add_dict_entry obj "/Parent" (Pdf.Integer s)
|
|
|
|
| None -> Printf.eprintf "Warning: Cpdfannot.process_extra_object: could not find serial number\n"; obj
|
|
|
|
end
|
2023-02-20 20:39:42 +01:00
|
|
|
| _ ->
|
|
|
|
(* If not a popup annotation, remove /Parent. It drags in lots of
|
2023-01-16 06:35:40 +01:00
|
|
|
extra objects (the whole page tree!) with a widget
|
|
|
|
annotation, and we are unlikely to be able to round-trip them
|
|
|
|
anyway. One day, if we can match FDF properly, it might be
|
|
|
|
possible, but not now. *)
|
2023-02-20 20:39:42 +01:00
|
|
|
Pdf.remove_dict_entry obj "/Parent"
|
2023-01-16 06:35:40 +01:00
|
|
|
end
|
2023-01-16 04:37:25 +01:00
|
|
|
in
|
|
|
|
begin match obj with
|
|
|
|
| Pdf.Dictionary d ->
|
|
|
|
begin match lookup "/Popup" d with
|
|
|
|
| Some (Pdf.Indirect i) ->
|
|
|
|
begin match lookup i objnum_to_serial_map with
|
|
|
|
| Some s -> Pdf.add_dict_entry obj "/Popup" (Pdf.Integer s)
|
|
|
|
| None -> Printf.eprintf "Warning: Cpdfannot.process_extra_object: could not find serial number 2\n"; obj
|
|
|
|
end
|
|
|
|
| _ -> obj
|
|
|
|
end
|
|
|
|
| _ -> obj
|
|
|
|
end
|
2023-01-16 03:06:59 +01:00
|
|
|
| x -> x
|
|
|
|
|
|
|
|
let postprocess_json pdf objnum_to_serial_map json =
|
|
|
|
map
|
|
|
|
(function
|
|
|
|
| `List [`Int pagenum; `Int serial; jo] ->
|
2023-02-17 16:30:39 +01:00
|
|
|
let pdfobj = Cpdfjson.object_of_json jo in
|
2023-01-16 03:06:59 +01:00
|
|
|
let fixed = postprocess_json_pdf objnum_to_serial_map pdf pdfobj in
|
2023-04-21 14:49:56 +02:00
|
|
|
`List
|
|
|
|
[`Int pagenum;
|
|
|
|
`Int serial;
|
|
|
|
Cpdfjson.json_of_object
|
|
|
|
~utf8:true ~clean_strings:true pdf (fun _ -> ())
|
|
|
|
~no_stream_data:false ~parse_content:false fixed]
|
2023-01-16 03:06:59 +01:00
|
|
|
| _ -> assert false)
|
|
|
|
json
|
2023-01-14 05:59:54 +01:00
|
|
|
|
2023-01-11 07:55:50 +01:00
|
|
|
let list_annotations_json range pdf =
|
2023-01-13 06:51:43 +01:00
|
|
|
extra := [];
|
2023-04-21 14:49:56 +02:00
|
|
|
serial := 0;
|
2023-01-16 03:06:59 +01:00
|
|
|
objnum_to_serial_map := [];
|
2021-12-21 14:44:46 +01:00
|
|
|
let module J = Cpdfyojson.Safe in
|
|
|
|
let pages = Pdfpage.pages_of_pagetree pdf in
|
|
|
|
let pagenums = indx pages in
|
2023-01-11 07:55:50 +01:00
|
|
|
let pairs = combine pages pagenums in
|
|
|
|
let pairs = option_map (fun (p, n) -> if mem n range then Some (p, n) else None) pairs in
|
|
|
|
let pages, pagenums = split pairs in
|
2023-01-14 05:59:54 +01:00
|
|
|
let json = flatten (map2 (annotations_json_page pdf) pages pagenums) in
|
2023-01-16 03:06:59 +01:00
|
|
|
let json = postprocess_json pdf !objnum_to_serial_map json in
|
2023-01-16 04:37:25 +01:00
|
|
|
let extra = map (postprocess_json_pdf !objnum_to_serial_map pdf) !extra in
|
|
|
|
(*Printf.printf "%i extra roots to explore\n" (length extra);
|
|
|
|
iter (fun x -> Printf.eprintf "%s\n\n" (Pdfwrite.string_of_pdf x)) extra;*)
|
2023-01-14 05:59:54 +01:00
|
|
|
let extra =
|
|
|
|
map
|
|
|
|
(fun n ->
|
2023-01-19 04:34:07 +01:00
|
|
|
`List
|
|
|
|
[`Int ~-n;
|
|
|
|
Cpdfjson.json_of_object ~utf8:true ~clean_strings:true pdf (fun _ -> ())
|
|
|
|
~no_stream_data:false ~parse_content:false (Pdf.lookup_obj pdf n)])
|
|
|
|
(setify
|
|
|
|
(flatten
|
|
|
|
(map
|
|
|
|
(fun x ->
|
|
|
|
let r = Pdf.objects_referenced [] [] pdf x in
|
|
|
|
(*Printf.printf "%i extra for annot %s\n" (length r)
|
|
|
|
(Pdfwrite.string_of_pdf x);*) r)
|
|
|
|
extra)))
|
2023-01-14 05:59:54 +01:00
|
|
|
in
|
2023-01-16 07:07:11 +01:00
|
|
|
let header =
|
|
|
|
`List
|
|
|
|
[`Int 0;
|
2023-01-19 04:34:07 +01:00
|
|
|
Cpdfjson.json_of_object ~utf8:true ~clean_strings:true pdf (fun _ -> ())
|
|
|
|
~no_stream_data:false ~parse_content:false
|
2023-01-16 07:07:11 +01:00
|
|
|
(Pdf.Dictionary ["/CPDFJSONannotformatversion", Pdf.Integer 1])]
|
|
|
|
in
|
|
|
|
let json = `List ([header] @ json @ extra) in
|
2021-12-21 14:44:46 +01:00
|
|
|
J.pretty_to_channel stdout json
|
|
|
|
|
2023-01-11 07:55:50 +01:00
|
|
|
let list_annotations ~json range encoding pdf =
|
2021-12-21 14:44:46 +01:00
|
|
|
if json
|
2023-01-11 07:55:50 +01:00
|
|
|
then list_annotations_json range pdf
|
2021-12-21 14:44:46 +01:00
|
|
|
else Cpdfpage.iter_pages (list_page_annotations encoding pdf) pdf range
|
|
|
|
|
2021-12-31 17:47:28 +01:00
|
|
|
(* Return annotations *)
|
2021-12-21 14:44:46 +01:00
|
|
|
let get_annotations encoding pdf =
|
|
|
|
let pages = Pdfpage.pages_of_pagetree pdf in
|
|
|
|
flatten
|
|
|
|
(map2
|
|
|
|
(fun page pagenumber ->
|
|
|
|
match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
|
|
|
|
| Some (Pdf.Array annots) ->
|
|
|
|
let strings =
|
|
|
|
map (get_annotation_string encoding pdf) (map (Pdf.direct pdf) annots)
|
|
|
|
in
|
|
|
|
combine (many pagenumber (length strings)) strings
|
|
|
|
| _ -> [])
|
|
|
|
pages
|
|
|
|
(ilist 1 (length pages)))
|
|
|
|
|
2022-01-16 17:30:36 +01:00
|
|
|
let get_annotations_json pdf =
|
|
|
|
let module J = Cpdfyojson.Safe in
|
|
|
|
let pages = Pdfpage.pages_of_pagetree pdf in
|
|
|
|
let pagenums = indx pages in
|
|
|
|
let json = `List (flatten (map2 (annotations_json_page pdf) pages pagenums)) in
|
|
|
|
Pdfio.bytes_of_string (J.to_string json)
|
|
|
|
|
2023-04-18 17:39:51 +02:00
|
|
|
(** Set annotations from JSON, keeping any existing ones. *)
|
|
|
|
let set_annotations_json pdf i =
|
|
|
|
let module J = Cpdfyojson.Safe in
|
|
|
|
let content = Pdfio.string_of_bytes (Pdfio.bytes_of_input i 0 i.Pdfio.in_channel_length) in
|
|
|
|
let json = J.from_string content in
|
|
|
|
(* Find largest negative objnumber. Then add number of annot objects. *)
|
|
|
|
match json with
|
|
|
|
| `List entries ->
|
2023-04-18 18:09:21 +02:00
|
|
|
(* Renumber the PDF so everything has bigger object numbers than that. *)
|
2023-04-18 17:39:51 +02:00
|
|
|
let maxobjnum =
|
2023-04-18 18:09:21 +02:00
|
|
|
fold_left
|
|
|
|
max
|
|
|
|
min_int
|
2023-04-21 14:49:56 +02:00
|
|
|
(map (fun e -> match e with `List [_; `Int i; _] -> i | `List [`Int i; _] -> abs i | _ -> error "Bad annots entry") entries)
|
2023-04-18 17:39:51 +02:00
|
|
|
in
|
2023-04-18 18:09:21 +02:00
|
|
|
let pdf_objnums =
|
|
|
|
map fst (list_of_hashtbl pdf.Pdf.objects.Pdf.pdfobjects)
|
|
|
|
in
|
|
|
|
let change_table =
|
|
|
|
hashtable_of_dictionary (map2 (fun f t -> (f, t)) pdf_objnums (ilist (maxobjnum + 1) (maxobjnum + length pdf_objnums)))
|
|
|
|
in
|
|
|
|
let pdf = Pdf.renumber change_table pdf in
|
2023-04-18 21:19:50 +02:00
|
|
|
(* 1. Rewrite all serial numbers in all places (annots and extra objects?) *)
|
|
|
|
(* 2. Add all the negative numbered objects straight to the file *)
|
|
|
|
(* 3. Add all the actual annotations to the /Annots array on each page *)
|
2023-04-18 17:39:51 +02:00
|
|
|
()
|
|
|
|
| _ -> error "Bad Annotations JSON file"
|
2023-01-13 06:51:43 +01:00
|
|
|
|
2021-12-21 14:44:46 +01:00
|
|
|
(* Equalise the page lengths of two PDFs by chopping or extending the first one.
|
|
|
|
*)
|
|
|
|
let equalise_lengths a b =
|
|
|
|
let a' =
|
|
|
|
if Pdfpage.endpage a < Pdfpage.endpage b then
|
|
|
|
Pdfpage.change_pages false a
|
|
|
|
(Pdfpage.pages_of_pagetree a @
|
|
|
|
many (Pdfpage.blankpage Pdfpaper.a4) (Pdfpage.endpage b - Pdfpage.endpage a))
|
|
|
|
else if Pdfpage.endpage a > Pdfpage.endpage b then
|
|
|
|
Pdfpage.change_pages false a
|
|
|
|
(take (Pdfpage.pages_of_pagetree a) (Pdfpage.endpage b))
|
|
|
|
else a
|
|
|
|
in
|
|
|
|
a', b
|
|
|
|
|
|
|
|
(* Copy annotations *)
|
|
|
|
|
|
|
|
(* FIXME: Why does this chop the files to the same length? Should be able to
|
|
|
|
apply annotations from a longer file to a shorter? *)
|
|
|
|
|
|
|
|
(* Rewrite any annotation destinations to point to pages in the
|
|
|
|
destination file. This prevents pages being copied, and ensures the links are
|
|
|
|
correct Any Indirect link inside a /Dest is rewritten if in the table. If not
|
|
|
|
inside a /Dest, nothing is rewritten. *)
|
|
|
|
let rec renumber_in_dest table indest = function
|
|
|
|
Pdf.Indirect i ->
|
|
|
|
begin
|
|
|
|
try Pdf.Indirect (Hashtbl.find table i) with _ -> Pdf.Indirect i
|
|
|
|
end
|
|
|
|
| Pdf.Array a ->
|
|
|
|
Pdf.recurse_array (renumber_in_dest table indest) a
|
|
|
|
| Pdf.Dictionary d ->
|
|
|
|
Pdf.Dictionary
|
|
|
|
(map
|
|
|
|
(function
|
|
|
|
("/Dest", v) -> ("/Dest", renumber_in_dest table true v)
|
|
|
|
| (k, v) -> (k, renumber_in_dest table indest v))
|
|
|
|
d)
|
|
|
|
| x -> x
|
|
|
|
|
|
|
|
let renumber_in_object pdf objnum table =
|
|
|
|
Pdf.addobj_given_num
|
|
|
|
pdf (objnum, (renumber_in_dest table false (Pdf.lookup_obj pdf objnum)))
|
|
|
|
|
|
|
|
let copy_annotations_page topdf frompdf frompage topage =
|
|
|
|
match Pdf.lookup_direct frompdf "/Annots" frompage.Pdfpage.rest with
|
|
|
|
Some (Pdf.Array frompage_annots as annots) ->
|
|
|
|
let table =
|
|
|
|
hashtable_of_dictionary
|
|
|
|
(combine
|
|
|
|
(Pdf.page_reference_numbers frompdf)
|
|
|
|
(Pdf.page_reference_numbers topdf))
|
|
|
|
in
|
|
|
|
iter
|
|
|
|
(function
|
|
|
|
(* FIXME: We assume they are indirects. Must also do direct, though rare.*)
|
|
|
|
Pdf.Indirect x ->
|
|
|
|
(*Printf.printf "Copying annotation %s which is\n%s\n"
|
|
|
|
(Pdfwrite.string_of_pdf (Pdf.Indirect x))
|
|
|
|
(Pdfwrite.string_of_pdf (Pdf.direct frompdf (Pdf.Indirect
|
|
|
|
x)));*)
|
|
|
|
renumber_in_object frompdf x table
|
|
|
|
| _ -> ())
|
|
|
|
frompage_annots;
|
|
|
|
let objects_to_copy = Pdf.objects_referenced [] [] frompdf annots in
|
|
|
|
iter
|
|
|
|
(fun n ->
|
|
|
|
ignore (Pdf.addobj_given_num topdf (n, Pdf.lookup_obj frompdf n)))
|
|
|
|
objects_to_copy;
|
|
|
|
let topage_annots =
|
|
|
|
match Pdf.lookup_direct frompdf "/Annots" topage.Pdfpage.rest with
|
|
|
|
| Some (Pdf.Array annots) -> annots
|
|
|
|
| _ -> []
|
|
|
|
in
|
|
|
|
let merged_dict = Pdf.Array (frompage_annots @ topage_annots) in
|
|
|
|
let topage' =
|
|
|
|
{topage with Pdfpage.rest =
|
|
|
|
Pdf.add_dict_entry topage.Pdfpage.rest "/Annots" merged_dict}
|
|
|
|
in
|
|
|
|
topdf, topage'
|
|
|
|
| Some x -> topdf, topage
|
|
|
|
| None -> topdf, topage
|
|
|
|
|
|
|
|
let copy_annotations range frompdf topdf =
|
|
|
|
let frompdf, topdf = equalise_lengths frompdf topdf in
|
|
|
|
match Pdf.renumber_pdfs [frompdf; topdf] with
|
|
|
|
| [frompdf; topdf] ->
|
|
|
|
let frompdf_pages = Pdfpage.pages_of_pagetree frompdf in
|
|
|
|
let topdf_pages = Pdfpage.pages_of_pagetree topdf in
|
|
|
|
let pdf = ref topdf
|
|
|
|
and pages = ref []
|
|
|
|
and pnum = ref 1
|
|
|
|
and frompdf_pages = ref frompdf_pages
|
|
|
|
and topdf_pages = ref topdf_pages in
|
|
|
|
(* Go through, updating pdf and collecting new pages. *)
|
|
|
|
while not (isnull !frompdf_pages) do
|
|
|
|
let frompdf_page = hd !frompdf_pages
|
|
|
|
and topdf_page = hd !topdf_pages in
|
|
|
|
let pdf', page =
|
|
|
|
if mem !pnum range
|
|
|
|
then copy_annotations_page !pdf frompdf frompdf_page topdf_page
|
|
|
|
else !pdf, topdf_page
|
|
|
|
in
|
|
|
|
pdf := pdf';
|
|
|
|
pages =| page;
|
|
|
|
incr pnum;
|
|
|
|
frompdf_pages := tl !frompdf_pages;
|
|
|
|
topdf_pages := tl !topdf_pages
|
|
|
|
done;
|
|
|
|
Pdfpage.change_pages true !pdf (rev !pages)
|
|
|
|
| _ -> assert false
|
|
|
|
|
2021-12-31 17:47:28 +01:00
|
|
|
(* Remove annotations *)
|
2021-12-21 14:44:46 +01:00
|
|
|
let remove_annotations range pdf =
|
|
|
|
let remove_annotations_page pagenum page =
|
|
|
|
if mem pagenum range then
|
|
|
|
let rest' =
|
|
|
|
Pdf.remove_dict_entry page.Pdfpage.rest "/Annots"
|
|
|
|
in
|
|
|
|
{page with Pdfpage.rest = rest'}
|
|
|
|
else
|
|
|
|
page
|
|
|
|
in
|
2023-03-23 20:24:50 +01:00
|
|
|
Cpdfpage.process_pages (Pdfpage.ppstub remove_annotations_page) pdf range
|