Split off a little code

This commit is contained in:
John Whitington 2021-10-02 12:22:59 +01:00
parent 1377c5af83
commit 5ae9ffe25f
8 changed files with 265 additions and 260 deletions

View File

@ -1,6 +1,6 @@
# Build the cpdf command line tools and top level
MODS = cpdfstream tjutil tjutf16 tjllist tjparsermonad tjjson \
xmlm cpdfjson cpdfstrftime cpdfcoord \
xmlm cpdferror cpdfjson cpdfstrftime cpdfcoord cpdfattach \
cpdfpagespec cpdfposition cpdf cpdfcommand
SOURCES = $(foreach x,$(MODS),$(x).ml $(x).mli) cpdfcommandrun.ml

229
cpdf.ml
View File

@ -1,6 +1,7 @@
(* CPDF Core routines *)
open Pdfutil
open Pdfio
open Cpdferror
let debug = ref false
@ -434,12 +435,6 @@ let protect fast pdf resources content =
let qs = addstream (many Pdfops.Op_Q deficit @ [Pdfops.Op_Q]) in
[Pdf.Indirect q] @ content @ [Pdf.Indirect qs]
exception SoftError of string
let error s = raise (SoftError s)
exception HardError of string
(* Union two resource dictionaries from the same PDF. *)
let combine_pdf_resources pdf a b =
let a_entries =
@ -532,228 +527,6 @@ let presentation range t d h i dir effect_dur pdf =
in
Pdfpage.change_pages true pdf pages'
(* Attaching files *)
let attach_file ?memory keepversion topage pdf file =
let data =
match memory with
Some data -> data
| None ->
let ch = open_in_bin file in
let len = in_channel_length ch in
let stream = mkbytes len in
let i = input_of_channel ch in
setinit i stream 0 len;
close_in ch;
stream
in
let filestream =
Pdf.Stream
(ref (Pdf.Dictionary
[("/Length", Pdf.Integer (bytes_size data));
("/Type", Pdf.Name "/EmbeddedFile");
("/Params",
Pdf.Dictionary
[("/Size", Pdf.Integer (bytes_size data));
("/CheckSum", Pdf.String (Digest.string (string_of_bytes data)))
])],
Pdf.Got data))
in
let filestream_num = Pdf.addobj pdf filestream in
let basename = Pdftext.pdfdocstring_of_utf8 (Filename.basename file) in
let filespec =
Pdf.Dictionary
[("/EF", Pdf.Dictionary ["/F", Pdf.Indirect filestream_num]);
("/F", Pdf.String basename);
("/Type", Pdf.Name "/Filespec");
("/Desc", Pdf.String "");
("/UF", Pdf.String basename)]
in
match topage with
| None ->
(* Look up /Names and /EmbeddedFiles and /Names. *)
let rootdict = Pdf.lookup_obj pdf pdf.Pdf.root in
let namedict =
match Pdf.lookup_direct pdf "/Names" rootdict with
| None -> Pdf.Dictionary []
| Some namedict -> namedict
in
let embeddednamedict =
match Pdf.lookup_direct pdf "/EmbeddedFiles" namedict with
| None -> Pdf.Dictionary []
| Some embeddednamedict -> embeddednamedict
in
let elts =
match Pdf.lookup_direct pdf "/Names" embeddednamedict with
| Some (Pdf.Array elts) -> elts
| _ -> []
in
let filespecobj = Pdf.addobj pdf filespec in
let names' = Pdf.Array (elts @ [Pdf.String basename; Pdf.Indirect filespecobj]) in
let embeddednamedict' = Pdf.add_dict_entry embeddednamedict "/Names" names' in
let namedict' = Pdf.add_dict_entry namedict "/EmbeddedFiles" embeddednamedict' in
let rootdict' = Pdf.add_dict_entry rootdict "/Names" namedict' in
let rootnum = Pdf.addobj pdf rootdict' in
{pdf with
Pdf.minor = if keepversion then pdf.Pdf.minor else max pdf.Pdf.minor 4;
Pdf.root = rootnum;
Pdf.trailerdict =
Pdf.add_dict_entry
pdf.Pdf.trailerdict "/Root" (Pdf.Indirect rootnum)}
| Some pagenumber ->
let pages = Pdfpage.pages_of_pagetree pdf in
if pagenumber < 0 || pagenumber > length pages then error "attach_file: Page not found" else
let page = select pagenumber pages in
let annots =
match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
| Some (Pdf.Array annots) -> annots
| _ -> []
in
let rect =
let minx, miny, maxx, maxy = Pdf.parse_rectangle page.Pdfpage.mediabox in
Pdf.Array [Pdf.Real 18.; Pdf.Real (maxy -. 45.); Pdf.Real 45.; Pdf.Real (maxy -. 18.)]
in
let filespecobj = Pdf.addobj pdf filespec in
let annot =
Pdf.Dictionary
[("/FS", Pdf.Indirect filespecobj);
("/Subtype", Pdf.Name "/FileAttachment");
("/Contents", Pdf.String basename);
("/Rect", rect)]
in
let annots' = Pdf.Array (annot::annots) in
let page' =
{page with Pdfpage.rest = Pdf.add_dict_entry page.Pdfpage.rest "/Annots" annots'}
in
let pages' = replace_number pagenumber page' pages in
let pdf = Pdfpage.change_pages true pdf pages' in
{pdf with
Pdf.minor = if keepversion then pdf.Pdf.minor else max pdf.Pdf.minor 4}
type attachment =
{name : string;
pagenumber : int;
data : unit -> Pdfio.bytes}
let list_attached_files pdf =
let toplevel =
match Pdf.lookup_direct pdf "/Root" pdf.Pdf.trailerdict with
| None -> []
| Some rootdict ->
match Pdf.lookup_direct pdf "/Names" rootdict with
| None -> []
| Some namedict ->
match Pdf.lookup_direct pdf "/EmbeddedFiles" namedict with
| Some nametree ->
map
(function (x, ef) ->
match Pdf.lookup_direct pdf "/EF" ef with
| Some ((Pdf.Dictionary _) as d) ->
begin match Pdf.lookup_direct pdf "/F" d with
| Some stream ->
{name = x;
pagenumber = 0;
data =
(fun () ->
try
Pdf.getstream stream;
Pdfcodec.decode_pdfstream pdf stream;
match stream with
Pdf.Stream {contents = (_, Pdf.Got data)} -> data
| _ -> raise Not_found
with
_ -> raise (Pdf.PDFError "could not retreive attachment data"))}
| None -> raise (Pdf.PDFError "/F not found")
end
| _ -> raise (Pdf.PDFError "/EF not found"))
(option_map
(function (Pdf.String s, ef) -> Some (s, ef) | _ -> None)
(Pdf.contents_of_nametree pdf nametree))
| _ -> []
in let pagelevel =
let pages = Pdfpage.pages_of_pagetree pdf in
flatten
(map2
(fun page pagenumber ->
option_map
(function annot ->
match Pdf.lookup_direct pdf "/Subtype" annot with
| Some (Pdf.Name "/FileAttachment") ->
(match Pdf.lookup_direct pdf "/Contents" annot with
| Some (Pdf.String s) ->
begin match Pdf.lookup_direct pdf "/FS" annot with
| Some ((Pdf.Dictionary _) as d) ->
(*Printf.eprintf "%s\n%!" (Pdfwrite.string_of_pdf d);*)
begin match Pdf.lookup_direct pdf "/EF" d with
| Some ((Pdf.Dictionary _) as d) ->
begin match Pdf.lookup_direct pdf "/F" d with
| Some stream ->
Some
{name = s;
pagenumber = pagenumber;
data =
(fun () ->
try
Pdf.getstream stream;
Pdfcodec.decode_pdfstream pdf stream;
match stream with
Pdf.Stream {contents = (_, Pdf.Got data)} -> data
| _ -> raise Not_found
with
_ -> raise (Pdf.PDFError "could not retreive attachment data"))}
| _ -> raise (Pdf.PDFError "no /F found in attachment")
end
| _ ->
Some
{name = s;
pagenumber = pagenumber;
data = (fun () -> raise (Pdf.PDFError "no attachment data"))}
end
| _ -> None
end
| _ -> None)
| _ -> None)
(match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
| Some (Pdf.Array annots) -> annots
| _ -> []))
pages
(indx pages))
in
toplevel @ pagelevel
(* \section{Remove Attached files} *)
let remove_attached_files_on_pages pdf =
let remove_from_page page =
{page with Pdfpage.rest =
Pdf.add_dict_entry page.Pdfpage.rest "/Annots"
(Pdf.Array
(option_map
(function annot ->
match Pdf.lookup_direct pdf "/Subtype" annot with
| Some (Pdf.Name "/FileAttachment") -> None
| _ -> Some annot)
(match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
| Some (Pdf.Array annots) -> annots
| _ -> [])))}
in
Pdfpage.change_pages true pdf (map remove_from_page (Pdfpage.pages_of_pagetree pdf))
let remove_attached_files pdf =
let pdf = remove_attached_files_on_pages pdf in
match Pdf.lookup_direct pdf "/Root" pdf.Pdf.trailerdict with
| None -> pdf
| Some rootdict ->
match Pdf.lookup_direct pdf "/Names" rootdict with
| None -> pdf
| Some namedict ->
let namedict' = Pdf.remove_dict_entry namedict "/EmbeddedFiles" in
let rootdict' = Pdf.add_dict_entry rootdict "/Names" namedict' in
let rootdict'num = Pdf.addobj pdf rootdict' in
{pdf with
Pdf.root =
rootdict'num;
Pdf.trailerdict =
Pdf.add_dict_entry pdf.Pdf.trailerdict "/Root" (Pdf.Indirect rootdict'num)}
(* \section{Copy an /ID from one file to another} *)
let copy_id keepversion copyfrom copyto =
match Pdf.lookup_direct copyfrom "/ID" copyfrom.Pdf.trailerdict with

View File

@ -9,11 +9,6 @@ all - the PDF string is output as-is. [UTF8] converts loslessly to UTF8.
correspond to 7 bit ASCII. *)
type encoding = Raw | UTF8 | Stripped
exception SoftError of string
exception HardError of string
(** Two exceptions recommended for use with the library, though currently not
raised by any function in this module. Cpdfcommand uses them extensively. *)
(** {2 Debug} *)
(** Debug: Print out a PDF in readable form to the terminal *)
@ -91,21 +86,6 @@ adds a presentation on the pages in [range]. See cpdfmanual.pdf for details.
val presentation : int list -> string option ->
float option -> bool -> bool -> int -> float -> Pdf.t -> Pdf.t
(** {2 File Attachments} *)
(** [attach_file keepversion topage pdf filename] attaches the file in [filename] to the pdf, optionally to a page (rather than document-level). If keepversion is true, the PDF version number won't be altered. *)
val attach_file : ?memory:Pdfio.bytes -> bool -> int option -> Pdf.t -> string -> Pdf.t
(** Remove attached files. *)
val remove_attached_files : Pdf.t -> Pdf.t
type attachment =
{name : string;
pagenumber : int;
data : unit -> Pdfio.bytes}
(** List attached files. Attachment name and page number. Page 0 is document level. *)
val list_attached_files : Pdf.t -> attachment list
(** {2 Bookmarks} *)
(** [parse_bookmark_file verify pdf input] parses the bookmark file in [input].

226
cpdfattach.ml Normal file
View File

@ -0,0 +1,226 @@
open Pdfutil
open Pdfio
open Cpdferror
(* Attaching files *)
let attach_file ?memory keepversion topage pdf file =
let data =
match memory with
Some data -> data
| None ->
let ch = open_in_bin file in
let len = in_channel_length ch in
let stream = mkbytes len in
let i = input_of_channel ch in
setinit i stream 0 len;
close_in ch;
stream
in
let filestream =
Pdf.Stream
(ref (Pdf.Dictionary
[("/Length", Pdf.Integer (bytes_size data));
("/Type", Pdf.Name "/EmbeddedFile");
("/Params",
Pdf.Dictionary
[("/Size", Pdf.Integer (bytes_size data));
("/CheckSum", Pdf.String (Digest.string (string_of_bytes data)))
])],
Pdf.Got data))
in
let filestream_num = Pdf.addobj pdf filestream in
let basename = Pdftext.pdfdocstring_of_utf8 (Filename.basename file) in
let filespec =
Pdf.Dictionary
[("/EF", Pdf.Dictionary ["/F", Pdf.Indirect filestream_num]);
("/F", Pdf.String basename);
("/Type", Pdf.Name "/Filespec");
("/Desc", Pdf.String "");
("/UF", Pdf.String basename)]
in
match topage with
| None ->
(* Look up /Names and /EmbeddedFiles and /Names. *)
let rootdict = Pdf.lookup_obj pdf pdf.Pdf.root in
let namedict =
match Pdf.lookup_direct pdf "/Names" rootdict with
| None -> Pdf.Dictionary []
| Some namedict -> namedict
in
let embeddednamedict =
match Pdf.lookup_direct pdf "/EmbeddedFiles" namedict with
| None -> Pdf.Dictionary []
| Some embeddednamedict -> embeddednamedict
in
let elts =
match Pdf.lookup_direct pdf "/Names" embeddednamedict with
| Some (Pdf.Array elts) -> elts
| _ -> []
in
let filespecobj = Pdf.addobj pdf filespec in
let names' = Pdf.Array (elts @ [Pdf.String basename; Pdf.Indirect filespecobj]) in
let embeddednamedict' = Pdf.add_dict_entry embeddednamedict "/Names" names' in
let namedict' = Pdf.add_dict_entry namedict "/EmbeddedFiles" embeddednamedict' in
let rootdict' = Pdf.add_dict_entry rootdict "/Names" namedict' in
let rootnum = Pdf.addobj pdf rootdict' in
{pdf with
Pdf.minor = if keepversion then pdf.Pdf.minor else max pdf.Pdf.minor 4;
Pdf.root = rootnum;
Pdf.trailerdict =
Pdf.add_dict_entry
pdf.Pdf.trailerdict "/Root" (Pdf.Indirect rootnum)}
| Some pagenumber ->
let pages = Pdfpage.pages_of_pagetree pdf in
if pagenumber < 0 || pagenumber > length pages then error "attach_file: Page not found" else
let page = select pagenumber pages in
let annots =
match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
| Some (Pdf.Array annots) -> annots
| _ -> []
in
let rect =
let minx, miny, maxx, maxy = Pdf.parse_rectangle page.Pdfpage.mediabox in
Pdf.Array [Pdf.Real 18.; Pdf.Real (maxy -. 45.); Pdf.Real 45.; Pdf.Real (maxy -. 18.)]
in
let filespecobj = Pdf.addobj pdf filespec in
let annot =
Pdf.Dictionary
[("/FS", Pdf.Indirect filespecobj);
("/Subtype", Pdf.Name "/FileAttachment");
("/Contents", Pdf.String basename);
("/Rect", rect)]
in
let annots' = Pdf.Array (annot::annots) in
let page' =
{page with Pdfpage.rest = Pdf.add_dict_entry page.Pdfpage.rest "/Annots" annots'}
in
let pages' = replace_number pagenumber page' pages in
let pdf = Pdfpage.change_pages true pdf pages' in
{pdf with
Pdf.minor = if keepversion then pdf.Pdf.minor else max pdf.Pdf.minor 4}
type attachment =
{name : string;
pagenumber : int;
data : unit -> Pdfio.bytes}
let list_attached_files pdf =
let toplevel =
match Pdf.lookup_direct pdf "/Root" pdf.Pdf.trailerdict with
| None -> []
| Some rootdict ->
match Pdf.lookup_direct pdf "/Names" rootdict with
| None -> []
| Some namedict ->
match Pdf.lookup_direct pdf "/EmbeddedFiles" namedict with
| Some nametree ->
map
(function (x, ef) ->
match Pdf.lookup_direct pdf "/EF" ef with
| Some ((Pdf.Dictionary _) as d) ->
begin match Pdf.lookup_direct pdf "/F" d with
| Some stream ->
{name = x;
pagenumber = 0;
data =
(fun () ->
try
Pdf.getstream stream;
Pdfcodec.decode_pdfstream pdf stream;
match stream with
Pdf.Stream {contents = (_, Pdf.Got data)} -> data
| _ -> raise Not_found
with
_ -> raise (Pdf.PDFError "could not retreive attachment data"))}
| None -> raise (Pdf.PDFError "/F not found")
end
| _ -> raise (Pdf.PDFError "/EF not found"))
(option_map
(function (Pdf.String s, ef) -> Some (s, ef) | _ -> None)
(Pdf.contents_of_nametree pdf nametree))
| _ -> []
in let pagelevel =
let pages = Pdfpage.pages_of_pagetree pdf in
flatten
(map2
(fun page pagenumber ->
option_map
(function annot ->
match Pdf.lookup_direct pdf "/Subtype" annot with
| Some (Pdf.Name "/FileAttachment") ->
(match Pdf.lookup_direct pdf "/Contents" annot with
| Some (Pdf.String s) ->
begin match Pdf.lookup_direct pdf "/FS" annot with
| Some ((Pdf.Dictionary _) as d) ->
(*Printf.eprintf "%s\n%!" (Pdfwrite.string_of_pdf d);*)
begin match Pdf.lookup_direct pdf "/EF" d with
| Some ((Pdf.Dictionary _) as d) ->
begin match Pdf.lookup_direct pdf "/F" d with
| Some stream ->
Some
{name = s;
pagenumber = pagenumber;
data =
(fun () ->
try
Pdf.getstream stream;
Pdfcodec.decode_pdfstream pdf stream;
match stream with
Pdf.Stream {contents = (_, Pdf.Got data)} -> data
| _ -> raise Not_found
with
_ -> raise (Pdf.PDFError "could not retreive attachment data"))}
| _ -> raise (Pdf.PDFError "no /F found in attachment")
end
| _ ->
Some
{name = s;
pagenumber = pagenumber;
data = (fun () -> raise (Pdf.PDFError "no attachment data"))}
end
| _ -> None
end
| _ -> None)
| _ -> None)
(match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
| Some (Pdf.Array annots) -> annots
| _ -> []))
pages
(indx pages))
in
toplevel @ pagelevel
(* \section{Remove Attached files} *)
let remove_attached_files_on_pages pdf =
let remove_from_page page =
{page with Pdfpage.rest =
Pdf.add_dict_entry page.Pdfpage.rest "/Annots"
(Pdf.Array
(option_map
(function annot ->
match Pdf.lookup_direct pdf "/Subtype" annot with
| Some (Pdf.Name "/FileAttachment") -> None
| _ -> Some annot)
(match Pdf.lookup_direct pdf "/Annots" page.Pdfpage.rest with
| Some (Pdf.Array annots) -> annots
| _ -> [])))}
in
Pdfpage.change_pages true pdf (map remove_from_page (Pdfpage.pages_of_pagetree pdf))
let remove_attached_files pdf =
let pdf = remove_attached_files_on_pages pdf in
match Pdf.lookup_direct pdf "/Root" pdf.Pdf.trailerdict with
| None -> pdf
| Some rootdict ->
match Pdf.lookup_direct pdf "/Names" rootdict with
| None -> pdf
| Some namedict ->
let namedict' = Pdf.remove_dict_entry namedict "/EmbeddedFiles" in
let rootdict' = Pdf.add_dict_entry rootdict "/Names" namedict' in
let rootdict'num = Pdf.addobj pdf rootdict' in
{pdf with
Pdf.root =
rootdict'num;
Pdf.trailerdict =
Pdf.add_dict_entry pdf.Pdf.trailerdict "/Root" (Pdf.Indirect rootdict'num)}

14
cpdfattach.mli Normal file
View File

@ -0,0 +1,14 @@
(** {2 File Attachments} *)
(** [attach_file keepversion topage pdf filename] attaches the file in [filename] to the pdf, optionally to a page (rather than document-level). If keepversion is true, the PDF version number won't be altered. *)
val attach_file : ?memory:Pdfio.bytes -> bool -> int option -> Pdf.t -> string -> Pdf.t
(** Remove attached files. *)
val remove_attached_files : Pdf.t -> Pdf.t
type attachment =
{name : string;
pagenumber : int;
data : unit -> Pdfio.bytes}
(** List attached files. Attachment name and page number. Page 0 is document level. *)
val list_attached_files : Pdf.t -> attachment list

View File

@ -27,17 +27,17 @@ it had been loaded. *)
let pdfread_pdf_of_input ?revision a b c =
try Pdfread.pdf_of_input ?revision a b c with
Pdf.PDFError s when String.length s >=10 && String.sub s 0 10 = "Encryption" ->
raise (Cpdf.SoftError "Bad owner or user password when reading document")
raise (Cpdferror.SoftError "Bad owner or user password when reading document")
let pdfread_pdf_of_channel_lazy ?revision ?source b c d =
try Pdfread.pdf_of_channel_lazy ?revision ?source b c d with
Pdf.PDFError s when String.length s >=10 && String.sub s 0 10 = "Encryption" ->
raise (Cpdf.SoftError "Bad owner or user password when reading document")
raise (Cpdferror.SoftError "Bad owner or user password when reading document")
let pdfread_pdf_of_file ?revision a b c =
try Pdfread.pdf_of_file ?revision a b c with
Pdf.PDFError s when String.length s >=10 && String.sub s 0 10 = "Encryption" ->
raise (Cpdf.SoftError "Bad owner or user password when reading document")
raise (Cpdferror.SoftError "Bad owner or user password when reading document")
let optstring = function
| "" -> None
@ -2352,7 +2352,7 @@ let rec get_single_pdf ?(decrypt=true) ?(fail=false) op read_lazy =
else
pdfread_pdf_of_file ?revision (optstring u) (optstring o) inname
with
| Cpdf.SoftError _ as e -> raise e (* Bad owner or user password *)
| Cpdferror.SoftError _ as e -> raise e (* Bad owner or user password *)
| _ ->
if args.gs_malformed then
begin
@ -2446,7 +2446,7 @@ let rec get_pdf_from_input_kind ?(read_lazy=false) ?(decrypt=true) ?(fail=false)
else
pdfread_pdf_of_file ?revision (optstring u) (optstring o) s
with
| Cpdf.SoftError _ as e -> raise e (* Bad owner or user password *)
| Cpdferror.SoftError _ as e -> raise e (* Bad owner or user password *)
| e ->
Printf.printf "%s\n" (Printexc.to_string e);
if args.gs_malformed then
@ -4046,9 +4046,9 @@ let go () =
write_pdf false (Cpdf.scale_contents ~fast:args.fast args.position scale pdf range)
| Some ListAttachedFiles ->
let pdf = get_single_pdf args.op false in
let attachments = Cpdf.list_attached_files pdf in
let attachments = Cpdfattach.list_attached_files pdf in
iter
(fun a -> Printf.printf "%i %s\n" a.Cpdf.pagenumber a.Cpdf.name)
(fun a -> Printf.printf "%i %s\n" a.Cpdfattach.pagenumber a.Cpdfattach.name)
attachments;
flprint ""
| Some DumpAttachedFiles ->
@ -4059,7 +4059,7 @@ let go () =
| Stdout -> error "Can't dump attachments to stdout"
end
| Some RemoveAttachedFiles ->
write_pdf false (Cpdf.remove_attached_files (get_single_pdf args.op false))
write_pdf false (Cpdfattach.remove_attached_files (get_single_pdf args.op false))
| Some (AttachFile files) ->
begin match args.inputs with
| [(k, _, _, _, _, _) as input] ->
@ -4072,7 +4072,7 @@ let go () =
| Some s -> Some (int_of_string s)
with _ -> error "Bad -to-page"
in
let pdf = fold_left (Cpdf.attach_file args.keepversion topage) pdf (rev files) in
let pdf = fold_left (Cpdfattach.attach_file args.keepversion topage) pdf (rev files) in
write_pdf false pdf
| _ -> error "attach file: No input file specified"
end
@ -4499,8 +4499,8 @@ let go_withargv argv =
if args.debug then raise e else exit 2
else
raise StayOnError
| Cpdf.SoftError s -> soft_error s
| Cpdf.HardError s -> error s
| Cpdferror.SoftError s -> soft_error s
| Cpdferror.HardError s -> error s
| e ->
prerr_string
("cpdf encountered an unexpected error. Technical Details follow:\n" ^

9
cpdferror.ml Normal file
View File

@ -0,0 +1,9 @@
(** Two exceptions recommended for use with the library, though currently not
raised by any function in this module. Cpdfcommand uses them extensively. *)
exception SoftError of string
exception HardError of string
let error s = raise (SoftError s)

3
cpdferror.mli Normal file
View File

@ -0,0 +1,3 @@
exception SoftError of string
val error : string -> 'a
exception HardError of string