(* Structured Graphics *)
open Pdfutil
open Pdfio

type fpoint = float * float

type winding_rule = EvenOdd | NonZero

type segment =
  | Straight of fpoint * fpoint
  | Bezier of fpoint * fpoint * fpoint * fpoint

(* Each segment list may be marked as a hole or not. *)
type hole = Hole | Not_hole

(* A [subpath] is either closed or open. *)
type closure = Closed | Open

(* A [subpath] is the pair of a hole and a list of segments. *)
type subpath = hole * closure * segment list

(* A path is made from a number of subpaths. *)
type path = winding_rule * subpath list

type tiling = Tiling

type function_shading =
  {funshading_domain : float * float * float * float;
   funshading_matrix : Pdftransform.transform_matrix;
   funshading_function : Pdffun.t}

type radial_shading =
  {radialshading_coords : float * float * float * float * float * float;
   radialshading_domain : float * float;
   radialshading_function : Pdffun.t list;
   radialshading_extend : bool * bool}

type axial_shading =
  {axialshading_coords : float * float * float * float;
   axialshading_domain : float * float;
   axialshading_function : Pdffun.t list;
   axialshading_extend : bool * bool}

type shading_kind =
 | FunctionShading of function_shading
 | AxialShading of axial_shading
 | RadialShading of radial_shading
 | FreeFormGouraudShading
 | LatticeFormGouraudShading
 | CoonsPatchMesh
 | TensorProductPatchMesh

type shading =
 {shading_colourspace : Pdf.pdfobject;
  shading_background : Pdf.pdfobject option;
  shading_bbox : Pdf.pdfobject option;
  shading_antialias : bool;
  shading_matrix : Pdftransform.transform_matrix;
  shading_extgstate : Pdf.pdfobject;
  shading : shading_kind}

type pattern =
  | ColouredTilingPattern of tiling
  | UncolouredTilingPattern of tiling
  | ShadingPattern of shading

type colvals =
  | Floats of float list
  | Named of (string * float list)
  | Pattern of pattern

let rec string_of_colvals = function
  | Floats fs ->
      "Floats " ^ fold_left ( ^ ) "" (map (function x -> string_of_float x ^ " ") fs)
  | Named (n, fs) ->
      "Named " ^ n ^ " " ^ string_of_colvals (Floats fs)
  | Pattern p ->
      "Pattern"

type objectclass =
  | PathObject
  | TextObject
  | ClippingPathObject
  | PageDescriptionLevel
  | ShadingObject
  | InlineImageObject
  | ExternalObject

let string_of_objectclass = function
  | PathObject -> "PathObject"
  | TextObject -> "TextObject"
  | ClippingPathObject -> "ClippingPathObject"
  | PageDescriptionLevel -> "PageDescriptionLevel"
  | ShadingObject -> "ShadingObject"
  | InlineImageObject -> "InlineImageObject"
  | ExternalObject -> "ExternalObject"

type transparency_attributes =
  {fill_transparency : float;
   line_transparency : float}

type path_attributes =
  {path_transform : Pdftransform.transform_matrix;
   path_fill : (Pdfspace.t * colvals) option;
   path_line : (Pdfspace.t * colvals) option;
   path_linewidth : float;
   path_joinstyle : int;
   path_capstyle : int;
   path_dash : float list * float;
   path_mitrelimit : float;
   path_transparency : transparency_attributes;
   path_intent : string}

type text_attributes =
  {textmode : int}

type textblock_attributes =
  {textblock_transform : Pdftransform.transform_matrix}

type textblock =
  text_attributes * Pdfops.t

type softmask_subtype =
  Alpha | Luminosity

type transparency_group =
  {tr_group_colourspace : Pdf.pdfobject option; (* FIXME: This should be colourspace *)
   isolated : bool;
   knockout : bool;
   tr_graphic : t}

and softmask =
  {softmask_subtype : softmask_subtype;
   transparency_group : transparency_group;
   softmask_bbox : float * float * float * float;
   backdrop : float list option;
   softmask_transfer : Pdffun.t option}

and image_attributes =
  {image_transform : Pdftransform.transform_matrix;
   image_transparency : float; (* The /ca value *)
   image_softmask : softmask option}

and fontname = string * Pdf.pdfobject (*r Name, font *)

(* The main type for a graphic. It must be kept paired with the PDF it comes
from, since it will reference objects (fonts, images etc) in that PDF. *)
and graphic_elt =
  | Path of (path * path_attributes)
  | Text of textblock list * textblock_attributes
  | MCPoint of string
  | MCPointProperties of string * Pdf.pdfobject 
  | MCSection of string * graphic_elt list
  | MCSectionProperties of string * Pdf.pdfobject * graphic_elt list
  | Image of image_attributes * int (* object number *)
  | GraphicInlineImage of Pdf.pdfobject * bytes * Pdftransform.transform_matrix
  | Clip of path * graphic_elt list
  | Shading of path option * shading * Pdftransform.transform_matrix

and t =
  {elements : graphic_elt list; (* Page content *)
   fonts : fontname list; (* Fonts *)
   resources : Pdf.pdfobject} (* Anything else in /Resources *)

(* Calculate the bounding box (xmin, xmax, ymin, ymax) of a graphic. *)
let bbox_of_segment = function
  | Straight ((x1, y1), (x2, y2)) ->
      fmin x1 x2, fmax x1 x2, fmin y1 y2, fmax y1 y2
  | Bezier ((x1, y1), (x2, y2), (x3, y3), (x4, y4)) ->
      fmin (fmin x1 x2) (fmin x3 x4), fmax (fmax x1 x2) (fmax x3 x4),
      fmin (fmin y1 y2) (fmin y3 y4), fmax (fmax y1 y2) (fmax y3 y4)

let bbox_of_path (_, subpaths) =
  let segments =
    flatten (map (function (_, _, l) -> l) subpaths)
  in
    fold_left
      box_union_float
      (max_float, min_float, max_float, min_float)
      (map bbox_of_segment segments)

let rec bbox_of_graphic_inner (xmin, xmax, ymin, ymax) = function
  | [] -> xmin, xmax, ymin, ymax
  | (Path (p, _) | Clip (p, _))::t ->
      bbox_of_graphic_inner
        (box_union_float (xmin, xmax, ymin, ymax) (bbox_of_path p)) t
  | h::t -> bbox_of_graphic_inner (xmin, xmax, ymin, ymax) t

let bbox_of_graphic graphic =
  bbox_of_graphic_inner
    (max_float, min_float, max_float, min_float)
    graphic.elements

(* For debug purposes, build a string of a graphic. *)
let string_of_segment = function
  | Straight ((ax, ay), (bx, by)) ->
      Printf.sprintf "Straight line: (%f, %f) --> (%f, %f)\n" ax ay bx by
  | Bezier ((ax, ay), (bx, by), (cx, cy), (dx, dy)) ->
      Printf.sprintf
        "Bezier curve: (%f, %f) --> (%f, %f) --> (%f, %f) --> (%f, %f)\n"
        ax ay bx by cx cy dx dy

let string_of_subpath (h, o ,segments) =
  Printf.sprintf "Hole: %b, Open: %b, segments:%s\n"
    (h = Hole) (o = Open) (fold_left ( ^ ) "" (map string_of_segment segments))

let string_of_path (windingrule, subpaths) =
  Printf.sprintf "%s %s"
    (match windingrule with
    | EvenOdd -> "Even-odd\n"
    | NonZero -> "Non-zero\n")
    (fold_left ( ^ ) "" (map string_of_subpath subpaths))

let string_of_textblock (st, op) =
  "TEXTPIECE: " ^ Pdfops.string_of_op op ^ "\n"

let string_of_font (f, i) = f ^ " " ^ Pdfwrite.string_of_pdf i ^ "\n"

let string_of_colvals = function
  | Floats l -> Printf.sprintf "Floats (%i)" (length l)
  | Named (s, fl) -> Printf.sprintf "Named %s, Floats (%i)" s (length fl)
  | Pattern _ -> Printf.sprintf "Pattern"

let string_of_attributes a =
  let line =
    match a.path_line with
    | None -> "none"
    | Some (cs, vals) ->
        Printf.sprintf "line colourspace is %s, %s vals"
          (Pdfspace.string_of_colourspace cs)
          (string_of_colvals vals)
  and fill =
    match a.path_fill with
    | None -> "none"
    | Some (cs, vals) ->
        Printf.sprintf "fill colourspace is %s, %s vals"
          (Pdfspace.string_of_colourspace cs)
          (string_of_colvals vals)
  in
    line ^ "\n" ^ fill ^ "\n"

let rec string_of_graphic_elt = function
  | MCSection (n, g) ->
      Printf.sprintf "Marked content section %s...\n" n ^ "BEGIN\n" ^
      (fold_left ( ^ ) "" (map string_of_graphic_elt g))
      ^ "\nEND Marked content section\n"
  | MCSectionProperties (n, d, g) ->
      Printf.sprintf "Marked content section %s with properties %s...\n" n
      (Pdfwrite.string_of_pdf d) 
      ^ "BEGIN\n" ^
      (fold_left ( ^ ) "" (map string_of_graphic_elt g))
      ^ "\nEND Marked content section\n"
  | MCPoint n ->
      Printf.sprintf "Marked content point %s...\n" n
  | MCPointProperties (n, d) ->
      Printf.sprintf "Marked content point %s with properties %s...\n" n (Pdfwrite.string_of_pdf d)
  | Path (p, attributes) -> Printf.sprintf "Path: %s\nAttributes\n%s\n" (string_of_path p) (string_of_attributes attributes)
  | Text (ts, attr) ->
      "-----BEGIN TEXT - fonts:\n" ^
      fold_left ( ^ ) "" (map string_of_textblock ts) ^
      "-----END TEXT\n"
  | Image (tr, x) ->
      "Image " ^ string_of_int x ^ "\n"
  | GraphicInlineImage _ ->
      "Inline image\n"
  | Clip (p, g) ->
      "Clipview: path = " ^ string_of_path p ^ "\ngraphic is " ^
      fold_left ( ^ ) "" (map string_of_graphic_elt g)
  | Shading (clip, shading, tr) ->
      "Shading\n"

and string_of_graphic g =
  "Elements:\n" ^
  fold_left ( ^ ) "" (map string_of_graphic_elt g.elements) ^ 
  "Fonts:\n" ^
  fold_left ( ^ ) ""
    (map
      (fun (name, obj) -> name ^ " "  ^ Pdfwrite.string_of_pdf obj)
       g.fonts) ^
  "Resources:\n" ^
  Pdfwrite.string_of_pdf g.resources

type state =
  {mutable objectclass : objectclass; (*r Not strictly part of the state, but fits here. *)
   mutable clip : path option; (*r Ditto - stores a clipping path which is to be invoked on the next path operation. *)
   mutable intent : string;
   mutable fill : colvals;
   mutable linewidth : float;
   mutable line : colvals;
   mutable mitrelimit : float;
   mutable joinstyle : int;
   mutable capstyle : int;
   mutable colourspace_stroke : Pdfspace.t;
   mutable colourspace_nonstroke : Pdfspace.t;
   mutable dash : float list * float;
   mutable flatness : int;
   mutable transform : Pdftransform.transform_matrix;
   mutable extra_transform : Pdftransform.transform_matrix;
   mutable text_transform : Pdftransform.transform_matrix;
   mutable text_line_transform : Pdftransform.transform_matrix;
   mutable opacity_stroke : float;
   mutable opacity_nonstroke : float;
   mutable character_spacing : float;
   mutable word_spacing : float;
   mutable scale : float;
   mutable leading : float;
   mutable font_and_size : (string * float) option;
   mutable font_render : int;
   mutable font_rise : float;
   mutable blendmode : int;
   mutable softmask : softmask option;
   mutable in_xobject : int;
   mutable opdo_matrix : Pdftransform.transform_matrix}

let default_state () =
  {objectclass = PageDescriptionLevel;
   clip = None;
   intent = "/RelativeColorimetric";
   fill = Floats [1.];
   linewidth = 1.;
   line = Floats [1.];
   mitrelimit = 10.;
   joinstyle = 0;
   capstyle = 0;
   colourspace_stroke = Pdfspace.DeviceGray;
   colourspace_nonstroke = Pdfspace.DeviceGray;
   dash = [], 0.;
   flatness = 0;
   transform = Pdftransform.i_matrix;
   extra_transform = Pdftransform.i_matrix;
   text_transform = Pdftransform.i_matrix;
   text_line_transform = Pdftransform.i_matrix;
   opacity_stroke = 1.;
   opacity_nonstroke = 1.;
   character_spacing = 0.;
   word_spacing = 0.;
   scale = 100.;
   leading = 0.;
   font_and_size = None; (*r No initial value. *)
   font_render = 0;
   font_rise = 0.;
   blendmode = 1;
   softmask = None;
   in_xobject = 0;
   opdo_matrix = Pdftransform.i_matrix}

let state = ref (default_state ())

let string_of_state s =
  (*i "Object class: " ^ string_of_objectclass s.objectclass ^ "\n" ^ i*)
  "Stroke Colourspace: " ^ Pdfspace.string_of_colourspace s.colourspace_stroke ^ "\n" ^
  "Nonstroke Colourspace: " ^ Pdfspace.string_of_colourspace s.colourspace_nonstroke ^ "\n" ^
  "Stroke colours: " ^ string_of_colvals s.line ^ "\n" ^ 
  "NonStroke colours: " ^ string_of_colvals s.fill ^ "\n"

let path_attributes_fill_and_stroke () =
  {path_transform = (!state).transform;
   path_fill = Some ((!state).colourspace_nonstroke, (!state).fill);
   path_line = Some ((!state).colourspace_stroke, (!state).line);
   path_linewidth = (!state).linewidth;
   path_joinstyle = (!state).joinstyle;
   path_capstyle = (!state).capstyle;
   path_dash = (!state).dash;
   path_mitrelimit = (!state).mitrelimit;
   path_transparency =
     {fill_transparency = (!state).opacity_nonstroke;
      line_transparency = (!state).opacity_stroke};
   path_intent = (!state).intent}

let path_attributes_fill () =
  {path_transform = (!state).transform;
   path_fill = Some ((!state).colourspace_nonstroke, (!state).fill);
   path_line = None;
   path_linewidth = (!state).linewidth;
   path_joinstyle = (!state).joinstyle;
   path_capstyle = (!state).capstyle;
   path_dash = (!state).dash;
   path_mitrelimit = (!state).mitrelimit;
   path_transparency =
     {fill_transparency = (!state).opacity_nonstroke;
      line_transparency = 1.};
   path_intent = (!state).intent}

let path_attributes_stroke () =
  {path_transform = (!state).transform;
   path_fill = None;
   path_line = Some ((!state).colourspace_stroke, (!state).line);
   path_linewidth = (!state).linewidth;
   path_joinstyle = (!state).joinstyle;
   path_capstyle = (!state).capstyle;
   path_dash = (!state).dash;
   path_mitrelimit = (!state).mitrelimit;
   path_transparency =
     {fill_transparency = 1.;
      line_transparency = (!state).opacity_stroke};
   path_intent = (!state).intent}

let textstate () =
  {textmode = 0}

let nonzero = EvenOdd

let rec initial_colour pdf resources = function
  | Pdf.Name "/DeviceGray"
  | Pdf.Array (Pdf.Name "/CalGray"::_) ->
      Floats [0.]
  | Pdf.Name "/DeviceRGB"
  | Pdf.Array (Pdf.Name "/CalRGB"::_) ->
      Floats [0.; 0.; 0.]
  | Pdf.Name "/DeviceCMYK" ->
      Floats [0.; 0.; 0.; 1.]
  | Pdf.Name "/Pattern"
  | Pdf.Array [Pdf.Name "/Pattern"] ->
      Floats [0.]
  | Pdf.Array elts as cs ->
      begin match elts with
        | [Pdf.Name "/ICCBased"; iccstream] ->
             begin match Pdf.lookup_direct pdf "/Alternate" iccstream with
             | Some space -> initial_colour pdf resources space
             | None ->
                 begin match Pdf.lookup_direct pdf "/N" iccstream with
                 | Some (Pdf.Integer 1) -> Floats [0.]
                 | Some (Pdf.Integer 3) -> Floats [0.; 0.; 0.]
                 | Some (Pdf.Integer 4) -> Floats [0.; 0.; 0.; 0.]
                 | _ -> raise (Pdf.PDFError "Bad ICCBased Alternate")
                 end
             end
        | Pdf.Name "/DeviceN"::_::alternate::_ 
        | [Pdf.Name "/Separation"; _; alternate; _] ->
            initial_colour pdf resources alternate
        | [Pdf.Name "/Pattern"; alternate] ->
            initial_colour pdf resources alternate
        | _ -> Pdfe.log (Printf.sprintf "%s\n" (Pdfwrite.string_of_pdf cs)); raise (Pdf.PDFError "Unknown colourspace A")
      end
  | Pdf.Indirect _ as indirect ->
      initial_colour pdf resources (Pdf.direct pdf indirect)
  | _ -> raise (Pdf.PDFError "Unknown colourspace B")

(* PartialPath (sp, cp, p, s) is starting point [sp], current point [cp] the
partial segment list [p], subpath [s] and graphic [g]. *)
type partial =
  | NoPartial
  | PartialText of textblock list
  | PartialPath of fpoint * fpoint * segment list * subpath list 

(* g is a [group_transparency] xobject *)
let rec read_transparency_group pdf g =
  let group =
    match Pdf.lookup_direct pdf "/Group" g with
    | Some gr -> gr
    | None -> raise (Pdf.PDFError "Pdfgraphics.read_transparency_group: no /Group found")
  in
    let colourspace =
      Pdf.lookup_direct pdf "/CS" group
    and isolated =
      match Pdf.lookup_direct pdf "/I" group with
      | Some (Pdf.Boolean b) -> b
      | _ -> false
    and knockout =
      match Pdf.lookup_direct pdf "/K" group with
      | Some (Pdf.Boolean b) -> b
      | _ -> false
    and graphic =
      let fakepage =
        let resources =
          match Pdf.lookup_direct pdf "/Resources" g with
          | Some (Pdf.Dictionary d) -> Pdf.Dictionary d
          | _ -> Pdf.Dictionary []
        and contents =
          [g]
        in
          {Pdfpage.content = contents;
           Pdfpage.mediabox = Pdf.Null;
           Pdfpage.resources = resources;
           Pdfpage.rotate = Pdfpage.Rotate0;
           Pdfpage.rest = Pdf.Dictionary []}
      in
        graphic_of_page pdf fakepage
    and a, b, c, d =
      Pdf.parse_rectangle pdf (Pdf.lookup_fail "no bbox" pdf "/BBox" g)
    in
      {tr_group_colourspace = colourspace;
       isolated = isolated;
       knockout = knockout;
       tr_graphic = graphic}, a, b, c, d

and read_soft_mask pdf mask =
  match
    match Pdf.lookup_direct pdf "/S" mask with
    | Some (Pdf.Name "/Alpha") -> Some Alpha
    | Some (Pdf.Name "/Luminosity") -> Some Luminosity
    | _ -> None
  with
  | None -> None
  | Some subtype ->
      let transparency_group, a, b, c, d =
        match Pdf.lookup_direct pdf "/G" mask with
        | Some g -> read_transparency_group pdf g
        | None -> raise (Pdf.PDFError "Pdfgraphics.transparency group not found in soft mask")
      and backdrop =
        match Pdf.lookup_direct pdf "/BC" mask with
        | Some (Pdf.Array nums) -> Some (map (Pdf.getnum pdf) nums)
        | _ -> None
      and transfer =
        match Pdf.lookup_direct pdf "/TR" mask with
        | Some (Pdf.Dictionary d) ->
            Some (Pdffun.parse_function pdf (Pdf.Dictionary d))
        | _ -> None
      in
        Some
          {softmask_subtype = subtype;
           transparency_group = transparency_group;
           backdrop =  backdrop;
           softmask_transfer = transfer;
           softmask_bbox = (a, b, c, d)}

and update_graphics_state_from_dict pdf resources gdict =
  begin match Pdf.lookup_direct pdf "/SMask" gdict with
  | Some softmask -> (!state).softmask <- read_soft_mask pdf softmask
  | None -> ()
  end;
  begin match Pdf.lookup_direct pdf "/CA" gdict with
  | Some (Pdf.Real o) -> (!state).opacity_stroke <- o
  | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/ca" gdict with
  | Some (Pdf.Real o) -> (!state).opacity_nonstroke <- o
  | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/BM" gdict with
  | Some (Pdf.Name n)
  | Some (Pdf.Array (Pdf.Name n::_)) ->
      (!state).blendmode <- 0 (* FIXME: Do properly *)
  | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/LW" gdict with
    | Some (Pdf.Integer width) ->
        (!state).linewidth <- float width
    | Some (Pdf.Real width) ->
        (!state).linewidth <- width
    | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/LC" gdict with
    | Some (Pdf.Integer style) ->
        (!state).capstyle <- style
    | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/LC" gdict with
    | Some (Pdf.Integer join) ->
        (!state).joinstyle <- join
    | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/ML" gdict with
    | Some (Pdf.Integer limit) ->
        (!state).mitrelimit <- float limit
    | Some (Pdf.Real limit) ->
        (!state).mitrelimit <- limit
    | _ -> ()
  end;
  begin match Pdf.lookup_direct pdf "/D" gdict with
  | Some (Pdf.Array [Pdf.Array dashes; phase]) ->
      let dashnums, phase =
        map
          (function
           | (Pdf.Integer n) -> float n
           | (Pdf.Real n) -> n
           | _ -> raise (Pdf.PDFError "Malformed dash."))
          dashes,
        match phase with
        | Pdf.Integer phase -> float phase
        | Pdf.Real phase -> phase
        | _ -> raise (Pdf.PDFError "Malformed dash phase.")
      in
        (!state).dash <- dashnums, phase
  | _ -> ()
  end

and statestack : state list ref = ref []

and copystate () =
  {!state with fill = (!state).fill}

and push_statestack () =
  (*i Printf.printf "push_statestack\n"; i*)
  statestack =| copystate ()

and pop_statestack () =
  (*i Printf.printf "pop_statestack: %i items in stack before pop\n" (length !statestack);
  Printf.printf "Before pop_statestack, line and fill spaces are %s and %s\n"
  (Pdfspace.string_of_colourspace (!state).colourspace_stroke) (Pdfspace.string_of_colourspace (!state).colourspace_nonstroke);  I*)
  begin match !statestack with
  | [] -> raise (Pdf.PDFError "Unbalanced q/Q Ops")
  | h::t -> statestack := t; state := h
  end(*i ;
  Printf.printf "After pop_statestack, line and fill spaces are %s and %s\n"
  (Pdfspace.string_of_colourspace (!state).colourspace_stroke) (Pdfspace.string_of_colourspace (!state).colourspace_nonstroke)  i*)

and read_tiling_pattern _ =
  ColouredTilingPattern Tiling

and read_function_shading pdf shading =
  let domain =
    match Pdf.lookup_direct pdf "/Domain" shading with
    | Some (Pdf.Array [a; b; c; d]) -> Pdf.getnum pdf a, Pdf.getnum pdf b, Pdf.getnum pdf c, Pdf.getnum pdf d
    | _ -> 0., 1., 0., 1.
  and matrix =
    Pdf.parse_matrix pdf "/Matrix" shading
  and func =
    Pdf.lookup_fail "No function found" pdf "/Function" shading
  in
    FunctionShading
      {funshading_domain = domain;
       funshading_matrix = matrix;
       funshading_function = Pdffun.parse_function pdf func}

and read_radial_shading pdf shading =
  let coords =
    match Pdf.lookup_direct pdf "/Coords" shading with
    | Some (Pdf.Array [a; b; c; d; e; f]) ->
        Pdf.getnum pdf a, Pdf.getnum pdf b, Pdf.getnum pdf c, Pdf.getnum pdf d, Pdf.getnum pdf e, Pdf.getnum pdf f
    | _ -> raise (Pdf.PDFError "Pdfgraphics.read_radial_shading: no coords in radial shading")
  and domain =
    match Pdf.lookup_direct pdf "/Domain" shading with
    | Some (Pdf.Array [a; b]) -> Pdf.getnum pdf a, Pdf.getnum pdf b
    | _ -> 0., 1.
  and func =
    match Pdf.lookup_direct pdf "/Function" shading with
    | Some (Pdf.Array fs) -> map (Pdffun.parse_function pdf) fs
    | Some f -> [Pdffun.parse_function pdf f]
    | _ -> raise (Pdf.PDFError "Pdfgraphics.read_radial_shading: no function in radial shading")
  and extend =
    match Pdf.lookup_direct pdf "/Extend" shading with
    | Some (Pdf.Array [Pdf.Boolean a; Pdf.Boolean b]) -> a, b
    | _ -> false, false
  in
    RadialShading
      {radialshading_coords = coords;
       radialshading_domain = domain;
       radialshading_function = func;
       radialshading_extend = extend}

and read_axial_shading pdf shading =
  let coords =
    match Pdf.lookup_direct pdf "/Coords" shading with
    | Some (Pdf.Array [a; b; c; d]) ->
        Pdf.getnum pdf a, Pdf.getnum pdf b, Pdf.getnum pdf c, Pdf.getnum pdf d
    | _ -> raise (Pdf.PDFError "Pdfgraphics.read_axial_shading: no coords in radial shading")
  and domain =
    match Pdf.lookup_direct pdf "/Domain" shading with
    | Some (Pdf.Array [a; b]) -> Pdf.getnum pdf a, Pdf.getnum pdf b
    | _ -> 0., 1.
  and func =
    match Pdf.lookup_direct pdf "/Function" shading with
    | Some (Pdf.Array fs) -> map (Pdffun.parse_function pdf) fs
    | Some f -> [Pdffun.parse_function pdf f]
    | _ -> raise (Pdf.PDFError "Pdfgraphics.read_axial_shading: no function in radial shading")
  and extend =
    match Pdf.lookup_direct pdf "/Extend" shading with
    | Some (Pdf.Array [Pdf.Boolean a; Pdf.Boolean b]) -> a, b
    | _ -> false, false
  in
    AxialShading
      {axialshading_coords = coords;
       axialshading_domain = domain;
       axialshading_function = func;
       axialshading_extend = extend}

(* Read a shading pattern *)
and read_shading pdf matrix extgstate shading =
  let colourspace =
    Pdf.lookup_fail "No colourspace in shading" pdf "/ColorSpace" shading
  and background =
    Pdf.lookup_direct pdf "/Background" shading
  and bbox =
    Pdf.lookup_direct pdf "/BBox" shading
  and antialias =
    match Pdf.lookup_direct pdf "/BBox" shading with
    | Some (Pdf.Boolean true) -> true
    | _ -> false
  in
    let shading =
      match Pdf.lookup_fail "no /ShadingType" pdf "/ShadingType" shading with
      | Pdf.Integer 1 -> read_function_shading pdf shading
      | Pdf.Integer 3 -> read_radial_shading pdf shading
      | Pdf.Integer 2 -> read_axial_shading pdf shading
      | Pdf.Integer 4 -> FreeFormGouraudShading
      | Pdf.Integer 5 -> LatticeFormGouraudShading
      | Pdf.Integer 6 -> CoonsPatchMesh
      | Pdf.Integer 7 -> TensorProductPatchMesh
      | _ -> raise (Pdf.PDFError "Pdfgraphics.unknown shadingtype")
    in
      {shading_colourspace = colourspace;
       shading_background = background;
       shading_bbox = bbox;
       shading_antialias = antialias;
       shading_matrix = matrix;
       shading_extgstate = extgstate;
       shading = shading}

and read_shading_pattern pdf p =
  let matrix = Pdf.parse_matrix pdf "/Matrix" p
  and extgstate =
    match Pdf.lookup_direct pdf "/ExtGState" p with
    | Some (Pdf.Dictionary _ as d) -> d
    | _ -> Pdf.Dictionary []
  in
    match Pdf.lookup_direct pdf "/Shading" p with
    | Some shading ->
        ShadingPattern (read_shading pdf matrix extgstate shading)
    | _ ->
        raise (Pdf.PDFError "No shading dictionary")

and read_pattern pdf page name =
  match Pdf.lookup_direct pdf "/Pattern" page.Pdfpage.resources with
  | None -> raise (Pdf.PDFError "No pattern dictionary")
  | Some patterndict ->
      match Pdf.lookup_direct pdf name patterndict with
      | None -> raise (Pdf.PDFError "Pattern not found")
      | Some pattern ->
          match Pdf.lookup_direct pdf "/PatternType" pattern with
          | Some (Pdf.Integer 1) ->
               read_tiling_pattern pattern
          | Some (Pdf.Integer 2) ->
               read_shading_pattern pdf pattern
          | _ -> raise (Pdf.PDFError "unknown pattern")

and process_op pdf page (partial, graphic) op =
  let ret = (partial, graphic) in
  (*i flprint (string_of_state !state); *)
  (*i flprint (Pdfpages.string_of_op op ^ "\n"); i*)
  match op with
  | Pdfops.Op_W ->
      (* Move the current partial path into Clip, and return *)
      begin match partial with
      | PartialPath (_, _, segments, subpaths) ->
          if segments = [] && subpaths = [] then ret else
            let path =
              if segments <> []
                then (Not_hole, Closed, rev segments)::subpaths
                else subpaths
            in
              (!state).clip <- Some (NonZero, path); ret
      | _ -> ret
      end
      (* FIXME: In NextClip needs to support possibly several clips, since we can do W n W n W n f, for instance? *)
  | Pdfops.Op_W' ->
      begin match partial with
      | PartialPath (_, _, segments, subpaths) ->
          if segments = [] && subpaths = [] then ret else
            let path =
              if segments <> []
                then (Not_hole, Closed, rev segments)::subpaths
                else subpaths
            in
              (!state).clip <- Some (EvenOdd, path); ret
      | _ -> ret
      end
  | Pdfops.InlineImage (dict, data) ->
      (NoPartial, GraphicInlineImage (dict, data, (!state).transform)::graphic)
  | Pdfops.Op_MP name ->
      begin match (!state).objectclass with
      | PageDescriptionLevel -> (NoPartial, MCPoint name::graphic)
      | TextObject -> ret (* FIXME -- Add it to the text partial. *)
      | _ -> ret (* Invalid, silently drop *)
      end
  | Pdfops.Op_DP (name, properties) ->
      begin match (!state).objectclass with
      | PageDescriptionLevel ->
          (NoPartial, MCPointProperties (name, properties)::graphic)
      | TextObject -> ret (* FIXME -- Add it to the text partial. *)
      | _ -> ret (* Invalid, silently drop *)
      end
  | Pdfops.Op_BX | Pdfops.Op_EX -> ret    
  | Pdfops.Op_ri n -> (!state).intent <- n; ret
  | Pdfops.Op_j j -> (!state).joinstyle <- j; ret
  | Pdfops.Op_J c -> (!state).capstyle <- c; ret
  | Pdfops.Op_w w -> (!state).linewidth <- w; ret
  | Pdfops.Op_M m -> (!state).mitrelimit <- m; ret
  | Pdfops.Op_q ->
      (*i flprint "Op_q\n"; i*)
      push_statestack ();
      ret
  | Pdfops.Op_Q ->
      (*i flprint "Op_Q\n"; i*)
      pop_statestack ();
      ret
  | Pdfops.Op_SC vals | Pdfops.Op_SCN vals ->
      (!state).line <- Floats vals;
      ret
  | Pdfops.Op_sc vals | Pdfops.Op_scn vals ->
      (!state).fill <- Floats vals;
      ret
  | Pdfops.Op_scnName (name, vals) ->
      begin match (!state).colourspace_nonstroke with
      | Pdfspace.Pattern | Pdfspace.PatternWithBaseColourspace _ ->
          begin try
            (!state).fill <- Pattern (read_pattern pdf page name);
            ret
          with
            _ -> ret
          end
      | _ -> 
          (!state).fill <- Named (name, vals);
          ret
      end
  | Pdfops.Op_SCNName (name, vals) ->
      begin match (!state).colourspace_stroke with
      | Pdfspace.Pattern | Pdfspace.PatternWithBaseColourspace _ ->
          begin try
            (!state).line <- Pattern (read_pattern pdf page name);
            ret
          with
            _ -> ret
          end
      | _ ->
          (!state).line <- Named (name, vals);
          ret
      end
  | Pdfops.Op_CS c ->
      (*i Printf.printf "Op_CS: %s\n" c; *)
      (!state).colourspace_nonstroke <- Pdfspace.read_colourspace pdf page.Pdfpage.resources (Pdf.Name c);
      ret
  | Pdfops.Op_cs c ->
      (*i flprint "Op_cs\n";
      Printf.printf "Pdfgraphics: Op_cs: %s\n" c; i*)
      (!state).colourspace_nonstroke <- Pdfspace.read_colourspace pdf page.Pdfpage.resources (Pdf.Name c);
      ret
  | Pdfops.Op_G gv ->
      (*i flprint "Op_G\n"; i*)
      (!state).colourspace_stroke <- Pdfspace.DeviceGray;
      (!state).line <- Floats [gv];
      ret
  | Pdfops.Op_g gv ->
      (*i flprint "Op_g\n"; i*)
      (!state).colourspace_nonstroke <- Pdfspace.DeviceGray;
      (!state).fill <- Floats [gv];
      ret
  | Pdfops.Op_RG (rv, gv, bv) ->
      (!state).colourspace_stroke <- Pdfspace.DeviceRGB;
      (!state).line <- Floats [rv; gv; bv];
      ret
  | Pdfops.Op_rg (rv, gv, bv) ->
      (!state).colourspace_nonstroke <- Pdfspace.DeviceRGB;
      (!state).fill <- Floats [rv; gv; bv];
      ret
  | Pdfops.Op_K (c, m, y, k) ->
      (!state).colourspace_stroke <- Pdfspace.DeviceCMYK;
      (!state).line <- Floats [c; y; m; k];
      ret
  | Pdfops.Op_k (c, m, y, k) ->
      (!state).colourspace_nonstroke <- Pdfspace.DeviceCMYK;
      (!state).fill <- Floats [c; y; m; k];
      ret
  | Pdfops.Op_gs name ->
      let ext_state_dict = Pdf.lookup_fail "Bad Op_gs" pdf "/ExtGState" page.Pdfpage.resources in
        let gdict = Pdf.lookup_fail "Bad Op_gs" pdf name ext_state_dict in
          update_graphics_state_from_dict pdf page.Pdfpage.resources gdict;
          ret
  | Pdfops.Op_m (x, y) ->
      (* Begin a new subpath. Get into path mode if not already there. If the last op was an
      [Op_m], it should have no effect. *)
      (!state).objectclass <- PathObject;
      begin match partial with
      | PartialPath (sp, cp, segs, subpaths) ->
          if segs = []
            then PartialPath ((x, y), (x, y), [], subpaths), graphic
            else PartialPath ((x, y), (x, y), [], (Not_hole, Open, rev segs)::subpaths), graphic
      | _ ->
          PartialPath ((x, y), (x, y), [], []), graphic
      end
  | Pdfops.Op_l (x, y) ->
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_l");
      begin match partial with 
      | PartialPath (sp, cp, segs, subpaths) ->
          PartialPath (sp, (x, y), Straight (cp, (x, y))::segs, subpaths), graphic
      | _ ->
          raise (Pdf.PDFError "Pdfgraphics: Op_l")
      end
  | Pdfops.Op_c (a, b, c, d, e, f) ->
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_c");
      begin match partial with 
      | PartialPath (sp, cp, segs, subpaths) ->
          let ep = (e, f) in
            let curve = Bezier (cp, (a, b), (c, d), ep) in
              PartialPath (sp, ep, curve::segs, subpaths), graphic
      | _ ->
          raise (Pdf.PDFError "Pdfgraphics: Op_c")
      end
  | Pdfops.Op_v (a, b, c, d) ->
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_v");
      begin match partial with 
      | PartialPath (sp, cp, segs, subpaths) ->
          let ep = (c, d) in
            let curve = Bezier (cp, cp, (a, b), ep) in
              PartialPath (sp, ep, curve::segs, subpaths), graphic
      | _ ->
          raise (Pdf.PDFError "Pdfgraphics: Op_v")
      end
  | Pdfops.Op_y (a, b, c, d) ->
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_y");
      begin match partial with 
      | PartialPath (sp, cp, segs, subpaths) ->
          let ep = (c, d) in
            let curve = Bezier (cp, (a, b), ep, ep) in
              PartialPath (sp, ep, curve::segs, subpaths), graphic
      | _ ->
          raise (Pdf.PDFError "Pdfgraphics: Op_y")
      end
  | Pdfops.Op_h ->
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_h - not in PathObject");
      begin match partial with 
      | PartialPath (sp, cp, segs, subpaths) ->
          PartialPath (sp, cp, [], (Not_hole, Closed, rev segs)::subpaths), graphic
      | _ ->
          raise (Pdf.PDFError "Pdfgraphics: Op_h - not a partial path")
      end
  | Pdfops.Op_s ->
      (* Close and stroke. Equivalent to h S *)
      process_ops pdf page ret [Pdfops.Op_h; Pdfops.Op_S]
  | Pdfops.Op_b ->
      (* Close, fill, stroke, nonzero. Equivalent to h B *)
      process_ops pdf page ret [Pdfops.Op_h; Pdfops.Op_B]
  | Pdfops.Op_b' ->
      (* Close, fill, stroke, evenodd. Equivalent to h B* *)
      process_ops pdf page ret [Pdfops.Op_h; Pdfops.Op_B']
  | Pdfops.Op_f | Pdfops.Op_F ->
      (* Close and Fill non-zero *)
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_f");
      let partial, graphic = process_op pdf page (partial, graphic) Pdfops.Op_h in
        (!state).objectclass <- PageDescriptionLevel;
        begin match partial with
        | PartialPath (sp, cp, segs, subpaths) ->
            (* segs is empty, due to [Op_h] *)
            PartialPath (sp, cp, [], []),
            Path ((NonZero, rev subpaths), path_attributes_fill ())::graphic
        | _ ->
           raise (Pdf.PDFError "Pdfgraphics: Op_f")
        end
  | Pdfops.Op_S ->
      (* Stroke *)
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_S");
      (!state).objectclass <- PageDescriptionLevel;
      begin match partial with
      | PartialPath (sp, cp, segs, subpaths) ->
          if segs = [] then
            PartialPath (sp, cp, [], []),
            Path ((EvenOdd, rev subpaths), path_attributes_stroke ())::graphic
          else
            PartialPath (sp, cp, [], []),
            Path ((EvenOdd, rev ((Not_hole, Open, rev segs)::subpaths)), path_attributes_stroke ())::graphic
      | _ ->
         raise (Pdf.PDFError "Pdfgraphics: Op_S")
      end
  | Pdfops.Op_B ->
      (* Fill and stroke, non-zero. *)
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_B");
      (!state).objectclass <- PageDescriptionLevel;
      begin match partial with
      | PartialPath (sp, cp, segs, subpaths) ->
          if segs = [] then
            PartialPath (sp, cp, [], []),
            Path ((NonZero, rev subpaths), path_attributes_fill_and_stroke ())::graphic
          else
            PartialPath (sp, cp, [], []),
            Path ((NonZero, rev ((Not_hole, Open, rev segs)::subpaths)), path_attributes_fill_and_stroke ())
            ::graphic
      | _ ->
        raise (Pdf.PDFError "Pdfgraphics: Op_B")
      end
  | Pdfops.Op_B' ->
      (* Fill and stroke, even-odd. *)
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_B*");
      let partial, graphic = process_op pdf page (partial, graphic) Pdfops.Op_h in
        (!state).objectclass <- PageDescriptionLevel;
        begin match partial with
        | PartialPath (sp, cp, segs, subpaths) ->
            if segs = [] then
              PartialPath (sp, cp, [], []),
              Path ((EvenOdd, rev subpaths), path_attributes_fill_and_stroke ())::graphic
            else
              PartialPath (sp, cp, [], []),
              Path ((EvenOdd, rev ((Not_hole, Open, rev segs)::subpaths)), path_attributes_fill_and_stroke ())
              ::graphic
        | _ ->
           raise (Pdf.PDFError "Pdfgraphics: Op_B*")
        end
  | Pdfops.Op_f' ->
      (* Fill, even-odd *)
      if (!state).objectclass <> PathObject then
        raise (Pdf.PDFError "Pdfgraphics: Op_f*");
      (!state).objectclass <- PageDescriptionLevel;
      begin match partial with
      | PartialPath (sp, cp, segs, subpaths) ->
          if segs = [] then
            PartialPath (sp, cp, [], []),
            Path ((EvenOdd, rev subpaths), path_attributes_fill ())::graphic
          else
            PartialPath (sp, cp, [], []),
            Path ((EvenOdd, rev ((Not_hole, Open, rev segs)::subpaths)), path_attributes_fill ())
            ::graphic
      | _ ->
         raise (Pdf.PDFError "Pdfgraphics: Op_f*")
      end
  | Pdfops.Op_n ->
      (* no-op *)
      (!state).objectclass <- PageDescriptionLevel;
      (* for now, until we support clipviews, clean up the polygon *)
      (NoPartial, graphic)
  | Pdfops.Op_re (x, y, w, h) ->
      (* Rectangle. *)
      let ops =
        [Pdfops.Op_m (x, y);
         Pdfops.Op_l (x +. w, y);
         Pdfops.Op_l (x +. w, y +. h);
         Pdfops.Op_l (x, y +. h);
         Pdfops.Op_h]
      in
        process_ops pdf page (partial, graphic) ops
  | Pdfops.Op_Do name ->
      begin match Pdf.lookup_direct pdf "/XObject" page.Pdfpage.resources with
      | Some d ->
          begin match Pdf.lookup_direct pdf name d with
          | Some xobj ->
              begin match Pdf.lookup_direct pdf "/Subtype" xobj with
              | Some (Pdf.Name "/Image") ->
                  let objnum =
                    match Pdf.find_indirect name d with
                    | None -> raise (Pdf.PDFError "image not found")
                    | Some i -> i
                  in
                    partial,
                    Image
                       ({image_transform = (!state).transform;
                         image_transparency = (!state).opacity_nonstroke;
                         image_softmask = (!state).softmask}
                        , objnum)::graphic
              | Some (Pdf.Name "/Form") ->
                  let elts = read_form_xobject pdf page xobj in
                    partial, rev elts @ graphic
              | _ -> raise (Pdf.PDFError "Unknown kind of xobject")
              end
          | _ -> raise (Pdf.PDFError "Unknown xobject")
          end
      | None -> raise (Pdf.PDFError "xobject not found")
      end
  | Pdfops.Op_cm tr ->
      (!state).transform <- Pdftransform.matrix_compose (!state).transform tr;
      ret
  |  (  Pdfops.Op_Tc _
      | Pdfops.Op_Tw _ 
      | Pdfops.Op_Tz _ 
      | Pdfops.Op_TL _ 
      | Pdfops.Op_Tf _ 
      | Pdfops.Op_Tr _ 
      | Pdfops.Op_Ts _ 
      | Pdfops.Op_Td _ 
      | Pdfops.Op_TD _ 
      | Pdfops.Op_Tm _ 
      | Pdfops.Op_T' 
      | Pdfops.Op_Tj _ 
      | Pdfops.Op_TJ _ 
      | Pdfops.Op_' _ 
      | Pdfops.Op_'' _ 
      | Pdfops.Op_d0 _ 
      | Pdfops.Op_d1 _) as op ->
       begin match partial with
       | PartialText t ->
           let st = textstate () in
             PartialText ((st, op)::t), graphic
       | _ ->
         (* If there's no partial text, this is an op affecting the text state but not in a text section. Such ops are allowed. FIXME: Deal with them properly - by ops altering the text state so this can be reflected in the initial state at the start of a text section *)
         ret
       end
  | Pdfops.Op_sh n ->
      let shading =
        let shadingdict = Pdf.lookup_fail "no /Shading" pdf "/Shading" page.Pdfpage.resources in
          let shading = Pdf.lookup_fail "named shading not found" pdf n shadingdict in
            read_shading pdf Pdftransform.i_matrix Pdf.Null shading
      and currentclip = (!state).clip in
        partial, Shading (currentclip, shading, (!state).transform)::graphic
  | Pdfops.Op_i flatness ->
      if flatness >= 0 && flatness <= 100 then (!state).flatness <- flatness;
      ret
  | Pdfops.Op_d (spec, phase) ->
      (!state).dash <- spec, phase;
      ret
  | Pdfops.Op_Unknown _ -> ret
  | _ -> Pdfe.log "Operator shouldn't appear at this place"; ret

and getuntil_matching_emc level prev = function
  | (Pdfops.Op_BMC _ | Pdfops.Op_BDC (_, _)) as h::t ->
      getuntil_matching_emc (level + 1) (h::prev) t
  | Pdfops.Op_EMC::t ->
      if level < 0
        then raise (Pdf.PDFError "Too many EMCs\n")
        else if level = 0
          then rev prev, t
          else getuntil_matching_emc (level - 1) (Pdfops.Op_EMC::prev) t
  | h::t -> getuntil_matching_emc level (h::prev) t
  | [] -> raise (Pdf.PDFError "Missing EMC\n")

and getuntil_matching_Q level prev = function
  | Pdfops.Op_q::t -> getuntil_matching_Q (level + 1) (Pdfops.Op_q::prev) t
  | Pdfops.Op_Q::t ->
      if level = 0
        then rev prev, Pdfops.Op_Q::t
        else getuntil_matching_Q (level - 1) (Pdfops.Op_Q::prev) t
  | [] -> rev prev, []
  | h::t -> getuntil_matching_Q level (h::prev) t

and process_ops pdf page (partial, graphic) ops =
  match ops with
  | [] -> partial, rev graphic
  | Pdfops.Op_n::t ->
      (* If there's a NextClip, select all operators within the scope of this
      clip. That is, all operators until an [Op_Q] which puts the stack level
      below the current level or the end of the stream, whichever comes first.*)
      begin match (!state).clip with
      | None ->
          process_ops pdf page (partial, graphic) t
      | Some path ->
          (* We process the operators concerned, putting them inside a Clip,
          and then proceed with the remaining operators (including any [Op_Q]).
          However, to deal with the case of overlapping pairs of marked content
          sections and q/Q pairs (which is allowed). Currently just chuck BDC we don't understand. *)
          let toq, rest = getuntil_matching_Q 0 [] t in
            let _, elts =
              process_ops pdf page (NoPartial, []) toq
            in
              process_ops pdf page (NoPartial, Clip (path, elts)::graphic) rest
      end
  | Pdfops.Op_BMC n::t ->
      (* FIXME: Marked content regions / q/Q pairs overlapping problem *)
      begin try
        let ops, rest = getuntil_matching_emc 0 [] t in
          let partial, graphic' = process_ops pdf page (partial, []) ops in
            process_ops pdf page (partial, MCSection (n, graphic')::graphic) rest
      with
        _ -> process_ops pdf page (partial, graphic) t
      end
  | Pdfops.Op_BDC (n, d)::t ->
      (* FIXME: Marked content regions / q/Q pairs overlapping problem *)
      begin try
        let ops, rest = getuntil_matching_emc 0 [] t in
          let partial, graphic' = process_ops pdf page (partial, []) ops in
            process_ops pdf page (partial, MCSectionProperties (n, d, graphic')::graphic) rest
      with
        _ -> process_ops pdf page (partial, graphic) t
      end
  | Pdfops.Op_BT::t ->
     (* Can't nest text sections, so just get to ET *)
     let textops, rest = cleavewhile (neq Pdfops.Op_ET) t in
       begin match rest with
       | Pdfops.Op_ET::_ | [] ->
          (* We allow blank in case of wrongly nested EMC / ET etc *)
          let more = tail_no_fail rest in
          (* We need to process the ops, and capture the text operations, but state changes inside
          text sections (e.g colour changes) have global effect, so need to keep the state *)
          (!state).objectclass <- TextObject;
          let partial, _ =
            process_ops pdf page (PartialText [], graphic) textops
          in
            begin match partial with
            | PartialText t ->
                let textblock =
                  Text (rev t, {textblock_transform = (!state).transform})
                in
                  process_ops pdf page (partial, textblock::graphic) (Pdfops.Op_ET::more)
            | _ -> raise (Pdf.PDFError "Bad operations in text block")
            end
       | _ ->
         (*i Printf.printf "textops: %s\n\n" (Pdfops.string_of_ops textops);
         Printf.printf "rest: %s\n\n" (Pdfops.string_of_ops rest); i*)
         raise (Pdf.PDFError "No Matching Op_ET")
       end
  | Pdfops.Op_ET::t ->
     (!state).objectclass <- PageDescriptionLevel;
     process_ops pdf page (partial, graphic) t
  | h::t -> process_ops pdf page (process_op pdf page (partial, graphic) h) t

(* Load the fonts as (name, pdfobject) pairs *)
and fonts_of_page pdf page =
  match Pdf.lookup_direct pdf "/Font" page.Pdfpage.resources with
  | Some (Pdf.Dictionary fs) -> fs
  | _ -> []

(* Find the operations of a form xobject. *)
and read_form_xobject pdf page pdfobject =
  let content = [Pdf.direct pdf pdfobject] in
    let pagedict =
      match Pdf.direct pdf page.Pdfpage.resources with
      | Pdf.Dictionary rs -> rs
      | _ -> []
    and xobjdict =
      match Pdf.direct pdf pdfobject with
      | Pdf.Stream {contents = (dict, _)} ->
          begin match Pdf.lookup_direct pdf "/Resources" dict with
          | Some (Pdf.Dictionary rs) -> rs
          | _ -> []
          end
      | _ -> raise (Pdf.PDFError "bad stream in read_form_xobject")
    in
      let total_resources =
        Pdf.Dictionary (mergedict pagedict xobjdict)
      in
        let fake_page =
          {Pdfpage.content = [];
           Pdfpage.mediabox = Pdf.Null;
           Pdfpage.resources = total_resources;
           Pdfpage.rotate = Pdfpage.Rotate0;
           Pdfpage.rest = Pdf.Dictionary []}
        in
          let _, graphic_elts =
              (process_ops pdf fake_page (NoPartial, [])
              (Pdfops.parse_operators pdf total_resources content))
          in
            graphic_elts

(* Main function - build a graphic from a page *)
and graphic_of_page pdf page =
  statestack := [];
  state := default_state ();
  if Pdfcrypt.is_encrypted pdf then
    raise (Pdf.PDFError "Pdfgraphics: File is encrypted")
  else
    begin
      let _, elts =
        let ops =
          Pdfops.parse_operators pdf page.Pdfpage.resources page.Pdfpage.content
        in
          process_ops pdf page (NoPartial, []) ops
      in
        {elements = elts;
         fonts = fonts_of_page pdf page;
         resources = page.Pdfpage.resources}
    end

let graphic_of_ops ops =
   graphic_of_page
     (Pdf.empty ())
     {(Pdfpage.blankpage Pdfpaper.a4) with
         Pdfpage.content =
           [Pdf.Stream {contents =
              (Pdf.Dictionary [], Pdf.Got (bytes_of_string (Pdfops.string_of_ops ops)))}]}

(* \section{Building a page from a graphic} *)

let int_of_shading_kind = function
  | FunctionShading _ -> 1
  | AxialShading _ -> 2
  | RadialShading _ -> 3
  | FreeFormGouraudShading -> 4
  | LatticeFormGouraudShading -> 5
  | CoonsPatchMesh -> 6
  | TensorProductPatchMesh -> 7

let entries_of_shading pdf s =
  match s.shading with
  | RadialShading r ->
      let coords =
        let a, b, c, d, e, f = r.radialshading_coords in
          Pdf.Array
            [Pdf.Real a; Pdf.Real b; Pdf.Real c; Pdf.Real d; Pdf.Real e; Pdf.Real f]
      and domain =
        let a, b = r.radialshading_domain in
          Pdf.Array
            [Pdf.Real a; Pdf.Real b]
      and funcnum =
        match r.radialshading_function with
        | [f] ->
           Pdf.addobj pdf (Pdffun.pdfobject_of_function pdf f)
        | funs ->
           Pdf.addobj pdf (Pdf.Array (map (Pdffun.pdfobject_of_function pdf) funs))
      and extend =
        Pdf.Array
          [Pdf.Boolean (fst r.radialshading_extend);
           Pdf.Boolean (snd r.radialshading_extend)]
      in
        ["/Coords", coords;
         "/Domain", domain;
         "/Function", Pdf.Indirect funcnum;
         "/Extend", extend]
  | AxialShading a ->
      let coords =
        let a, b, c, d = a.axialshading_coords in
          Pdf.Array
            [Pdf.Real a; Pdf.Real b; Pdf.Real c; Pdf.Real d]
      and domain =
        let a, b = a.axialshading_domain in
          Pdf.Array
            [Pdf.Real a; Pdf.Real b]
      and funcnum =
        match a.axialshading_function with
        | [f] ->
           Pdf.addobj pdf (Pdffun.pdfobject_of_function pdf f)
        | funs ->
           Pdf.addobj pdf (Pdf.Array (map (Pdffun.pdfobject_of_function pdf) funs))
      and extend =
        Pdf.Array
          [Pdf.Boolean (fst a.axialshading_extend);
           Pdf.Boolean (snd a.axialshading_extend)]
      in
        ["/Coords", coords;
         "/Domain", domain;
         "/Function", Pdf.Indirect funcnum;
         "/Extend", extend]
  | _ -> []

let shading_object_of_shading pdf s =
  let background =
    match s.shading_background with
    | None -> []
    | Some b -> ["/Background", b]
  and bbox =
    match s.shading_bbox with
    | None -> []
    | Some b -> ["/BBox", b]
  in
    Pdf.Dictionary
      (["/ShadingType", Pdf.Integer (int_of_shading_kind s.shading);
       "/ColorSpace", s.shading_colourspace;
       "/AntiAlias", Pdf.Boolean s.shading_antialias]
      @ background @ bbox @ entries_of_shading pdf s)

let pattern_object_of_pattern xobject_level opdo_matrix pdf = function
  | ShadingPattern s ->
      begin try
        let shading_matrix =
          if xobject_level > 0 then
            let inverted = Pdftransform.matrix_invert opdo_matrix in
              Pdftransform.matrix_compose inverted s.shading_matrix
          else
            s.shading_matrix
        in
          Pdf.Dictionary
            ["/Type", Pdf.Name "/Pattern";
             "/PatternType", Pdf.Integer 2;
             "/Shading", shading_object_of_shading pdf s;
             "/Matrix", Pdf.make_matrix shading_matrix]
      with
        Pdftransform.NonInvertable -> raise (Pdf.PDFError "Pdfgraphics.Bad pattern")
      end
  | _ ->
      Pdfe.log "Unknown pattern\n";
      Pdf.Dictionary []

(* Output a move and line/curve ops. *)
let ops_of_segs segs closure =
  let raw_seg_ops =
    map
      (function
       | Straight (_, (x, y)) -> Pdfops.Op_l (x, y)
       | Bezier (_, (bx, by), (cx, cy), (dx, dy)) -> Pdfops.Op_c (bx, by, cx, cy, dx, dy))
      segs
  and get_move = function
    | Straight ((x, y), _) | Bezier ((x, y), _, _, _) -> Pdfops.Op_m (x, y)
  in
    (* Predicate: Do we need to close this subpath? *)
    match segs with
    | [] -> []
    | h::_ -> get_move h::raw_seg_ops @ (if closure = Closed then [Pdfops.Op_h] else [])

let protect ops =
  [Pdfops.Op_q] @ ops @ [Pdfops.Op_Q]

let attribute_ops_of_path (_, a) =
  [Pdfops.Op_w a.path_linewidth;
   Pdfops.Op_J a.path_capstyle;
   Pdfops.Op_j a.path_joinstyle;
   begin match a.path_dash with (x, y) -> Pdfops.Op_d (x, y) end;
   Pdfops.Op_M a.path_mitrelimit;
   Pdfops.Op_ri a.path_intent]

let transform_ops_of_path (_, a) =
  [Pdfops.Op_cm a.path_transform]

let stroke_ops_of_path ((winding, _), a) =
  match winding, a.path_fill, a.path_line with
  | _, None, None -> Pdfops.Op_n
  | EvenOdd, Some _, Some _ -> Pdfops.Op_B'
  | EvenOdd, Some _, None -> Pdfops.Op_f'
  | NonZero, Some _, Some _ -> Pdfops.Op_B
  | NonZero, Some _, None -> Pdfops.Op_f
  | _, None, Some _ -> Pdfops.Op_S

let path_ops_of_path (_, subpaths) =
  flatten (map (fun (_, closure, segs) -> ops_of_segs segs closure) subpaths)

let ops_of_path pdf page (((winding, subpaths), a) as p) =
  (* Add a colourspace returning new resources and a new name, or return the name it's already held under. *)
  let name_of_colourspace cs resources =
    match cs with
    (*i | Pdf.Name (("/DeviceGray" | "/DeviceRGB" | "/DeviceCMYK" | "/Pattern") as str) -> resources, str i*)
    | Pdfspace.DeviceGray | Pdfspace.DeviceRGB | Pdfspace.DeviceCMYK | Pdfspace.Pattern -> resources, Pdfspace.string_of_colourspace cs
    | _ ->
      let existing_colourspacedict =
        match Pdf.lookup_direct pdf "/ColorSpace" resources with
        | Some ((Pdf.Dictionary _) as d) -> d
        | _ -> Pdf.Dictionary []
      in
        (* FIXME: For now, we just always create a new one. Must search to see if it's already there for efficiency. *)
        let name = Pdf.unique_key "cs" existing_colourspacedict in
          let newcolourspacedict = Pdf.add_dict_entry existing_colourspacedict name (Pdfspace.write_colourspace pdf cs) in
            Pdf.add_dict_entry resources "/ColorSpace" newcolourspacedict, name
  in
  let resources = page.Pdfpage.resources in
  let attribute_ops = attribute_ops_of_path p
  and transform = transform_ops_of_path p
  and stroke_op = stroke_ops_of_path p in
  let colours_stroke, resources =
    match a.path_line with
    | Some (cs, Floats vals) ->
        let resources', name = name_of_colourspace cs resources in
        [Pdfops.Op_CS name; Pdfops.Op_SCN vals], resources
    | Some (cs, Named (n, vals)) ->
        let resources', name = name_of_colourspace cs resources in
          [Pdfops.Op_CS name; Pdfops.Op_SCNName (n, vals)], resources'
    | _ -> [], resources
  in
  let colours_nonstroke, resources =
    match a.path_fill with
    | Some (cs, Floats vals) ->
        let resources', name = name_of_colourspace cs resources in
          [Pdfops.Op_cs name; Pdfops.Op_scn vals], resources'
    | Some (cs, Named (n, vals)) ->
        let resources', name = name_of_colourspace cs resources in
          [Pdfops.Op_cs name; Pdfops.Op_scnName (n, vals)], resources'
    | Some (_, Pattern p) ->
        (* Build /Pattern cs and reference to pattern, having built the
        pattern in the pattern dictionary *)
        let pattern = pattern_object_of_pattern (!state).in_xobject (!state).opdo_matrix pdf p in
         let resources, name =
           let existing_patterndict =
             match Pdf.lookup_direct pdf "/Pattern" resources with
             | Some ((Pdf.Dictionary _) as d) -> d
             | _ -> Pdf.Dictionary []
           in
             let name = Pdf.unique_key "pt" existing_patterndict in
               let newpatterndict = Pdf.add_dict_entry existing_patterndict name pattern in
                 Pdf.add_dict_entry page.Pdfpage.resources "/Pattern" newpatterndict, name
        in
          [Pdfops.Op_cs "/Pattern"; Pdfops.Op_scnName (name, [])], resources
    | _ -> [], resources
  in
  let gs, resources =
    if a.path_transparency.fill_transparency < 1. || a.path_transparency.line_transparency < 1.
      then
         let resources, name =
           let existing_extgstate =
             match Pdf.lookup_direct pdf "/ExtGState" resources with
             | Some ((Pdf.Dictionary _) as d) -> d
             | _ -> Pdf.Dictionary []
           in
             let name = Pdf.unique_key "gs" existing_extgstate
             and gsdict =
               Pdf.Dictionary
                 [("/ca", Pdf.Real a.path_transparency.fill_transparency);
                  ("/CA", Pdf.Real a.path_transparency.line_transparency)]
             in
               let new_extgstate = Pdf.add_dict_entry existing_extgstate name gsdict in
                 Pdf.add_dict_entry page.Pdfpage.resources "/ExtGState" new_extgstate, name
         in
            [Pdfops.Op_gs name], resources
      else
        [], resources
  in
   let path_ops = path_ops_of_path (winding, subpaths) in
     protect (gs @ transform @ attribute_ops @ colours_stroke @ colours_nonstroke @ path_ops @ [stroke_op]),
     resources

let ops_of_textstate st = []

let ops_of_textpiece (st, op) =
  ops_of_textstate st @ [op]

(* Upon entry to this, the transformation matrix is identity *)
let ops_of_text tr ops =
  protect ([Pdfops.Op_cm tr; Pdfops.Op_BT] @ (flatten <| map ops_of_textpiece ops) @ [Pdfops.Op_ET])

(* Transform a bounding box by a given matrix *)
let extreme_of_4 f a b c d =
  hd <| sort f [a; b; c; d]

let min_of_4 = extreme_of_4 compare

let max_of_4 = extreme_of_4 (fun a b -> ~-(compare a b))

let transform_bbox tr l b r t =
  let (x0, y0) = Pdftransform.transform_matrix tr (l, t)
  and (x1, y1) = Pdftransform.transform_matrix tr (l, b)
  and (x2, y2) = Pdftransform.transform_matrix tr (r, t)
  and (x3, y3) = Pdftransform.transform_matrix tr (r, b) in
    min_of_4 x0 x1 x2 x3,
    min_of_4 y0 y1 y2 y3,
    max_of_4 x0 x1 x2 x3,
    max_of_4 y0 y1 y2 y3

(* Build a transparency group xobject, add it to the pdf and return its object number *)
let rec pdfobject_of_transparency_group (a, b, c, d) pdf t =
  (!state).in_xobject <- (!state).in_xobject + 1;
  let r =
    let page = page_of_graphic pdf (0., 0., 0., 0.) t.tr_graphic
    and group_attributes =
      let cs =
        match t.tr_group_colourspace with 
        | None -> []
        | Some pdfobject -> ["/CS", pdfobject]
      in
        Pdf.Dictionary
          (["/Type", Pdf.Name "/Group";
            "/S", Pdf.Name "/Transparency";
            "/I", Pdf.Boolean t.isolated;
            "/K", Pdf.Boolean t.knockout] @ cs)
    in
      let extras =
        ["/Type", Pdf.Name "/XObject";
         "/Subtype", Pdf.Name "/Form";
         "/BBox", Pdf.Array [Pdf.Real a; Pdf.Real b; Pdf.Real c; Pdf.Real d];
         "/Resources", page.Pdfpage.resources;
         "/Group", group_attributes]
      in
        match page.Pdfpage.content with
        | Pdf.Stream ({contents = Pdf.Dictionary dict, Pdf.Got data})::_ ->
            Pdf.addobj pdf (Pdf.Stream ({contents = Pdf.Dictionary (extras @ dict), Pdf.Got data}))
        | _ -> raise (Pdf.PDFError "Pdfgraphics: Bad page content")
  in
    (!state).in_xobject <- (!state).in_xobject - 1;
    r

and pdfobject_of_softmask pdf m =
  let bc =
    match m.backdrop with
    | None -> []
    | Some fs -> ["/BC", Pdf.Array (map (function x -> Pdf.Real x) fs)]
  and tr =
    match m.softmask_transfer with
    | None -> []
    | Some f -> ["/TR", Pdffun.pdfobject_of_function pdf f]
  in
    Pdf.addobj pdf
      (Pdf.Dictionary
        (["/Type", Pdf.Name "/Mask";
         "/S", Pdf.Name (match m.softmask_subtype with Alpha -> "/Alpha" | Luminosity -> "/Luminosity");
         "/G", Pdf.Indirect (pdfobject_of_transparency_group m.softmask_bbox pdf m.transparency_group)]
        @ bc @ tr))

and ops_of_image pdf page (a, i) =
  (!state).opdo_matrix <- a.image_transform;
  let resources = page.Pdfpage.resources in
    let ops, resources =
      let opgs, resources =
        if a.image_transparency < 1. || a.image_softmask <> None
          then
             let resources, name =
               let existing_extgstate =
                 match Pdf.lookup_direct pdf "/ExtGState" page.Pdfpage.resources with
                 | Some ((Pdf.Dictionary _) as d) -> d
                 | _ -> Pdf.Dictionary []
               in
                 let name = Pdf.unique_key "gs" existing_extgstate
                 and gsdict =
                   let softmask =
                     match a.image_softmask with
                     | None -> []
                     | Some m -> ["/SMask", Pdf.Indirect (pdfobject_of_softmask pdf m)]
                   in
                     Pdf.Dictionary
                       ([("/ca", Pdf.Real a.image_transparency)] @ softmask)
                 in
                   let new_extgstate = Pdf.add_dict_entry existing_extgstate name gsdict in
                     Pdf.add_dict_entry resources "/ExtGState" new_extgstate, name
             in
                [Pdfops.Op_gs name], resources
          else
            [], resources
      in
        [Pdfops.Op_cm a.image_transform] @ opgs @ [Pdfops.Op_Do ("/Im" ^ string_of_int i)], resources
    in
      protect ops, resources

and ops_of_shading pdf page path shading transform =
  let resources', name =
    (* Add new entry to shading dictionary, return its name, new resources *)
    let existing_shadingdict =
      match Pdf.lookup_direct pdf "/Shading" page.Pdfpage.resources with
      | Some ((Pdf.Dictionary _) as d) -> d
      | _ -> Pdf.Dictionary []
    in
      let name = Pdf.unique_key "sh" existing_shadingdict
      and objnum = Pdf.addobj pdf (shading_object_of_shading pdf shading) in
        let shadingref = Pdf.Indirect objnum in
          let new_shadingdict = Pdf.add_dict_entry existing_shadingdict name shadingref in
            let r =
              Pdf.add_dict_entry page.Pdfpage.resources "/Shading" new_shadingdict
            in
              r, name
  in
    let ops =
      let pathops, clipops =
       match path with
       | None -> [], []
       | Some p ->
           path_ops_of_path p, [Pdfops.Op_W; Pdfops.Op_n] (* FIXME: Even-odd vs Non-Zero *)
      in
        pathops @ clipops @ [Pdfops.Op_cm transform; Pdfops.Op_sh name]
    in
      protect ops, resources'

and ops_of_graphic_acc pdf page oplists = function
  | [] ->
      flatten (rev oplists), page
  | Path p::t ->
      let ops, resources' = ops_of_path pdf page p in
        let page' = {page with Pdfpage.resources = resources'} in
          ops_of_graphic_acc pdf page' (ops::oplists) t
  | Image (attr, i)::t ->
      let ops, resources' = ops_of_image pdf page (attr, i) in
        let page' = {page with Pdfpage.resources = resources'} in
          ops_of_graphic_acc pdf page' (ops::oplists) t
  | Text (ts, {textblock_transform = tr})::t ->
      let ops = ops_of_text tr ts in
        ops_of_graphic_acc pdf page (ops::oplists) t
  | MCSection (n, graphic)::t ->
      let oplist, page' =
        ops_of_graphic_acc pdf page [] graphic
      in
        ops_of_graphic_acc pdf page' (([Pdfops.Op_BMC n] @ oplist @ [Pdfops.Op_EMC])::oplists) t
  | MCSectionProperties (n, d, graphic)::t ->
      let oplist, page' =
        ops_of_graphic_acc pdf page [] graphic
      in
        ops_of_graphic_acc pdf page' (([Pdfops.Op_BDC (n, d)] @ oplist @ [Pdfops.Op_EMC])::oplists) t
  | MCPoint n::t ->
      ops_of_graphic_acc pdf page ([Pdfops.Op_MP n]::oplists) t
  | MCPointProperties (n, d)::t ->
      ops_of_graphic_acc pdf page ([Pdfops.Op_DP (n, d)]::oplists) t
  | GraphicInlineImage (dict, data, tr)::t ->
      ops_of_graphic_acc pdf page (protect [Pdfops.Op_cm tr; Pdfops.InlineImage (dict, data)]::oplists) t
  | Clip ((w, _) as p, elts)::t ->
      let ops, page' =
        let path_ops =
          [Pdfops.Op_cm (!state).transform] @ path_ops_of_path p 
        and clipviewops =
          [if w = NonZero then Pdfops.Op_W else Pdfops.Op_W'; Pdfops.Op_n]
        and insideclipops, page' =
          ops_of_graphic_acc pdf page [] elts
        in
          protect (path_ops @ clipviewops @ insideclipops), page'
      in
        ops_of_graphic_acc pdf page' (ops::oplists) t
   | Shading (path, shading, transform)::t ->
       let ops, resources' = ops_of_shading pdf page path shading transform in
         let oplists'= protect ops::oplists
         and page' = {page with Pdfpage.resources = resources'} in
           ops_of_graphic_acc pdf page' oplists' t

(* Build a page from a graphic in the same PDF. *)
and image_numbers_of_elts prev = function
  | Image (_, i)::t -> image_numbers_of_elts (i::prev) t
  | MCSection (_, elts)::t
  | MCSectionProperties (_, _, elts)::t
  | Clip (_, elts)::t ->
      let these = image_numbers_of_elts [] elts in
        image_numbers_of_elts (these @ prev) t
  | _::t -> image_numbers_of_elts prev t
  | [] -> prev

and make_xobjects pdf elts =
  let numbers = image_numbers_of_elts [] elts in
    setify <| map (function n -> ("/Im" ^ string_of_int n), Pdf.Indirect n) numbers

and make_resources pdf g page' =
  let resources =
    match g.resources with
    | Pdf.Dictionary rs -> rs
    | _ -> []
  and fontdict =
    Pdf.Dictionary g.fonts
  and xobjdict =
    let objs = make_xobjects pdf g.elements in
      Pdf.Dictionary objs
  and resources_frompage =
    match page'.Pdfpage.resources with
    | Pdf.Dictionary d -> d
    | _ -> raise (Pdf.PDFError "bad resources in page in make_resources")
  in
    let resources = remove "/Shading" resources in
    let resources = remove "/Pattern" resources in
    let resources = remove "/ExtGState" resources in
    let resources = remove "/ColorSpace" resources in
      (* [fold_right] so that entries overwrite *)
      Pdf.Dictionary
        (fold_right
           (fun (k, v) d -> add k v d)
           ["/Font", fontdict; "/XObject", xobjdict]
           (resources_frompage @ resources))

and page_of_graphic pdf (xmin, ymin, xmax, ymax) graphic =
  let page =
    Pdfpage.custompage (Pdf.Array [Pdf.Real xmin; Pdf.Real ymin; Pdf.Real xmax; Pdf.Real ymax])
  in
    let ops, page' = ops_of_graphic_acc pdf page [] graphic.elements in
      (* We're not including the ExtGState because it's in the page', so need
      to merge with resources *)
      let resources = make_resources pdf graphic page' in
        {page' with
           Pdfpage.content = [Pdfops.stream_of_ops ops];
           Pdfpage.resources = resources}

let ops_of_simple_graphic graphic =
  fst (ops_of_graphic_acc (Pdf.empty ()) (Pdfpage.blankpage Pdfpaper.a4) [] graphic.elements)

(* FIXME Add in here a function to copy a page/graphic from one document to another *)

let streams_of_simple_graphic g =
  (page_of_graphic (Pdf.empty ()) (0., 0., 600., 400.) g).Pdfpage.content

(* Transforming a graphic *)
let transform_segment tr s =
  let f = Pdftransform.transform_matrix tr in
    match s with
    | Straight (x, y) -> Straight (f x, f y)
    | Bezier (a, b, c, d) -> Bezier (f a, f b, f c, f d)

let transform_subpath tr (h, c, segments) =
  (h, c, map (transform_segment tr) segments)

let transform_path tr (w, subpaths) =
  (w, map (transform_subpath tr) subpaths)

let transform_element tr = function
  | Path (pth, attr) -> Path (transform_path tr pth, attr)
  | x -> x (* FIXME: Add rest of elements. *)

let transform_graphic tr g =
  {g with elements = map (transform_element tr) g.elements} 
