cpdf-source/cpdfdraw.ml

377 lines
14 KiB
OCaml
Raw Normal View History

2022-12-15 13:41:19 +01:00
open Pdfutil
2023-05-11 16:55:48 +02:00
open Cpdferror
2022-12-15 13:41:19 +01:00
2022-12-22 17:20:00 +01:00
type colspec =
2022-12-15 13:41:19 +01:00
NoCol
| RGB of float * float * float
| Grey of float
| CYMK of float * float * float * float
type drawops =
2022-12-21 17:09:04 +01:00
| Rect of float * float * float * float
| Bezier of float * float * float * float * float * float
2023-05-12 20:01:59 +02:00
| Bezier23 of float * float * float * float
| Bezier13 of float * float * float * float
2022-12-15 13:41:19 +01:00
| To of float * float
| Line of float * float
2022-12-16 14:13:55 +01:00
| ClosePath
2022-12-22 17:20:00 +01:00
| SetFill of colspec
| SetStroke of colspec
2022-12-15 15:20:41 +01:00
| SetLineThickness of float
| SetLineCap of int
| SetLineJoin of int
| SetMiterLimit of float
| SetDashPattern of float list * float
2022-12-16 13:13:38 +01:00
| Matrix of Pdftransform.transform_matrix
2023-05-09 15:30:30 +02:00
| Qq of drawops list
2022-12-16 14:13:55 +01:00
| Fill
| FillEvenOdd
| Stroke
| FillStroke
| FillStrokeEvenOdd
2022-12-21 17:40:13 +01:00
| Clip
| ClipEvenOdd
2023-05-04 14:53:49 +02:00
| FormXObject of float * float * float * float * string * drawops list
2023-05-03 15:19:55 +02:00
| Use of string
2022-12-22 21:42:55 +01:00
| ImageXObject of string * Pdf.pdfobject
2023-05-03 15:19:55 +02:00
| Image of string
2023-04-27 20:14:58 +02:00
| NewPage
| Opacity of float
| SOpacity of float
2023-07-07 15:34:51 +02:00
| Font of Cpdfembed.cpdffont * float
2023-05-09 15:30:30 +02:00
| TextSection of drawops list
2023-04-27 20:14:58 +02:00
| Text of string
2023-05-02 15:47:18 +02:00
| SpecialText of string
2023-04-28 20:03:10 +02:00
| Newline
2023-05-01 15:39:42 +02:00
| Leading of float
| CharSpace of float
| WordSpace of float
| TextScale of float
| RenderMode of int
| Rise of float
2022-12-22 17:20:00 +01:00
2023-05-11 16:31:10 +02:00
let rec string_of_drawop = function
| Qq o -> "Qq (" ^ string_of_drawops o ^ ")"
| FormXObject (_, _, _, _, _, o) -> "FormXObject (" ^ string_of_drawops o ^ ")"
| TextSection o -> "TextSection (" ^ string_of_drawops o ^ ")"
2023-05-12 20:01:59 +02:00
| Rect _ -> "Rect" | Bezier _ -> "Bezier" | Bezier23 _ -> "Bezier23"
| Bezier13 _ -> "Bezier13" | To _ -> "To" | Line _ -> "Line"
2023-05-11 16:31:10 +02:00
| ClosePath -> "ClosePath" | SetFill _ -> "SetFill" | SetStroke _ -> "SetStroke"
| SetLineThickness _ -> "SetLineThickness" | SetLineCap _ -> "SetLineCap"
| SetLineJoin _ -> "SetLineJoin" | SetMiterLimit _ -> "SetMiterLimit"
| SetDashPattern _ -> "SetDashPattern" | Matrix _ -> "SetMatrix"
| Fill -> "Fill" | FillEvenOdd -> "FillEvenOdd" | Stroke -> "Stroke"
| FillStroke -> "FillStroke" | FillStrokeEvenOdd -> "FillStrokeEvenOdd"
| Clip -> "Clip" | ClipEvenOdd -> "ClipEvenOdd" | Use _ -> "Use"
| ImageXObject _ -> "ImageXObject" | Image _ -> "Image" | NewPage -> "NewPage"
| Opacity _ -> "Opacity" | SOpacity _ -> "SOpacity" | Font _ -> "Font" | Text _ -> "Text"
| SpecialText _ -> "SpecialText" | Newline -> "Newline" | Leading _ -> "Leading"
| CharSpace _ -> "CharSpace" | WordSpace _ -> "WordSpace" | TextScale _ -> "TextScale"
| RenderMode _ -> "RenderMode" | Rise _ -> "Rise"
and string_of_drawops l =
fold_left (fun x y -> x ^ " " ^ y) "" (map string_of_drawop l)
2023-05-12 23:54:08 +02:00
(* Per page / xobject resources *)
2023-05-04 16:01:12 +02:00
type res =
{images : (string, (string * int)) Hashtbl.t; (* (name, (pdf name, objnum)) *)
2023-05-05 17:17:35 +02:00
extgstates : ((string * float), string) Hashtbl.t; (* (kind, value), name *)
fonts : (Pdftext.font, (string * int)) Hashtbl.t; (* (font, (objnum, pdf name)) *)
2023-05-05 14:42:47 +02:00
form_xobjects : (string, (string * int)) Hashtbl.t; (* (name, (pdf name, objnum)) *)
2023-05-07 17:40:02 +02:00
mutable page_names : string list;
2023-05-04 16:01:12 +02:00
mutable time : Cpdfstrftime.t;
mutable current_fontpack : Cpdfembed.t;
mutable font_size : float;
2023-05-04 16:01:12 +02:00
mutable num : int}
2023-05-03 16:49:14 +02:00
2023-05-10 18:03:53 +02:00
let empty_res () =
2023-05-04 16:01:12 +02:00
{images = null_hash ();
extgstates = null_hash ();
fonts = null_hash ();
form_xobjects = null_hash ();
2023-05-07 17:40:02 +02:00
page_names = [];
2023-05-04 16:01:12 +02:00
time = Cpdfstrftime.dummy;
2023-07-07 15:34:51 +02:00
current_fontpack =
(Cpdfembed.fontpack_of_standardfont
(Pdftext.StandardFont (Pdftext.TimesRoman, Pdftext.WinAnsiEncoding)));
font_size = 12.;
2023-05-04 16:01:12 +02:00
num = 0}
2022-12-22 17:20:00 +01:00
2023-05-08 16:13:17 +02:00
let resstack =
2023-05-10 18:03:53 +02:00
ref [empty_res ()]
2023-05-08 16:13:17 +02:00
let res () =
2023-05-11 16:55:48 +02:00
try hd !resstack with _ -> error "graphics stack empty"
2023-05-08 16:13:17 +02:00
2023-05-08 17:58:19 +02:00
let rescopy r =
{r with
images = Hashtbl.copy r.images;
fonts = Hashtbl.copy r.fonts;
extgstates = Hashtbl.copy r.extgstates;
form_xobjects = Hashtbl.copy r.form_xobjects}
2023-05-08 16:48:18 +02:00
let respush () =
2023-05-08 17:58:19 +02:00
resstack := (rescopy (res ()))::!resstack
2023-05-08 16:48:18 +02:00
let respop () =
2023-05-11 16:55:48 +02:00
let n = (res ()).num in
2023-05-09 13:36:45 +02:00
resstack := tl !resstack;
(* not necessary, since names are isolated in the xobject, but it makes
manual debugging of PDF files easier if we don't re-use numbers *)
(res ()).num <- max n (res ()).num
2023-05-08 16:48:18 +02:00
2023-05-04 16:01:12 +02:00
let fresh_name s =
2023-05-08 16:13:17 +02:00
(res ()).num <- (res ()).num + 1;
s ^ string_of_int (res ()).num
2022-12-16 13:13:38 +01:00
2023-05-07 17:40:02 +02:00
(* At end of page, we keep things for which we have indirects - but ExtGStates
aren't indirect, so they go. *)
2023-05-03 14:43:57 +02:00
let reset_state () =
2023-05-08 16:13:17 +02:00
Hashtbl.clear (res ()).extgstates;
(res ()).page_names <- []
2023-05-01 17:53:28 +02:00
2023-05-01 20:00:28 +02:00
let process_specials pdf endpage filename bates batespad num page s =
let pairs =
Cpdfaddtext.replace_pairs pdf endpage None filename bates batespad num page
in
2023-05-08 16:13:17 +02:00
Cpdfaddtext.process_text (res ()).time s pairs
2023-05-01 20:00:28 +02:00
(* FIXME: implement for other kinds of font *)
let runs_of_utf8 s =
2023-07-07 15:34:51 +02:00
match (res ()).current_fontpack with
| ((f::_, _) as fontpack) ->
2023-07-07 15:34:51 +02:00
let codepoints = Pdftext.codepoints_of_utf8 s in
let charcodes = option_map (Cpdfembed.get_char fontpack) codepoints in
let fontname =
fst (Hashtbl.find (res ()).fonts f)
in
[Pdfops.Op_Tf (fontname, (res ()).font_size); Pdfops.Op_Tj (implode (map (fun (c, _, _) -> char_of_int c) charcodes))]
2023-07-07 15:34:51 +02:00
| _ -> failwith "charcodes_of_utf8: unknown font"
2023-05-05 17:17:35 +02:00
let extgstate kind v =
2023-05-08 16:13:17 +02:00
try Hashtbl.find (res ()).extgstates (kind, v) with
2023-05-05 17:17:35 +02:00
Not_found ->
2023-05-08 17:58:19 +02:00
let n = fresh_name "/G" in
2023-05-08 16:13:17 +02:00
Hashtbl.add (res ()).extgstates (kind, v) n;
2023-05-05 17:17:35 +02:00
n
2023-05-08 17:29:03 +02:00
let read_resource pdf n res =
match Pdf.lookup_direct pdf n res with
| Some (Pdf.Dictionary d) -> d
| _ -> []
let update_resources pdf old_resources =
2023-05-12 17:24:36 +02:00
let gss_resources = map (fun ((kind, v), n) -> (n, Pdf.Dictionary [(kind, Pdf.Real v)])) (list_of_hashtbl (res ()).extgstates) in
2023-05-08 17:29:03 +02:00
let select_resources t =
option_map (fun (_, (n, o)) -> if mem n (res ()).page_names then Some (n, Pdf.Indirect o) else None) (list_of_hashtbl t)
in
let update = fold_right (fun (k, v) d -> add k v d) in
let new_gss = update gss_resources (read_resource pdf "/ExtGState" old_resources) in
let new_xobjects = update (select_resources (res ()).form_xobjects @ select_resources (res ()).images) (read_resource pdf "/XObject" old_resources) in
let new_fonts = update (select_resources (res ()).fonts) (read_resource pdf "/Font" old_resources) in
let add_if_non_empty dict name newdict =
if newdict = Pdf.Dictionary [] then dict else
Pdf.add_dict_entry dict name newdict
in
add_if_non_empty
(add_if_non_empty
(add_if_non_empty old_resources "/XObject" (Pdf.Dictionary new_xobjects))
"/ExtGState"
(Pdf.Dictionary new_gss))
"/Font"
(Pdf.Dictionary new_fonts)
2023-05-01 20:00:28 +02:00
let rec ops_of_drawop pdf endpage filename bates batespad num page = function
2023-05-09 15:30:30 +02:00
| Qq ops -> [Pdfops.Op_q] @ ops_of_drawops pdf endpage filename bates batespad num page ops @ [Pdfops.Op_Q]
2022-12-16 13:13:38 +01:00
| Matrix m -> [Pdfops.Op_cm m]
2022-12-15 13:41:19 +01:00
| Rect (x, y, w, h) -> [Pdfops.Op_re (x, y, w, h)]
2022-12-21 17:09:04 +01:00
| Bezier (a, b, c, d, e, f) -> [Pdfops.Op_c (a, b, c, d, e, f)]
2023-05-12 20:01:59 +02:00
| Bezier23 (a, b, c, d) -> [Pdfops.Op_v (a, b, c, d)]
| Bezier13 (a, b, c, d) -> [Pdfops.Op_y (a, b, c, d)]
2022-12-15 13:41:19 +01:00
| To (x, y) -> [Pdfops.Op_m (x, y)]
| Line (x, y) -> [Pdfops.Op_l (x, y)]
2022-12-16 14:13:55 +01:00
| SetFill x ->
2022-12-15 13:41:19 +01:00
begin match x with
| RGB (r, g, b) -> [Op_rg (r, g, b)]
| Grey g -> [Op_g g]
| CYMK (c, y, m, k) -> [Op_k (c, y, m, k)]
| NoCol -> []
end
2022-12-16 14:13:55 +01:00
| SetStroke x ->
2022-12-15 13:41:19 +01:00
begin match x with
| RGB (r, g, b) -> [Op_RG (r, g, b)]
| Grey g -> [Op_G g]
| CYMK (c, y, m, k) -> [Op_K (c, y, m, k)]
| NoCol -> []
end
2023-05-12 16:33:28 +02:00
| ClosePath -> [Pdfops.Op_h]
2022-12-16 14:13:55 +01:00
| Fill -> [Pdfops.Op_f]
| FillEvenOdd -> [Pdfops.Op_f']
| Stroke -> [Pdfops.Op_S]
| FillStroke -> [Pdfops.Op_B]
| FillStrokeEvenOdd -> [Pdfops.Op_B']
2022-12-21 17:40:13 +01:00
| Clip -> [Pdfops.Op_W; Pdfops.Op_n]
2023-05-12 20:36:53 +02:00
| ClipEvenOdd -> [Pdfops.Op_W'; Pdfops.Op_n]
2023-05-12 16:33:28 +02:00
| SetLineThickness t -> [Pdfops.Op_w t]
2022-12-16 14:13:55 +01:00
| SetLineCap c -> [Pdfops.Op_J c]
| SetLineJoin j -> [Pdfops.Op_j j]
| SetMiterLimit m -> [Pdfops.Op_M m]
| SetDashPattern (x, y) -> [Pdfops.Op_d (x, y)]
2023-05-11 16:31:10 +02:00
| FormXObject (a, b, c, d, n, ops) ->
create_form_xobject a b c d pdf endpage filename bates batespad num page n ops;
[]
2023-05-07 17:40:02 +02:00
| Use n ->
2023-05-11 16:55:48 +02:00
let pdfname = try fst (Hashtbl.find (res ()).form_xobjects n) with _ -> error ("Form XObject not found: " ^ n) in
2023-05-08 16:13:17 +02:00
(res ()).page_names <- pdfname::(res ()).page_names;
2023-05-07 17:40:02 +02:00
[Pdfops.Op_Do pdfname]
| Image s ->
2023-05-11 16:55:48 +02:00
let pdfname = try fst (Hashtbl.find (res ()).images s) with _ -> error ("Image not found: " ^ s) in
2023-05-08 16:13:17 +02:00
(res ()).page_names <- pdfname::(res ()).page_names;
2023-05-07 17:40:02 +02:00
[Pdfops.Op_Do pdfname]
2022-12-22 21:42:55 +01:00
| ImageXObject (s, obj) ->
2023-05-08 17:58:19 +02:00
Hashtbl.add (res ()).images s (fresh_name "/I", Pdf.addobj pdf obj);
2022-12-22 17:20:00 +01:00
[]
2023-04-27 20:14:58 +02:00
| NewPage -> Pdfe.log ("NewPage remaining in graphic stream"); assert false
2023-05-05 17:17:35 +02:00
| Opacity v -> [Pdfops.Op_gs (extgstate "/ca" v)]
| SOpacity v -> [Pdfops.Op_gs (extgstate "/CA" v)]
| Font (cpdffont, size) ->
let fontpack =
match cpdffont with
| PreMadeFontPack fp -> fp
| EmbedInfo {fontfile; fontname; encoding} ->
Cpdfembed.embed_truetype pdf ~fontfile ~fontname ~codepoints:[int_of_char 'a'] ~encoding
| ExistingNamedFont ->
error "-draw does not support using an exsiting named font"
in
let ns =
map
(fun font ->
try fst (Hashtbl.find (res ()).fonts font) with
Not_found ->
let o = Pdftext.write_font pdf font in
let n = fresh_name "/F" in
Hashtbl.add (res ()).fonts font (n, o);
n)
(fst fontpack)
2023-05-05 17:17:35 +02:00
in
(res ()).current_fontpack <- fontpack;
(res ()).page_names <- ns @ (res ()).page_names;
(res ()).font_size <- size;
[]
2023-05-09 15:30:30 +02:00
| TextSection ops -> [Pdfops.Op_BT] @ ops_of_drawops pdf endpage filename bates batespad num page ops @ [Pdfops.Op_ET]
| Text s -> runs_of_utf8 s
| SpecialText s -> runs_of_utf8 (process_specials pdf endpage filename bates batespad num page s)
2023-05-01 15:39:42 +02:00
| Leading f -> [Pdfops.Op_TL f]
| CharSpace f -> [Pdfops.Op_Tc f]
| WordSpace f -> [Pdfops.Op_Tw f]
| TextScale f -> [Pdfops.Op_Tz f]
| RenderMode i -> [Pdfops.Op_Tr i]
| Rise f -> [Pdfops.Op_Ts f]
| Newline -> [Pdfops.Op_T']
2022-12-15 13:41:19 +01:00
2023-05-01 20:00:28 +02:00
and ops_of_drawops pdf endpage filename bates batespad num page drawops =
flatten (map (ops_of_drawop pdf endpage filename bates batespad num page) drawops)
2022-12-15 13:41:19 +01:00
2023-05-04 14:53:49 +02:00
and create_form_xobject a b c d pdf endpage filename bates batespad num page n ops =
2023-05-08 16:48:18 +02:00
respush ();
2023-05-08 17:29:03 +02:00
reset_state ();
2023-05-03 20:01:25 +02:00
let data =
Pdfio.bytes_of_string (Pdfops.string_of_ops (ops_of_drawops pdf endpage filename bates batespad num page ops))
in
let obj =
Pdf.Stream
{contents =
(Pdf.Dictionary
[("/Length", Pdf.Integer (Pdfio.bytes_size data));
("/Subtype", Pdf.Name "/Form");
2023-05-08 17:29:03 +02:00
("/Resources", update_resources pdf (Pdf.Dictionary []));
2023-05-04 14:53:49 +02:00
("/BBox", Pdf.Array [Pdf.Real a; Pdf.Real b; Pdf.Real c; Pdf.Real d])
2023-05-03 20:01:25 +02:00
],
Pdf.Got data)}
in
2023-05-08 17:58:19 +02:00
respop ();
Hashtbl.add (res ()).form_xobjects n (fresh_name "/X", (Pdf.addobj pdf obj))
2023-05-03 20:01:25 +02:00
2023-05-04 19:57:08 +02:00
let minimum_resource_number pdf range =
2023-05-05 15:46:51 +02:00
let pages = Pdfpage.pages_of_pagetree pdf in
let pages_in_range =
option_map2 (fun p n -> if mem n range then Some p else None) pages (indx pages) in
let number_of_name s =
match implode (rev (takewhile (function '0'..'9' -> true | _ -> false) (rev (explode s)))) with
| "" -> None
| s -> Some (int_of_string s)
in
let resource_names_page p =
let names n =
match Pdf.lookup_direct pdf n p.Pdfpage.resources with
| Some (Pdf.Dictionary d) -> map fst d
| _ -> []
in
names "/XObject" @ names "/ExtGState" @ names "/Font"
in
match
sort
(fun a b -> compare b a)
(option_map number_of_name (flatten (map resource_names_page pages_in_range)))
with
| [] -> 0
| n::_ -> n + 1
2023-05-04 19:57:08 +02:00
2023-05-12 15:04:14 +02:00
let rec contains_specials_drawop = function
| SpecialText _ -> true
| Qq l | TextSection l | FormXObject (_, _, _, _, _, l) -> contains_specials l
| _ -> false
and contains_specials l =
List.exists contains_specials_drawop l
2023-05-05 17:27:41 +02:00
2023-05-11 20:18:14 +02:00
let draw_single ~fast ~underneath ~filename ~bates ~batespad fast range pdf drawops =
2023-05-08 16:13:17 +02:00
(res ()).num <- max (res ()).num (minimum_resource_number pdf range);
2023-05-02 15:47:18 +02:00
let endpage = Pdfpage.endpage pdf in
let pages = Pdfpage.pages_of_pagetree pdf in
2023-05-11 20:18:14 +02:00
let ops =
2023-05-08 17:29:03 +02:00
if contains_specials drawops
then None
2023-05-11 20:18:14 +02:00
else Some (ops_of_drawops pdf endpage filename bates batespad 0 (hd pages) drawops)
2023-05-05 17:27:41 +02:00
in
2023-05-02 15:47:18 +02:00
let ss =
map2
2023-05-05 14:42:47 +02:00
(fun n p ->
2023-05-08 17:29:03 +02:00
if mem n range
2023-05-11 20:18:14 +02:00
then (match ops with Some x -> x | None -> ops_of_drawops pdf endpage filename bates batespad n p drawops)
else [])
2023-05-02 15:47:18 +02:00
(ilist 1 endpage)
pages
in
2023-05-04 16:51:03 +02:00
let pages =
2023-05-11 20:18:14 +02:00
map3
(fun n p ops ->
if not (mem n range) then p else
let page = {p with Pdfpage.resources = update_resources pdf p.Pdfpage.resources} in
(if underneath then Pdfpage.prepend_operators else Pdfpage.postpend_operators) pdf ops ~fast page)
2023-05-08 15:15:03 +02:00
(ilist 1 endpage)
2023-05-04 16:51:03 +02:00
(Pdfpage.pages_of_pagetree pdf)
2023-05-11 20:18:14 +02:00
ss
2023-05-04 16:51:03 +02:00
in
Pdfpage.change_pages true pdf pages
2023-05-03 14:43:57 +02:00
2023-05-11 20:18:14 +02:00
let draw ?(fast=false) ?(underneath=false) ~filename ~bates ~batespad fast range pdf drawops =
2023-05-10 18:03:53 +02:00
resstack := [empty_res ()];
2023-05-08 16:13:17 +02:00
(res ()).time <- Cpdfstrftime.current_time ();
2023-05-03 14:43:57 +02:00
let pdf = ref pdf in
let range = ref range in
2023-05-10 20:11:31 +02:00
(* Double up a trailing NewPage so it actually does something... *)
2023-05-11 15:39:37 +02:00
let drawops = match rev drawops with NewPage::t -> rev (NewPage::NewPage::t) | _ -> drawops in
2023-05-03 14:43:57 +02:00
let chunks = ref (split_around (eq NewPage) drawops) in
while !chunks <> [] do
reset_state ();
2023-05-11 20:18:14 +02:00
if hd !chunks <> [] then pdf := draw_single ~fast ~underneath ~filename ~bates ~batespad fast !range !pdf (hd !chunks);
2023-05-03 14:43:57 +02:00
chunks := tl !chunks;
if !chunks <> [] then begin
let endpage = Pdfpage.endpage !pdf in
pdf := Cpdfpad.padafter [endpage] !pdf;
range := [endpage + 1]
end
done;
!pdf