diff --git a/README.md b/README.md index 00db36e..1d99c31 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,16 @@ Go wrapper for [MuPDF](http://mupdf.com/) fitz library that can extract pages fr * `static` - build with static external MuPDF library (used with `extlib`) * `pkgconfig` - enable pkg-config (used with `extlib`) * `musl` - use musl compiled library +* `nocgo` - experimental [purego](https://github.com/ebitengine/purego) implementation (can also be used with `CGO_ENABLED=0`) ### Notes The bundled libraries are built without CJK fonts, if you need them you must use the external library. Calling e.g. Image() or Text() methods concurrently for the same document is not supported. + +Purego implementation requires `libffi` and `libmupdf` shared libraries on runtime. +You must set `fitz.FzVersion` in your code or set `FZ_VERSION` environment variable to exact version of the shared library. ### Example ```go diff --git a/fitz.go b/fitz.go index 1426ba4..7bff30d 100644 --- a/fitz.go +++ b/fitz.go @@ -1,77 +1,9 @@ // Package fitz provides wrapper for the [MuPDF](http://mupdf.com/) fitz library -// that can extract pages from PDF, EPUB and MOBI documents as images, text, html or svg. +// that can extract pages from PDF, EPUB, MOBI, DOCX, XLSX and PPTX documents as IMG, TXT, HTML or SVG. package fitz -/* -#include -#include - -const char *fz_version = FZ_VERSION; -#if defined(_WIN32) - typedef unsigned long long store; -#else - typedef unsigned long store; -#endif - -fz_document *open_document(fz_context *ctx, const char *filename) { - fz_document *doc; - - fz_try(ctx) { - doc = fz_open_document(ctx, filename); - } - fz_catch(ctx) { - return NULL; - } - - return doc; -} - -fz_document *open_document_with_stream(fz_context *ctx, const char *magic, fz_stream *stream) { - fz_document *doc; - - fz_try(ctx) { - doc = fz_open_document_with_stream(ctx, magic, stream); - } - fz_catch(ctx) { - return NULL; - } - - return doc; -} - -fz_page *load_page(fz_context *ctx, fz_document *doc, int number) { - fz_page *page; - - fz_try(ctx) { - page = fz_load_page(ctx, doc, number); - } - fz_catch(ctx) { - return NULL; - } - - return page; -} - -int run_page_contents(fz_context *ctx, fz_page *page, fz_device *dev, fz_matrix transform, fz_cookie *cookie) { - fz_try(ctx) { - fz_run_page_contents(ctx, page, dev, transform, cookie); - } - fz_catch(ctx) { - return 0; - } - - return 1; -} -*/ -import "C" - import ( "errors" - "image" - "io" - "os" - "path/filepath" - "sync" "unsafe" ) @@ -93,14 +25,9 @@ var ( // MaxStore is maximum size in bytes of the resource store, before it will start evicting cached resources such as fonts and images. var MaxStore = 256 << 20 -// Document represents fitz document. -type Document struct { - ctx *C.struct_fz_context - data []byte // binds data to the Document lifecycle avoiding premature GC - doc *C.struct_fz_document - mtx sync.Mutex - stream *C.fz_stream -} +// FzVersion is used for experimental purego implementation, it must be exactly the same as libmupdf shared library version. +// It is also possible to set `FZ_VERSION` environment variable. +var FzVersion = "1.24.9" // Outline type. type Outline struct { @@ -121,517 +48,19 @@ type Link struct { URI string } -// New returns new fitz document. -func New(filename string) (f *Document, err error) { - f = &Document{} - - filename, err = filepath.Abs(filename) - if err != nil { - return - } - - if _, e := os.Stat(filename); e != nil { - err = ErrNoSuchFile - return - } - - f.ctx = (*C.struct_fz_context)(unsafe.Pointer(C.fz_new_context_imp(nil, nil, C.store(MaxStore), C.fz_version))) - if f.ctx == nil { - err = ErrCreateContext - return - } - - C.fz_register_document_handlers(f.ctx) - - cfilename := C.CString(filename) - defer C.free(unsafe.Pointer(cfilename)) - - f.doc = C.open_document(f.ctx, cfilename) - if f.doc == nil { - err = ErrOpenDocument - return - } - - ret := C.fz_needs_password(f.ctx, f.doc) - v := int(ret) != 0 - if v { - err = ErrNeedsPassword - } - - return -} - -// NewFromMemory returns new fitz document from byte slice. -func NewFromMemory(b []byte) (f *Document, err error) { - f = &Document{} - - f.ctx = (*C.struct_fz_context)(unsafe.Pointer(C.fz_new_context_imp(nil, nil, C.store(MaxStore), C.fz_version))) - if f.ctx == nil { - err = ErrCreateContext - return - } - - C.fz_register_document_handlers(f.ctx) - - stream := C.fz_open_memory(f.ctx, (*C.uchar)(&b[0]), C.size_t(len(b))) - f.stream = C.fz_keep_stream(f.ctx, stream) - - if f.stream == nil { - err = ErrOpenMemory - return - } - - magic := contentType(b) - if magic == "" { - err = ErrOpenMemory - return - } - - f.data = b - - cmagic := C.CString(magic) - defer C.free(unsafe.Pointer(cmagic)) - - f.doc = C.open_document_with_stream(f.ctx, cmagic, f.stream) - if f.doc == nil { - err = ErrOpenDocument - } - - ret := C.fz_needs_password(f.ctx, f.doc) - v := int(ret) != 0 - if v { - err = ErrNeedsPassword - } - - return -} - -// NewFromReader returns new fitz document from io.Reader. -func NewFromReader(r io.Reader) (f *Document, err error) { - b, e := io.ReadAll(r) - if e != nil { - err = e - return - } - - f, err = NewFromMemory(b) - - return -} - -// NumPage returns total number of pages in document. -func (f *Document) NumPage() int { - return int(C.fz_count_pages(f.ctx, f.doc)) -} - -// Image returns image for given page number. -func (f *Document) Image(pageNumber int) (*image.RGBA, error) { - return f.ImageDPI(pageNumber, 300.0) -} - -// ImageDPI returns image for given page number and DPI. -func (f *Document) ImageDPI(pageNumber int, dpi float64) (*image.RGBA, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return nil, ErrPageMissing - } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return nil, ErrLoadPage - } - - defer C.fz_drop_page(f.ctx, page) - - var bounds C.fz_rect - bounds = C.fz_bound_page(f.ctx, page) - - var ctm C.fz_matrix - ctm = C.fz_scale(C.float(dpi/72), C.float(dpi/72)) - - var bbox C.fz_irect - bounds = C.fz_transform_rect(bounds, ctm) - bbox = C.fz_round_rect(bounds) - - pixmap := C.fz_new_pixmap_with_bbox(f.ctx, C.fz_device_rgb(f.ctx), bbox, nil, 1) - if pixmap == nil { - return nil, ErrCreatePixmap - } - - C.fz_clear_pixmap_with_value(f.ctx, pixmap, C.int(0xff)) - defer C.fz_drop_pixmap(f.ctx, pixmap) - - device := C.fz_new_draw_device(f.ctx, ctm, pixmap) - C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) - defer C.fz_drop_device(f.ctx, device) - - drawMatrix := C.fz_identity - ret := C.run_page_contents(f.ctx, page, device, drawMatrix, nil) - if ret == 0 { - return nil, ErrRunPageContents - } - - C.fz_close_device(f.ctx, device) - - pixels := C.fz_pixmap_samples(f.ctx, pixmap) - if pixels == nil { - return nil, ErrPixmapSamples - } - - img := image.RGBA{} - img.Pix = C.GoBytes(unsafe.Pointer(pixels), C.int(4*bbox.x1*bbox.y1)) - img.Rect = image.Rect(int(bbox.x0), int(bbox.y0), int(bbox.x1), int(bbox.y1)) - img.Stride = 4 * img.Rect.Max.X - - return &img, nil -} - -// ImagePNG returns image for given page number as PNG bytes. -func (f *Document) ImagePNG(pageNumber int, dpi float64) ([]byte, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return nil, ErrPageMissing - } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return nil, ErrLoadPage - } - - defer C.fz_drop_page(f.ctx, page) - - var bounds C.fz_rect - bounds = C.fz_bound_page(f.ctx, page) - - var ctm C.fz_matrix - ctm = C.fz_scale(C.float(dpi/72), C.float(dpi/72)) - - var bbox C.fz_irect - bounds = C.fz_transform_rect(bounds, ctm) - bbox = C.fz_round_rect(bounds) - - pixmap := C.fz_new_pixmap_with_bbox(f.ctx, C.fz_device_rgb(f.ctx), bbox, nil, 1) - if pixmap == nil { - return nil, ErrCreatePixmap - } - - C.fz_clear_pixmap_with_value(f.ctx, pixmap, C.int(0xff)) - defer C.fz_drop_pixmap(f.ctx, pixmap) - - device := C.fz_new_draw_device(f.ctx, ctm, pixmap) - C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) - defer C.fz_drop_device(f.ctx, device) - - drawMatrix := C.fz_identity - ret := C.run_page_contents(f.ctx, page, device, drawMatrix, nil) - if ret == 0 { - return nil, ErrRunPageContents - } - - C.fz_close_device(f.ctx, device) - - buf := C.fz_new_buffer_from_pixmap_as_png(f.ctx, pixmap, C.fz_default_color_params) - defer C.fz_drop_buffer(f.ctx, buf) - - size := C.fz_buffer_storage(f.ctx, buf, nil) - str := C.GoStringN(C.fz_string_from_buffer(f.ctx, buf), C.int(size)) - - return []byte(str), nil -} - -// Links returns slice of links for given page number. -func (f *Document) Links(pageNumber int) ([]Link, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return nil, ErrPageMissing - } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return nil, ErrLoadPage - } - - defer C.fz_drop_page(f.ctx, page) - - links := C.fz_load_links(f.ctx, page) - defer C.fz_drop_link(f.ctx, links) - - linkCount := 0 - for currLink := links; currLink != nil; currLink = currLink.next { - linkCount++ - } - - if linkCount == 0 { - return nil, nil - } - - gLinks := make([]Link, linkCount) - - currLink := links - for i := 0; i < linkCount; i++ { - gLinks[i] = Link{ - URI: C.GoString(currLink.uri), - } - currLink = currLink.next - } - - return gLinks, nil -} - -// Text returns text for given page number. -func (f *Document) Text(pageNumber int) (string, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return "", ErrPageMissing - } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return "", ErrLoadPage - } - - defer C.fz_drop_page(f.ctx, page) - - var bounds C.fz_rect - bounds = C.fz_bound_page(f.ctx, page) - - var ctm C.fz_matrix - ctm = C.fz_scale(C.float(72.0/72), C.float(72.0/72)) - - text := C.fz_new_stext_page(f.ctx, bounds) - defer C.fz_drop_stext_page(f.ctx, text) - - var opts C.fz_stext_options - opts.flags = 0 - - device := C.fz_new_stext_device(f.ctx, text, &opts) - C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) - defer C.fz_drop_device(f.ctx, device) - - var cookie C.fz_cookie - ret := C.run_page_contents(f.ctx, page, device, ctm, &cookie) - if ret == 0 { - return "", ErrRunPageContents - } - - C.fz_close_device(f.ctx, device) - - buf := C.fz_new_buffer_from_stext_page(f.ctx, text) - defer C.fz_drop_buffer(f.ctx, buf) - - str := C.GoString(C.fz_string_from_buffer(f.ctx, buf)) - - return str, nil -} - -// HTML returns html for given page number. -func (f *Document) HTML(pageNumber int, header bool) (string, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return "", ErrPageMissing +func bytePtrToString(p *byte) string { + if p == nil { + return "" } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return "", ErrLoadPage + if *p == 0 { + return "" } - defer C.fz_drop_page(f.ctx, page) - - var bounds C.fz_rect - bounds = C.fz_bound_page(f.ctx, page) - - var ctm C.fz_matrix - ctm = C.fz_scale(C.float(72.0/72), C.float(72.0/72)) - - text := C.fz_new_stext_page(f.ctx, bounds) - defer C.fz_drop_stext_page(f.ctx, text) - - var opts C.fz_stext_options - opts.flags = C.FZ_STEXT_PRESERVE_IMAGES - - device := C.fz_new_stext_device(f.ctx, text, &opts) - C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) - defer C.fz_drop_device(f.ctx, device) - - var cookie C.fz_cookie - ret := C.run_page_contents(f.ctx, page, device, ctm, &cookie) - if ret == 0 { - return "", ErrRunPageContents - } - - C.fz_close_device(f.ctx, device) - - buf := C.fz_new_buffer(f.ctx, 1024) - defer C.fz_drop_buffer(f.ctx, buf) - - out := C.fz_new_output_with_buffer(f.ctx, buf) - defer C.fz_drop_output(f.ctx, out) - - if header { - C.fz_print_stext_header_as_html(f.ctx, out) + // Find NUL terminator. + n := 0 + for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ { + ptr = unsafe.Pointer(uintptr(ptr) + 1) } - C.fz_print_stext_page_as_html(f.ctx, out, text, C.int(pageNumber)) - if header { - C.fz_print_stext_trailer_as_html(f.ctx, out) - } - - C.fz_close_output(f.ctx, out) - - str := C.GoString(C.fz_string_from_buffer(f.ctx, buf)) - - return str, nil -} - -// SVG returns svg document for given page number. -func (f *Document) SVG(pageNumber int) (string, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return "", ErrPageMissing - } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return "", ErrLoadPage - } - - defer C.fz_drop_page(f.ctx, page) - - var bounds C.fz_rect - bounds = C.fz_bound_page(f.ctx, page) - - var ctm C.fz_matrix - ctm = C.fz_scale(C.float(72.0/72), C.float(72.0/72)) - bounds = C.fz_transform_rect(bounds, ctm) - - buf := C.fz_new_buffer(f.ctx, 1024) - defer C.fz_drop_buffer(f.ctx, buf) - - out := C.fz_new_output_with_buffer(f.ctx, buf) - defer C.fz_drop_output(f.ctx, out) - - device := C.fz_new_svg_device(f.ctx, out, bounds.x1-bounds.x0, bounds.y1-bounds.y0, C.FZ_SVG_TEXT_AS_PATH, 1) - C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) - defer C.fz_drop_device(f.ctx, device) - - var cookie C.fz_cookie - ret := C.run_page_contents(f.ctx, page, device, ctm, &cookie) - if ret == 0 { - return "", ErrRunPageContents - } - - C.fz_close_device(f.ctx, device) - C.fz_close_output(f.ctx, out) - - str := C.GoString(C.fz_string_from_buffer(f.ctx, buf)) - - return str, nil -} - -// ToC returns the table of contents (also known as outline). -func (f *Document) ToC() ([]Outline, error) { - data := make([]Outline, 0) - - outline := C.fz_load_outline(f.ctx, f.doc) - if outline == nil { - return nil, ErrLoadOutline - } - defer C.fz_drop_outline(f.ctx, outline) - - var walk func(outline *C.fz_outline, level int) - - walk = func(outline *C.fz_outline, level int) { - for outline != nil { - res := Outline{} - res.Level = level - res.Title = C.GoString(outline.title) - res.URI = C.GoString(outline.uri) - res.Page = int(outline.page.page) - res.Top = float64(outline.y) - data = append(data, res) - - if outline.down != nil { - walk(outline.down, level+1) - } - outline = outline.next - } - } - - walk(outline, 1) - return data, nil -} - -// Metadata returns the map with standard metadata. -func (f *Document) Metadata() map[string]string { - data := make(map[string]string) - - lookup := func(key string) string { - ckey := C.CString(key) - defer C.free(unsafe.Pointer(ckey)) - - buf := make([]byte, 256) - C.fz_lookup_metadata(f.ctx, f.doc, ckey, (*C.char)(unsafe.Pointer(&buf[0])), C.int(len(buf))) - - return string(buf) - } - - data["format"] = lookup("format") - data["encryption"] = lookup("encryption") - data["title"] = lookup("info:Title") - data["author"] = lookup("info:Author") - data["subject"] = lookup("info:Subject") - data["keywords"] = lookup("info:Keywords") - data["creator"] = lookup("info:Creator") - data["producer"] = lookup("info:Producer") - data["creationDate"] = lookup("info:CreationDate") - data["modDate"] = lookup("info:modDate") - - return data -} - -// Bound gives the Bounds of a given Page in the document. -func (f *Document) Bound(pageNumber int) (image.Rectangle, error) { - f.mtx.Lock() - defer f.mtx.Unlock() - - if pageNumber >= f.NumPage() { - return image.Rectangle{}, ErrPageMissing - } - - page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) - if page == nil { - return image.Rectangle{}, ErrLoadPage - } - - defer C.fz_drop_page(f.ctx, page) - - var bounds C.fz_rect - bounds = C.fz_bound_page(f.ctx, page) - return image.Rect(int(bounds.x0), int(bounds.y0), int(bounds.x1), int(bounds.y1)), nil -} - -// Close closes the underlying fitz document. -func (f *Document) Close() error { - if f.stream != nil { - C.fz_drop_stream(f.ctx, f.stream) - } - - C.fz_drop_document(f.ctx, f.doc) - C.fz_drop_context(f.ctx) - - f.data = nil - return nil + return string(unsafe.Slice(p, n)) } diff --git a/fitz_cgo.go b/fitz_cgo.go index 53b4176..51035ab 100644 --- a/fitz_cgo.go +++ b/fitz_cgo.go @@ -1,18 +1,598 @@ -//go:build !extlib +//go:build cgo && !nocgo package fitz /* -#cgo CFLAGS: -Iinclude - -#cgo linux,amd64,!musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_amd64 -lmupdfthird_linux_amd64 -lm -#cgo linux,amd64,musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_amd64_musl -lmupdfthird_linux_amd64_musl -lm -#cgo linux,!android,arm64,!musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_arm64 -lmupdfthird_linux_arm64 -lm -#cgo linux,!android,arm64,musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_arm64_musl -lmupdfthird_linux_arm64_musl -lm -#cgo android,arm64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_android_arm64 -lmupdfthird_android_arm64 -lm -llog -#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_windows_amd64 -lmupdfthird_windows_amd64 -lm -lcomdlg32 -lgdi32 -#cgo windows,arm64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_windows_arm64 -lmupdfthird_windows_arm64 -lm -lcomdlg32 -lgdi32 -#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_darwin_amd64 -lmupdfthird_darwin_amd64 -lm -#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_darwin_arm64 -lmupdfthird_darwin_arm64 -lm +#include +#include + +const char *fz_version = FZ_VERSION; +#if defined(_WIN32) + typedef unsigned long long store; +#else + typedef unsigned long store; +#endif + +fz_document *open_document(fz_context *ctx, const char *filename) { + fz_document *doc; + + fz_try(ctx) { + doc = fz_open_document(ctx, filename); + } + fz_catch(ctx) { + return NULL; + } + + return doc; +} + +fz_document *open_document_with_stream(fz_context *ctx, const char *magic, fz_stream *stream) { + fz_document *doc; + + fz_try(ctx) { + doc = fz_open_document_with_stream(ctx, magic, stream); + } + fz_catch(ctx) { + return NULL; + } + + return doc; +} + +fz_page *load_page(fz_context *ctx, fz_document *doc, int number) { + fz_page *page; + + fz_try(ctx) { + page = fz_load_page(ctx, doc, number); + } + fz_catch(ctx) { + return NULL; + } + + return page; +} + +int run_page_contents(fz_context *ctx, fz_page *page, fz_device *dev, fz_matrix transform, fz_cookie *cookie) { + fz_try(ctx) { + fz_run_page_contents(ctx, page, dev, transform, cookie); + } + fz_catch(ctx) { + return 0; + } + + return 1; +} */ import "C" + +import ( + "image" + "io" + "os" + "path/filepath" + "sync" + "unsafe" +) + +// Document represents fitz document. +type Document struct { + ctx *C.struct_fz_context + data []byte // binds data to the Document lifecycle avoiding premature GC + doc *C.struct_fz_document + mtx sync.Mutex + stream *C.fz_stream +} + +// New returns new fitz document. +func New(filename string) (f *Document, err error) { + f = &Document{} + + filename, err = filepath.Abs(filename) + if err != nil { + return + } + + if _, e := os.Stat(filename); e != nil { + err = ErrNoSuchFile + return + } + + f.ctx = (*C.struct_fz_context)(unsafe.Pointer(C.fz_new_context_imp(nil, nil, C.store(MaxStore), C.fz_version))) + if f.ctx == nil { + err = ErrCreateContext + return + } + + C.fz_register_document_handlers(f.ctx) + + cfilename := C.CString(filename) + defer C.free(unsafe.Pointer(cfilename)) + + f.doc = C.open_document(f.ctx, cfilename) + if f.doc == nil { + err = ErrOpenDocument + return + } + + ret := C.fz_needs_password(f.ctx, f.doc) + v := int(ret) != 0 + if v { + err = ErrNeedsPassword + } + + return +} + +// NewFromMemory returns new fitz document from byte slice. +func NewFromMemory(b []byte) (f *Document, err error) { + f = &Document{} + + f.ctx = (*C.struct_fz_context)(unsafe.Pointer(C.fz_new_context_imp(nil, nil, C.store(MaxStore), C.fz_version))) + if f.ctx == nil { + err = ErrCreateContext + return + } + + C.fz_register_document_handlers(f.ctx) + + stream := C.fz_open_memory(f.ctx, (*C.uchar)(&b[0]), C.size_t(len(b))) + f.stream = C.fz_keep_stream(f.ctx, stream) + + if f.stream == nil { + err = ErrOpenMemory + return + } + + magic := contentType(b) + if magic == "" { + err = ErrOpenMemory + return + } + + f.data = b + + cmagic := C.CString(magic) + defer C.free(unsafe.Pointer(cmagic)) + + f.doc = C.open_document_with_stream(f.ctx, cmagic, f.stream) + if f.doc == nil { + err = ErrOpenDocument + } + + ret := C.fz_needs_password(f.ctx, f.doc) + v := int(ret) != 0 + if v { + err = ErrNeedsPassword + } + + return +} + +// NewFromReader returns new fitz document from io.Reader. +func NewFromReader(r io.Reader) (f *Document, err error) { + b, e := io.ReadAll(r) + if e != nil { + err = e + return + } + + f, err = NewFromMemory(b) + + return +} + +// NumPage returns total number of pages in document. +func (f *Document) NumPage() int { + return int(C.fz_count_pages(f.ctx, f.doc)) +} + +// Image returns image for given page number. +func (f *Document) Image(pageNumber int) (*image.RGBA, error) { + return f.ImageDPI(pageNumber, 300.0) +} + +// ImageDPI returns image for given page number and DPI. +func (f *Document) ImageDPI(pageNumber int, dpi float64) (*image.RGBA, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return nil, ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return nil, ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + var bounds C.fz_rect + bounds = C.fz_bound_page(f.ctx, page) + + var ctm C.fz_matrix + ctm = C.fz_scale(C.float(dpi/72), C.float(dpi/72)) + + var bbox C.fz_irect + bounds = C.fz_transform_rect(bounds, ctm) + bbox = C.fz_round_rect(bounds) + + pixmap := C.fz_new_pixmap_with_bbox(f.ctx, C.fz_device_rgb(f.ctx), bbox, nil, 1) + if pixmap == nil { + return nil, ErrCreatePixmap + } + + C.fz_clear_pixmap_with_value(f.ctx, pixmap, C.int(0xff)) + defer C.fz_drop_pixmap(f.ctx, pixmap) + + device := C.fz_new_draw_device(f.ctx, ctm, pixmap) + C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) + defer C.fz_drop_device(f.ctx, device) + + drawMatrix := C.fz_identity + ret := C.run_page_contents(f.ctx, page, device, drawMatrix, nil) + if ret == 0 { + return nil, ErrRunPageContents + } + + C.fz_close_device(f.ctx, device) + + pixels := C.fz_pixmap_samples(f.ctx, pixmap) + if pixels == nil { + return nil, ErrPixmapSamples + } + + img := image.NewRGBA(image.Rect(int(bbox.x0), int(bbox.y0), int(bbox.x1), int(bbox.y1))) + copy(img.Pix, C.GoBytes(unsafe.Pointer(pixels), C.int(4*bbox.x1*bbox.y1))) + + return img, nil +} + +// ImagePNG returns image for given page number as PNG bytes. +func (f *Document) ImagePNG(pageNumber int, dpi float64) ([]byte, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return nil, ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return nil, ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + var bounds C.fz_rect + bounds = C.fz_bound_page(f.ctx, page) + + var ctm C.fz_matrix + ctm = C.fz_scale(C.float(dpi/72), C.float(dpi/72)) + + var bbox C.fz_irect + bounds = C.fz_transform_rect(bounds, ctm) + bbox = C.fz_round_rect(bounds) + + pixmap := C.fz_new_pixmap_with_bbox(f.ctx, C.fz_device_rgb(f.ctx), bbox, nil, 1) + if pixmap == nil { + return nil, ErrCreatePixmap + } + + C.fz_clear_pixmap_with_value(f.ctx, pixmap, C.int(0xff)) + defer C.fz_drop_pixmap(f.ctx, pixmap) + + device := C.fz_new_draw_device(f.ctx, ctm, pixmap) + C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) + defer C.fz_drop_device(f.ctx, device) + + drawMatrix := C.fz_identity + ret := C.run_page_contents(f.ctx, page, device, drawMatrix, nil) + if ret == 0 { + return nil, ErrRunPageContents + } + + C.fz_close_device(f.ctx, device) + + buf := C.fz_new_buffer_from_pixmap_as_png(f.ctx, pixmap, C.fz_default_color_params) + defer C.fz_drop_buffer(f.ctx, buf) + + size := C.fz_buffer_storage(f.ctx, buf, nil) + str := C.GoStringN(C.fz_string_from_buffer(f.ctx, buf), C.int(size)) + + return []byte(str), nil +} + +// Links returns slice of links for given page number. +func (f *Document) Links(pageNumber int) ([]Link, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return nil, ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return nil, ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + links := C.fz_load_links(f.ctx, page) + defer C.fz_drop_link(f.ctx, links) + + linkCount := 0 + for currLink := links; currLink != nil; currLink = currLink.next { + linkCount++ + } + + if linkCount == 0 { + return nil, nil + } + + gLinks := make([]Link, linkCount) + + currLink := links + for i := 0; i < linkCount; i++ { + gLinks[i] = Link{ + URI: C.GoString(currLink.uri), + } + currLink = currLink.next + } + + return gLinks, nil +} + +// Text returns text for given page number. +func (f *Document) Text(pageNumber int) (string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return "", ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return "", ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + var bounds C.fz_rect + bounds = C.fz_bound_page(f.ctx, page) + + var ctm C.fz_matrix + ctm = C.fz_scale(C.float(72.0/72), C.float(72.0/72)) + + text := C.fz_new_stext_page(f.ctx, bounds) + defer C.fz_drop_stext_page(f.ctx, text) + + var opts C.fz_stext_options + opts.flags = 0 + + device := C.fz_new_stext_device(f.ctx, text, &opts) + C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) + defer C.fz_drop_device(f.ctx, device) + + var cookie C.fz_cookie + ret := C.run_page_contents(f.ctx, page, device, ctm, &cookie) + if ret == 0 { + return "", ErrRunPageContents + } + + C.fz_close_device(f.ctx, device) + + buf := C.fz_new_buffer_from_stext_page(f.ctx, text) + defer C.fz_drop_buffer(f.ctx, buf) + + str := C.GoString(C.fz_string_from_buffer(f.ctx, buf)) + + return str, nil +} + +// HTML returns html for given page number. +func (f *Document) HTML(pageNumber int, header bool) (string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return "", ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return "", ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + var bounds C.fz_rect + bounds = C.fz_bound_page(f.ctx, page) + + var ctm C.fz_matrix + ctm = C.fz_scale(C.float(72.0/72), C.float(72.0/72)) + + text := C.fz_new_stext_page(f.ctx, bounds) + defer C.fz_drop_stext_page(f.ctx, text) + + var opts C.fz_stext_options + opts.flags = C.FZ_STEXT_PRESERVE_IMAGES + + device := C.fz_new_stext_device(f.ctx, text, &opts) + C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) + defer C.fz_drop_device(f.ctx, device) + + var cookie C.fz_cookie + ret := C.run_page_contents(f.ctx, page, device, ctm, &cookie) + if ret == 0 { + return "", ErrRunPageContents + } + + C.fz_close_device(f.ctx, device) + + buf := C.fz_new_buffer(f.ctx, 1024) + defer C.fz_drop_buffer(f.ctx, buf) + + out := C.fz_new_output_with_buffer(f.ctx, buf) + defer C.fz_drop_output(f.ctx, out) + + if header { + C.fz_print_stext_header_as_html(f.ctx, out) + } + C.fz_print_stext_page_as_html(f.ctx, out, text, C.int(pageNumber)) + if header { + C.fz_print_stext_trailer_as_html(f.ctx, out) + } + + C.fz_close_output(f.ctx, out) + + str := C.GoString(C.fz_string_from_buffer(f.ctx, buf)) + + return str, nil +} + +// SVG returns svg document for given page number. +func (f *Document) SVG(pageNumber int) (string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return "", ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return "", ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + var bounds C.fz_rect + bounds = C.fz_bound_page(f.ctx, page) + + var ctm C.fz_matrix + ctm = C.fz_scale(C.float(72.0/72), C.float(72.0/72)) + bounds = C.fz_transform_rect(bounds, ctm) + + buf := C.fz_new_buffer(f.ctx, 1024) + defer C.fz_drop_buffer(f.ctx, buf) + + out := C.fz_new_output_with_buffer(f.ctx, buf) + defer C.fz_drop_output(f.ctx, out) + + device := C.fz_new_svg_device(f.ctx, out, bounds.x1-bounds.x0, bounds.y1-bounds.y0, C.FZ_SVG_TEXT_AS_PATH, 1) + C.fz_enable_device_hints(f.ctx, device, C.FZ_NO_CACHE) + defer C.fz_drop_device(f.ctx, device) + + var cookie C.fz_cookie + ret := C.run_page_contents(f.ctx, page, device, ctm, &cookie) + if ret == 0 { + return "", ErrRunPageContents + } + + C.fz_close_device(f.ctx, device) + C.fz_close_output(f.ctx, out) + + str := C.GoString(C.fz_string_from_buffer(f.ctx, buf)) + + return str, nil +} + +// ToC returns the table of contents (also known as outline). +func (f *Document) ToC() ([]Outline, error) { + data := make([]Outline, 0) + + outline := C.fz_load_outline(f.ctx, f.doc) + if outline == nil { + return nil, ErrLoadOutline + } + defer C.fz_drop_outline(f.ctx, outline) + + var walk func(outline *C.fz_outline, level int) + + walk = func(outline *C.fz_outline, level int) { + for outline != nil { + res := Outline{} + res.Level = level + res.Title = C.GoString(outline.title) + res.URI = C.GoString(outline.uri) + res.Page = int(outline.page.page) + res.Top = float64(outline.y) + data = append(data, res) + + if outline.down != nil { + walk(outline.down, level+1) + } + outline = outline.next + } + } + + walk(outline, 1) + return data, nil +} + +// Metadata returns the map with standard metadata. +func (f *Document) Metadata() map[string]string { + data := make(map[string]string) + + lookup := func(key string) string { + ckey := C.CString(key) + defer C.free(unsafe.Pointer(ckey)) + + buf := make([]byte, 256) + C.fz_lookup_metadata(f.ctx, f.doc, ckey, (*C.char)(unsafe.Pointer(&buf[0])), C.int(len(buf))) + + return string(buf) + } + + data["format"] = lookup("format") + data["encryption"] = lookup("encryption") + data["title"] = lookup("info:Title") + data["author"] = lookup("info:Author") + data["subject"] = lookup("info:Subject") + data["keywords"] = lookup("info:Keywords") + data["creator"] = lookup("info:Creator") + data["producer"] = lookup("info:Producer") + data["creationDate"] = lookup("info:CreationDate") + data["modDate"] = lookup("info:modDate") + + return data +} + +// Bound gives the Bounds of a given Page in the document. +func (f *Document) Bound(pageNumber int) (image.Rectangle, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return image.Rectangle{}, ErrPageMissing + } + + page := C.load_page(f.ctx, f.doc, C.int(pageNumber)) + if page == nil { + return image.Rectangle{}, ErrLoadPage + } + + defer C.fz_drop_page(f.ctx, page) + + var bounds C.fz_rect + bounds = C.fz_bound_page(f.ctx, page) + + return image.Rect(int(bounds.x0), int(bounds.y0), int(bounds.x1), int(bounds.y1)), nil +} + +// Close closes the underlying fitz document. +func (f *Document) Close() error { + if f.stream != nil { + C.fz_drop_stream(f.ctx, f.stream) + } + + C.fz_drop_document(f.ctx, f.doc) + C.fz_drop_context(f.ctx) + + f.data = nil + + return nil +} diff --git a/fitz_cgo_cgo.go b/fitz_cgo_cgo.go new file mode 100644 index 0000000..e99becb --- /dev/null +++ b/fitz_cgo_cgo.go @@ -0,0 +1,18 @@ +//go:build cgo && !nocgo && !extlib + +package fitz + +/* +#cgo CFLAGS: -Iinclude + +#cgo linux,amd64,!musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_amd64 -lmupdfthird_linux_amd64 -lm +#cgo linux,amd64,musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_amd64_musl -lmupdfthird_linux_amd64_musl -lm +#cgo linux,!android,arm64,!musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_arm64 -lmupdfthird_linux_arm64 -lm +#cgo linux,!android,arm64,musl LDFLAGS: -L${SRCDIR}/libs -lmupdf_linux_arm64_musl -lmupdfthird_linux_arm64_musl -lm +#cgo android,arm64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_android_arm64 -lmupdfthird_android_arm64 -lm -llog +#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_windows_amd64 -lmupdfthird_windows_amd64 -lm -lcomdlg32 -lgdi32 +#cgo windows,arm64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_windows_arm64 -lmupdfthird_windows_arm64 -lm -lcomdlg32 -lgdi32 +#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_darwin_amd64 -lmupdfthird_darwin_amd64 -lm +#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/libs -lmupdf_darwin_arm64 -lmupdfthird_darwin_arm64 -lm +*/ +import "C" diff --git a/fitz_cgo_extlib.go b/fitz_cgo_extlib.go index c7d5f7e..96a20ed 100644 --- a/fitz_cgo_extlib.go +++ b/fitz_cgo_extlib.go @@ -1,4 +1,4 @@ -//go:build extlib && !pkgconfig +//go:build cgo && !nocgo && extlib && !pkgconfig package fitz diff --git a/fitz_cgo_extlib_pkgconfig.go b/fitz_cgo_extlib_pkgconfig.go index 8df56c6..fa48212 100644 --- a/fitz_cgo_extlib_pkgconfig.go +++ b/fitz_cgo_extlib_pkgconfig.go @@ -1,4 +1,4 @@ -//go:build extlib && pkgconfig +//go:build cgo && !nocgo && extlib && pkgconfig package fitz diff --git a/fitz_nocgo.go b/fitz_nocgo.go new file mode 100644 index 0000000..44a5d89 --- /dev/null +++ b/fitz_nocgo.go @@ -0,0 +1,1073 @@ +//go:build !cgo || nocgo + +package fitz + +import ( + "image" + "io" + "os" + "path/filepath" + "sync" + "unsafe" + + "github.com/ebitengine/purego" + "github.com/jupiterrider/ffi" +) + +// Document represents fitz document. +type Document struct { + ctx *fzContext + data []byte // binds data to the Document lifecycle avoiding premature GC + doc *fzDocument + mtx sync.Mutex + stream *fzStream +} + +// New returns new fitz document. +func New(filename string) (f *Document, err error) { + f = &Document{} + + filename, err = filepath.Abs(filename) + if err != nil { + return + } + + if _, e := os.Stat(filename); e != nil { + err = ErrNoSuchFile + return + } + + f.ctx = fzNewContextImp(nil, nil, uint64(MaxStore), FzVersion) + if f.ctx == nil { + err = ErrCreateContext + return + } + + fzRegisterDocumentHandlers(f.ctx) + + f.doc = fzOpenDocument(f.ctx, filename) + if f.doc == nil { + err = ErrOpenDocument + return + } + + ret := fzNeedsPassword(f.ctx, f.doc) + v := int(ret) != 0 + if v { + err = ErrNeedsPassword + } + + return +} + +// NewFromMemory returns new fitz document from byte slice. +func NewFromMemory(b []byte) (f *Document, err error) { + f = &Document{} + + f.ctx = fzNewContextImp(nil, nil, uint64(MaxStore), FzVersion) + if f.ctx == nil { + err = ErrCreateContext + return + } + + fzRegisterDocumentHandlers(f.ctx) + + stream := fzOpenMemory(f.ctx, unsafe.SliceData(b), uint64(len(b))) + f.stream = fzKeepStream(f.ctx, stream) + + if f.stream == nil { + err = ErrOpenMemory + return + } + + magic := contentType(b) + if magic == "" { + err = ErrOpenMemory + return + } + + f.data = b + + f.doc = fzOpenDocumentWithStream(f.ctx, magic, f.stream) + if f.doc == nil { + err = ErrOpenDocument + } + + ret := fzNeedsPassword(f.ctx, f.doc) + v := int(ret) != 0 + if v { + err = ErrNeedsPassword + } + + return +} + +// NewFromReader returns new fitz document from io.Reader. +func NewFromReader(r io.Reader) (f *Document, err error) { + b, e := io.ReadAll(r) + if e != nil { + err = e + return + } + + f, err = NewFromMemory(b) + + return +} + +// NumPage returns total number of pages in document. +func (f *Document) NumPage() int { + return fzCountPages(f.ctx, f.doc) +} + +// Image returns image for given page number. +func (f *Document) Image(pageNumber int) (*image.RGBA, error) { + return f.ImageDPI(pageNumber, 300.0) +} + +// ImageDPI returns image for given page number and DPI. +func (f *Document) ImageDPI(pageNumber int, dpi float64) (*image.RGBA, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return nil, ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return nil, ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + var bounds fzRect + bounds = boundPage(f.ctx, page) + + var ctm fzMatrix + ctm = scale(float32(dpi/72), float32(dpi/72)) + + var bbox fzIRect + bounds = transformRect(bounds, ctm) + bbox = roundRect(bounds) + + pixmap := fzNewPixmap(f.ctx, fzDeviceRgb(f.ctx), int(bbox.X1), int(bbox.Y1), nil, 1) + if pixmap == nil { + return nil, ErrCreatePixmap + } + + fzClearPixmapWithValue(f.ctx, pixmap, 0xff) + defer fzDropPixmap(f.ctx, pixmap) + + device := newDrawDevice(f.ctx, ctm, pixmap) + fzEnableDeviceHints(f.ctx, device, fzNoCache) + defer fzDropDevice(f.ctx, device) + + runPageContents(f.ctx, page, device, fzIdentity) + + fzCloseDevice(f.ctx, device) + + pixels := fzPixmapSamples(f.ctx, pixmap) + if pixels == nil { + return nil, ErrPixmapSamples + } + + img := image.NewRGBA(image.Rect(int(bbox.X0), int(bbox.Y0), int(bbox.X1), int(bbox.Y1))) + copy(img.Pix, unsafe.Slice(pixels, 4*bbox.X1*bbox.Y1)) + + return img, nil +} + +// ImagePNG returns image for given page number as PNG bytes. +func (f *Document) ImagePNG(pageNumber int, dpi float64) ([]byte, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return nil, ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return nil, ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + var bounds fzRect + bounds = boundPage(f.ctx, page) + + var ctm fzMatrix + ctm = scale(float32(dpi/72), float32(dpi/72)) + + var bbox fzIRect + bounds = transformRect(bounds, ctm) + bbox = roundRect(bounds) + + pixmap := fzNewPixmap(f.ctx, fzDeviceRgb(f.ctx), int(bbox.X1), int(bbox.Y1), nil, 1) + if pixmap == nil { + return nil, ErrCreatePixmap + } + + fzClearPixmapWithValue(f.ctx, pixmap, 0xff) + defer fzDropPixmap(f.ctx, pixmap) + + device := newDrawDevice(f.ctx, ctm, pixmap) + fzEnableDeviceHints(f.ctx, device, fzNoCache) + defer fzDropDevice(f.ctx, device) + + runPageContents(f.ctx, page, device, fzIdentity) + + fzCloseDevice(f.ctx, device) + + params := fzColorParams{1, 1, 0, 0} + buf := newBufferFromPixmapAsPNG(f.ctx, pixmap, params) + defer fzDropBuffer(f.ctx, buf) + + size := fzBufferStorage(f.ctx, buf, nil) + + ret := make([]byte, size) + copy(ret, unsafe.Slice(fzStringFromBuffer(f.ctx, buf), size)) + + return ret, nil +} + +// Links returns slice of links for given page number. +func (f *Document) Links(pageNumber int) ([]Link, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return nil, ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return nil, ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + links := fzLoadLinks(f.ctx, page) + defer fzDropLink(f.ctx, links) + + linkCount := 0 + for currLink := links; currLink != nil; currLink = currLink.Next { + linkCount++ + } + + if linkCount == 0 { + return nil, nil + } + + gLinks := make([]Link, linkCount) + + currLink := links + for i := 0; i < linkCount; i++ { + gLinks[i] = Link{ + URI: bytePtrToString((*uint8)(unsafe.Pointer(currLink.Uri))), + } + currLink = currLink.Next + } + + return gLinks, nil +} + +// Text returns text for given page number. +func (f *Document) Text(pageNumber int) (string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return "", ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return "", ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + var bounds fzRect + bounds = boundPage(f.ctx, page) + + var ctm fzMatrix + ctm = scale(float32(72.0/72), float32(72.0/72)) + + text := newStextPage(f.ctx, bounds) + defer fzDropStextPage(f.ctx, text) + + var opts fzStextOptions + opts.Flags = 0 + + device := fzNewStextDevice(f.ctx, text, &opts) + fzEnableDeviceHints(f.ctx, device, fzNoCache) + defer fzDropDevice(f.ctx, device) + + runPageContents(f.ctx, page, device, ctm) + + fzCloseDevice(f.ctx, device) + + buf := fzNewBufferFromStextPage(f.ctx, text) + defer fzDropBuffer(f.ctx, buf) + + ret := fzStringFromBuffer(f.ctx, buf) + + return bytePtrToString(ret), nil +} + +// HTML returns html for given page number. +func (f *Document) HTML(pageNumber int, header bool) (string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return "", ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return "", ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + var bounds fzRect + bounds = boundPage(f.ctx, page) + + var ctm fzMatrix + ctm = scale(float32(72.0/72), float32(72.0/72)) + + text := newStextPage(f.ctx, bounds) + defer fzDropStextPage(f.ctx, text) + + var opts fzStextOptions + opts.Flags = fzStextPreserveImages + + device := fzNewStextDevice(f.ctx, text, &opts) + fzEnableDeviceHints(f.ctx, device, fzNoCache) + defer fzDropDevice(f.ctx, device) + + runPageContents(f.ctx, page, device, ctm) + + fzCloseDevice(f.ctx, device) + + buf := fzNewBuffer(f.ctx, 1024) + defer fzDropBuffer(f.ctx, buf) + + out := fzNewOutputWithBuffer(f.ctx, buf) + defer fzDropOutput(f.ctx, out) + + if header { + fzPrintStextHeaderAsHTML(f.ctx, out) + } + fzPrintStextPageAsHTML(f.ctx, out, text, pageNumber) + if header { + fzPrintStextTrailerAsHTML(f.ctx, out) + } + + fzCloseOutput(f.ctx, out) + + ret := fzStringFromBuffer(f.ctx, buf) + + return bytePtrToString(ret), nil +} + +// SVG returns svg document for given page number. +func (f *Document) SVG(pageNumber int) (string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return "", ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return "", ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + var bounds fzRect + bounds = boundPage(f.ctx, page) + + var ctm fzMatrix + ctm = scale(float32(72.0/72), float32(72.0/72)) + bounds = transformRect(bounds, ctm) + + buf := fzNewBuffer(f.ctx, 1024) + defer fzDropBuffer(f.ctx, buf) + + out := fzNewOutputWithBuffer(f.ctx, buf) + defer fzDropOutput(f.ctx, out) + + device := newSvgDevice(f.ctx, out, bounds.X1-bounds.X0, bounds.Y1-bounds.Y0, fzSvgTextAsPath, 1) + fzEnableDeviceHints(f.ctx, device, fzNoCache) + defer fzDropDevice(f.ctx, device) + + runPageContents(f.ctx, page, device, ctm) + + fzCloseDevice(f.ctx, device) + fzCloseOutput(f.ctx, out) + + ret := fzStringFromBuffer(f.ctx, buf) + + return bytePtrToString(ret), nil +} + +// ToC returns the table of contents (also known as outline). +func (f *Document) ToC() ([]Outline, error) { + data := make([]Outline, 0) + + outline := fzLoadOutline(f.ctx, f.doc) + if outline == nil { + return nil, ErrLoadOutline + } + + defer fzDropOutline(f.ctx, outline) + + var walk func(outline *fzOutline, level int) + + walk = func(outline *fzOutline, level int) { + for outline != nil { + res := Outline{} + res.Level = level + res.Title = bytePtrToString((*uint8)(unsafe.Pointer(outline.Title))) + res.URI = bytePtrToString((*uint8)(unsafe.Pointer(outline.Uri))) + res.Page = int(outline.Page.Page) + res.Top = float64(outline.Y) + data = append(data, res) + + if outline.Down != nil { + walk(outline.Down, level+1) + } + outline = outline.Next + } + } + + walk(outline, 1) + + return data, nil +} + +// Metadata returns the map with standard metadata. +func (f *Document) Metadata() map[string]string { + data := make(map[string]string) + + lookup := func(key string) string { + buf := make([]byte, 256) + fzLookupMetadata(f.ctx, f.doc, key, unsafe.SliceData(buf), len(buf)) + + return string(buf) + } + + data["format"] = lookup("format") + data["encryption"] = lookup("encryption") + data["title"] = lookup("info:Title") + data["author"] = lookup("info:Author") + data["subject"] = lookup("info:Subject") + data["keywords"] = lookup("info:Keywords") + data["creator"] = lookup("info:Creator") + data["producer"] = lookup("info:Producer") + data["creationDate"] = lookup("info:CreationDate") + data["modDate"] = lookup("info:modDate") + + return data +} + +// Bound gives the Bounds of a given Page in the document. +func (f *Document) Bound(pageNumber int) (image.Rectangle, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + if pageNumber >= f.NumPage() { + return image.Rectangle{}, ErrPageMissing + } + + page := fzLoadPage(f.ctx, f.doc, pageNumber) + if page == nil { + return image.Rectangle{}, ErrLoadPage + } + + defer fzDropPage(f.ctx, page) + + var bounds fzRect + bounds = boundPage(f.ctx, page) + + return image.Rect(int(bounds.X0), int(bounds.Y0), int(bounds.X1), int(bounds.Y1)), nil +} + +// Close closes the underlying fitz document. +func (f *Document) Close() error { + if f.stream != nil { + fzDropStream(f.ctx, f.stream) + } + + fzDropDocument(f.ctx, f.doc) + fzDropContext(f.ctx) + + f.data = nil + + return nil +} + +var ( + libmupdf uintptr + + fzBoundPage *bundle + fzTransformRect *bundle + fzRoundRect *bundle + fzScale *bundle + fzNewDrawDevice *bundle + fzRunPageContents *bundle + fzNewBufferFromPixmapAsPNG *bundle + fzNewStextPage *bundle + fzNewSvgDevice *bundle + + fzNewContextImp func(alloc *fzAllocContext, locks *fzLocksContext, maxStore uint64, version string) *fzContext + fzDropContext func(ctx *fzContext) + fzOpenDocument func(ctx *fzContext, filename string) *fzDocument + fzOpenDocumentWithStream func(ctx *fzContext, magic string, stream *fzStream) *fzDocument + fzOpenMemory func(ctx *fzContext, data *uint8, len uint64) *fzStream + fzKeepStream func(ctx *fzContext, stm *fzStream) *fzStream + fzDropStream func(ctx *fzContext, stm *fzStream) + fzRegisterDocumentHandlers func(ctx *fzContext) + fzNeedsPassword func(ctx *fzContext, doc *fzDocument) int + fzDropDocument func(ctx *fzContext, doc *fzDocument) + fzCountPages func(ctx *fzContext, doc *fzDocument) int + fzLoadPage func(ctx *fzContext, doc *fzDocument, number int) *fzPage + fzDropPage func(ctx *fzContext, page *fzPage) + fzNewPixmap func(ctx *fzContext, colorspace *fzColorspace, w, h int, seps *fzSeparations, alpha int) *fzPixmap + fzDropPixmap func(ctx *fzContext, pix *fzPixmap) + fzPixmapSamples func(ctx *fzContext, pix *fzPixmap) *uint8 + fzClearPixmapWithValue func(ctx *fzContext, pix *fzPixmap, value int) + fzEnableDeviceHints func(ctx *fzContext, dev *fzDevice, hints int) + fzDropDevice func(ctx *fzContext, dev *fzDevice) + fzCloseDevice func(ctx *fzContext, dev *fzDevice) + fzDeviceRgb func(ctx *fzContext) *fzColorspace + fzNewBuffer func(ctx *fzContext, size uint64) *fzBuffer + fzDropBuffer func(ctx *fzContext, buf *fzBuffer) + fzBufferStorage func(ctx *fzContext, buf *fzBuffer, data **uint8) uint64 + fzStringFromBuffer func(ctx *fzContext, buf *fzBuffer) *uint8 + fzLoadLinks func(ctx *fzContext, page *fzPage) *fzLink + fzDropLink func(ctx *fzContext, link *fzLink) + fzDropStextPage func(ctx *fzContext, page *fzStextPage) + fzNewStextDevice func(ctx *fzContext, page *fzStextPage, options *fzStextOptions) *fzDevice + fzNewBufferFromStextPage func(ctx *fzContext, page *fzStextPage) *fzBuffer + fzLookupMetadata func(ctx *fzContext, doc *fzDocument, key string, buf *uint8, size int) int + fzLoadOutline func(ctx *fzContext, doc *fzDocument) *fzOutline + fzDropOutline func(ctx *fzContext, outline *fzOutline) + fzNewOutputWithBuffer func(ctx *fzContext, buf *fzBuffer) *fzOutput + fzDropOutput func(ctx *fzContext, out *fzOutput) + fzCloseOutput func(ctx *fzContext, out *fzOutput) + fzPrintStextPageAsHTML func(ctx *fzContext, out *fzOutput, page *fzStextPage, id int) + fzPrintStextHeaderAsHTML func(ctx *fzContext, out *fzOutput) + fzPrintStextTrailerAsHTML func(ctx *fzContext, out *fzOutput) +) + +func init() { + libmupdf = loadLibrary() + + if os.Getenv("FZ_VERSION") != "" { + FzVersion = os.Getenv("FZ_VERSION") + } + + fzBoundPage = newBundle("fz_bound_page", &typeFzRect, &ffi.TypePointer, &ffi.TypePointer) + fzTransformRect = newBundle("fz_transform_rect", &typeFzRect, &typeFzRect, &typeFzMatrix) + fzRoundRect = newBundle("fz_round_rect", &typeFzIRect, &typeFzRect) + fzScale = newBundle("fz_scale", &typeFzMatrix, &ffi.TypeFloat, &ffi.TypeFloat) + fzNewDrawDevice = newBundle("fz_new_draw_device", &ffi.TypePointer, &ffi.TypePointer, &typeFzMatrix, &ffi.TypePointer) + fzRunPageContents = newBundle("fz_run_page_contents", &ffi.TypeVoid, &ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer, &typeFzMatrix, &ffi.TypePointer) + fzNewBufferFromPixmapAsPNG = newBundle("fz_new_buffer_from_pixmap_as_png", &ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer, &typeFzColorParams) + fzNewStextPage = newBundle("fz_new_stext_page", &ffi.TypePointer, &ffi.TypePointer, &typeFzRect) + fzNewSvgDevice = newBundle("fz_new_svg_device", &ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer, &ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeSint32, &ffi.TypeSint32) + + purego.RegisterLibFunc(&fzNewContextImp, libmupdf, "fz_new_context_imp") + purego.RegisterLibFunc(&fzDropContext, libmupdf, "fz_drop_context") + purego.RegisterLibFunc(&fzOpenDocument, libmupdf, "fz_open_document") + purego.RegisterLibFunc(&fzOpenDocumentWithStream, libmupdf, "fz_open_document_with_stream") + purego.RegisterLibFunc(&fzOpenMemory, libmupdf, "fz_open_memory") + purego.RegisterLibFunc(&fzKeepStream, libmupdf, "fz_keep_stream") + purego.RegisterLibFunc(&fzDropStream, libmupdf, "fz_drop_stream") + purego.RegisterLibFunc(&fzRegisterDocumentHandlers, libmupdf, "fz_register_document_handlers") + purego.RegisterLibFunc(&fzNeedsPassword, libmupdf, "fz_needs_password") + purego.RegisterLibFunc(&fzDropDocument, libmupdf, "fz_drop_document") + purego.RegisterLibFunc(&fzCountPages, libmupdf, "fz_count_pages") + purego.RegisterLibFunc(&fzLoadPage, libmupdf, "fz_load_page") + purego.RegisterLibFunc(&fzDropPage, libmupdf, "fz_drop_page") + purego.RegisterLibFunc(&fzNewPixmap, libmupdf, "fz_new_pixmap") + purego.RegisterLibFunc(&fzDropPixmap, libmupdf, "fz_drop_pixmap") + purego.RegisterLibFunc(&fzPixmapSamples, libmupdf, "fz_pixmap_samples") + purego.RegisterLibFunc(&fzClearPixmapWithValue, libmupdf, "fz_clear_pixmap_with_value") + purego.RegisterLibFunc(&fzEnableDeviceHints, libmupdf, "fz_enable_device_hints") + purego.RegisterLibFunc(&fzDropDevice, libmupdf, "fz_drop_device") + purego.RegisterLibFunc(&fzCloseDevice, libmupdf, "fz_close_device") + purego.RegisterLibFunc(&fzDeviceRgb, libmupdf, "fz_device_rgb") + purego.RegisterLibFunc(&fzNewBuffer, libmupdf, "fz_new_buffer") + purego.RegisterLibFunc(&fzDropBuffer, libmupdf, "fz_drop_buffer") + purego.RegisterLibFunc(&fzBufferStorage, libmupdf, "fz_buffer_storage") + purego.RegisterLibFunc(&fzStringFromBuffer, libmupdf, "fz_string_from_buffer") + purego.RegisterLibFunc(&fzLoadLinks, libmupdf, "fz_load_links") + purego.RegisterLibFunc(&fzDropLink, libmupdf, "fz_drop_link") + purego.RegisterLibFunc(&fzDropStextPage, libmupdf, "fz_drop_stext_page") + purego.RegisterLibFunc(&fzNewStextDevice, libmupdf, "fz_new_stext_device") + purego.RegisterLibFunc(&fzNewBufferFromStextPage, libmupdf, "fz_new_buffer_from_stext_page") + purego.RegisterLibFunc(&fzLookupMetadata, libmupdf, "fz_lookup_metadata") + purego.RegisterLibFunc(&fzLoadOutline, libmupdf, "fz_load_outline") + purego.RegisterLibFunc(&fzDropOutline, libmupdf, "fz_drop_outline") + purego.RegisterLibFunc(&fzNewOutputWithBuffer, libmupdf, "fz_new_output_with_buffer") + purego.RegisterLibFunc(&fzDropOutput, libmupdf, "fz_drop_output") + purego.RegisterLibFunc(&fzCloseOutput, libmupdf, "fz_close_output") + purego.RegisterLibFunc(&fzPrintStextPageAsHTML, libmupdf, "fz_print_stext_page_as_html") + purego.RegisterLibFunc(&fzPrintStextHeaderAsHTML, libmupdf, "fz_print_stext_header_as_html") + purego.RegisterLibFunc(&fzPrintStextTrailerAsHTML, libmupdf, "fz_print_stext_trailer_as_html") +} + +type bundle struct { + sym uintptr + cif ffi.Cif +} + +func (b *bundle) call(rValue unsafe.Pointer, aValues ...unsafe.Pointer) { + ffi.Call(&b.cif, b.sym, rValue, aValues...) +} + +func newBundle(name string, rType *ffi.Type, aTypes ...*ffi.Type) *bundle { + b := new(bundle) + var err error + + if b.sym, err = purego.Dlsym(libmupdf, name); err != nil { + panic(err) + } + + nArgs := uint32(len(aTypes)) + + if status := ffi.PrepCif(&b.cif, ffi.DefaultAbi, nArgs, rType, aTypes...); status != ffi.OK { + panic(status) + } + + return b +} + +var typeFzRect = ffi.Type{Type: ffi.Struct, Elements: &[]*ffi.Type{&ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeFloat, nil}[0]} +var typeFzIRect = ffi.Type{Type: ffi.Struct, Elements: &[]*ffi.Type{&ffi.TypeSint32, &ffi.TypeSint32, &ffi.TypeSint32, &ffi.TypeSint32, nil}[0]} +var typeFzMatrix = ffi.Type{Type: ffi.Struct, Elements: &[]*ffi.Type{&ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeFloat, &ffi.TypeFloat, nil}[0]} +var typeFzColorParams = ffi.Type{Type: ffi.Struct, Elements: &[]*ffi.Type{&ffi.TypeUint8, &ffi.TypeUint8, &ffi.TypeUint8, &ffi.TypeUint8, nil}[0]} + +func boundPage(ctx *fzContext, page *fzPage) fzRect { + var ret fzRect + fzBoundPage.call(unsafe.Pointer(&ret), unsafe.Pointer(&ctx), unsafe.Pointer(&page)) + + return ret +} + +func transformRect(rect fzRect, m fzMatrix) fzRect { + var ret fzRect + fzTransformRect.call(unsafe.Pointer(&ret), unsafe.Pointer(&rect), unsafe.Pointer(&m)) + + return ret +} + +func roundRect(rect fzRect) fzIRect { + var ret fzIRect + fzRoundRect.call(unsafe.Pointer(&ret), unsafe.Pointer(&rect)) + + return ret +} + +func scale(sx, sy float32) fzMatrix { + var ret fzMatrix + fzScale.call(unsafe.Pointer(&ret), unsafe.Pointer(&sx), unsafe.Pointer(&sy)) + + return ret +} + +func newDrawDevice(ctx *fzContext, transform fzMatrix, dest *fzPixmap) *fzDevice { + var ret *fzDevice + fzNewDrawDevice.call(unsafe.Pointer(&ret), unsafe.Pointer(&ctx), unsafe.Pointer(&transform), unsafe.Pointer(&dest)) + + return ret +} + +func runPageContents(ctx *fzContext, page *fzPage, dev *fzDevice, transform fzMatrix) { + var cookie fzCookie + fzRunPageContents.call(nil, unsafe.Pointer(&ctx), unsafe.Pointer(&page), unsafe.Pointer(&dev), unsafe.Pointer(&transform), unsafe.Pointer(&cookie)) +} + +func newBufferFromPixmapAsPNG(ctx *fzContext, pix *fzPixmap, params fzColorParams) *fzBuffer { + var ret *fzBuffer + fzNewBufferFromPixmapAsPNG.call(unsafe.Pointer(&ret), unsafe.Pointer(&ctx), unsafe.Pointer(&pix), unsafe.Pointer(¶ms)) + + return ret +} +func newStextPage(ctx *fzContext, mediabox fzRect) *fzStextPage { + var ret *fzStextPage + fzNewStextPage.call(unsafe.Pointer(&ret), unsafe.Pointer(&ctx), unsafe.Pointer(&mediabox)) + + return ret +} + +func newSvgDevice(ctx *fzContext, out *fzOutput, pageWidth, pageHeight float32, textFormat, reuseImages int) *fzDevice { + var ret *fzDevice + fzNewSvgDevice.call(unsafe.Pointer(&ret), unsafe.Pointer(&ctx), unsafe.Pointer(&out), unsafe.Pointer(&pageWidth), unsafe.Pointer(&pageHeight), unsafe.Pointer(&textFormat), unsafe.Pointer(&reuseImages)) + + return ret +} + +const ( + fzNoCache = 2 + fzStextPreserveImages = 4 + fzSvgTextAsPath = 0 +) + +var fzIdentity = fzMatrix{A: 1, B: 0, C: 0, D: 1, E: 0, F: 0} + +type fzContext struct { + User *byte + Alloc fzAllocContext + Locks fzLocksContext + Error fzErrorContext + Warn fzWarnContext + Aa fzAaContext + Seed48 [7]uint16 + IccEnabled int32 + ThrowOnRepair int32 + Handler *fzDocumentHandlerContext + Archive *fzArchiveHandlerContext + Style *fzStyleContext + Tuning *fzTuningContext + StdDbg *fzOutput + Font *fzFontContext + Colorspace *fzColorspaceContext + Store *fzStore + GlyphCache *fzGlyphCache +} + +type fzDocument struct { + Refs int32 + DropDocument *[0]byte + NeedsPassword *[0]byte + AuthenticatePassword *[0]byte + HasPermission *[0]byte + LoadOutline *[0]byte + OutlineIterator *[0]byte + Layout *[0]byte + MakeBookmark *[0]byte + LookupBookmark *[0]byte + ResolveLinkDest *[0]byte + FormatLinkUri *[0]byte + CountChapters *[0]byte + CountPages *[0]byte + LoadPage *[0]byte + PageLabel *[0]byte + LookupMetadata *[0]byte + SetMetadata *[0]byte + GetOutputIntent *[0]byte + OutputAccelerator *[0]byte + RunStructure *[0]byte + AsPdf *[0]byte + DidLayout int32 + IsReflowable int32 + Open *fzPage +} + +type fzOutline struct { + Refs int32 + Title *int8 + Uri *int8 + Page fzLocation + X float32 + Y float32 + Next *fzOutline + Down *fzOutline + Open int32 + _ [4]byte +} + +type fzPage struct { + Refs int32 + Doc *fzDocument + Chapter int32 + Number int32 + Incomplete int32 + DropPage *[0]byte + BoundPage *[0]byte + RunPageContents *[0]byte + RunPageAnnots *[0]byte + RunPageWidgets *[0]byte + LoadLinks *[0]byte + PagePresentation *[0]byte + ControlSeparation *[0]byte + SeparationDisabled *[0]byte + Separations *[0]byte + Overprint *[0]byte + CreateLink *[0]byte + DeleteLink *[0]byte + Prev **fzPage + Next *fzPage +} + +type fzOutput struct { + State *byte + Write *[0]byte + Seek *[0]byte + Tell *[0]byte + Close *[0]byte + Drop *[0]byte + Reset *[0]byte + Stream *[0]byte + Truncate *[0]byte + Closed int32 + Bp *int8 + Wp *int8 + Ep *int8 + Buffered int32 + Bits int32 +} + +type fzLocation struct { + Chapter int32 + Page int32 +} + +type fzStream struct { + Refs int32 + Error int32 + Eof int32 + Progressive int32 + Pos int64 + Avail int32 + Bits int32 + Rp *uint8 + Wp *uint8 + State *byte + Next *[0]byte + Drop *[0]byte + Seek *[0]byte +} + +type fzRect struct { + X0 float32 + Y0 float32 + X1 float32 + Y1 float32 +} + +type fzIRect struct { + X0 int32 + Y0 int32 + X1 int32 + Y1 int32 +} + +type fzMatrix struct { + A float32 + B float32 + C float32 + D float32 + E float32 + F float32 +} + +type fzCookie struct { + Abort int32 + Progress int32 + Max uint64 + Errors int32 + Incomplete int32 +} + +type fzDevice struct { + Refs int32 + Hints int32 + Flags int32 + CloseDevice *[0]byte + DropDevice *[0]byte + FillPath *[0]byte + StrokePath *[0]byte + ClipPath *[0]byte + ClipStrokePath *[0]byte + FillText *[0]byte + StrokeText *[0]byte + ClipText *[0]byte + ClipStrokeText *[0]byte + IgnoreText *[0]byte + FillShade *[0]byte + FillImage *[0]byte + FillImageMask *[0]byte + ClipImageMask *[0]byte + PopClip *[0]byte + BeginMask *[0]byte + EndMask *[0]byte + BeginGroup *[0]byte + EndGroup *[0]byte + BeginTile *[0]byte + EndTile *[0]byte + RenderFlags *[0]byte + SetDefaultColorspaces *[0]byte + BeginLayer *[0]byte + EndLayer *[0]byte + BeginStructure *[0]byte + EndStructure *[0]byte + BeginMetatext *[0]byte + EndMetatext *[0]byte + D1Rect fzRect + ContainerLen int32 + ContainerCap int32 + Container *fzDeviceContainerStack +} + +type fzColorspace struct { + Storable fzKeyStorable + Type uint32 + Flags int32 + N int32 + Name *int8 + U [288]byte +} + +type fzStorable struct { + Refs int32 + Drop *[0]byte + Droppable *[0]byte +} + +type fzKeyStorable struct { + Storable fzStorable + KeyRefs int16 + _ [6]byte +} + +type fzPixmap struct { + Storable fzStorable + X int32 + Y int32 + W int32 + H int32 + N uint8 + S uint8 + Alpha uint8 + Flags uint8 + Stride int64 + Seps *fzSeparations + Xres int32 + Yres int32 + Colorspace *fzColorspace + Samples *uint8 + Underlying *fzPixmap +} + +type fzColorParams struct { + Ri uint8 + Bp uint8 + Op uint8 + Opm uint8 +} + +type fzBuffer struct { + Refs int32 + Data *uint8 + Cap uint64 + Len uint64 + Bits int32 + Shared int32 +} + +type fzLink struct { + Refs int32 + Next *fzLink + Rect fzRect + Uri *int8 + RectFn *[0]byte + UriFn *[0]byte + Drop *[0]byte +} + +type fzStextPage struct { + Pool *fzPool + Mediabox fzRect + FirstBlock *fzStextBlock + LastBlock *fzStextBlock +} + +type fzStextOptions struct { + Flags int32 + Scale float32 +} + +type fzStextBlock struct { + Type int32 + Bbox fzRect + _ [4]byte + U [32]byte + Prev *fzStextBlock + Next *fzStextBlock +} + +type fzDeviceContainerStack struct { + Scissor fzRect + Type int32 + User int32 +} + +type fzAllocContext struct { + User *byte + Malloc *[0]byte + Realloc *[0]byte + Free *[0]byte +} + +type fzLocksContext struct { + User *byte + Lock *[0]byte + Unlock *[0]byte +} + +type fzErrorContext struct { + Top *fzErrorStackSlot + Stack [256]fzErrorStackSlot + Padding fzErrorStackSlot + StackBase *fzErrorStackSlot + ErrCode int32 + ErrNum int32 + PrintUser *byte + Print *[0]byte + Message [256]int8 +} + +type fzWarnContext struct { + User *byte + Print *[0]byte + Count int32 + Message [256]int8 + _ [4]byte +} + +type fzAaContext struct { + Hscale int32 + Vscale int32 + Scale int32 + Bits int32 + TextBits int32 + MinLineWidth float32 +} + +type fzErrorStackSlot struct { + Buffer [1]int32 + State int32 + Code int32 + Padding [24]int8 +} + +type fzFontContext struct{} +type fzColorspaceContext struct{} +type fzTuningContext struct{} +type fzStyleContext struct{} +type fzDocumentHandlerContext struct{} +type fzArchiveHandlerContext struct{} +type fzStore struct{} +type fzGlyphCache struct{} +type fzSeparations struct{} +type fzPool struct{} diff --git a/fitz_test.go b/fitz_test.go index 5dab27f..82ccb09 100644 --- a/fitz_test.go +++ b/fitz_test.go @@ -1,6 +1,7 @@ package fitz_test import ( + "errors" "fmt" "image" "image/jpeg" @@ -183,7 +184,7 @@ func TestPNG(t *testing.T) { if err != nil { t.Error(err) } - + defer doc.Close() tmpDir, err := os.MkdirTemp(os.TempDir(), "fitz") @@ -198,6 +199,7 @@ func TestPNG(t *testing.T) { if err != nil { t.Error(err) } + if err = os.WriteFile(filepath.Join(tmpDir, fmt.Sprintf("test%03d.png", n)), png, 0644); err != nil { t.Error(err) } @@ -287,7 +289,7 @@ func TestBound(t *testing.T) { } _, err = doc.Bound(doc.NumPage()) - if err != fitz.ErrPageMissing { + if !errors.Is(err, fitz.ErrPageMissing) { t.Error(fmt.Errorf("ErrPageMissing not returned got %v", err)) } } diff --git a/go.mod b/go.mod index fe9c189..b73ada1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/gen2brain/go-fitz go 1.22 + +require ( + github.com/ebitengine/purego v0.8.0 + github.com/jupiterrider/ffi v0.1.1 +) + +require golang.org/x/sys v0.25.0 // indirect diff --git a/go.sum b/go.sum index e69de29..133eeb8 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= +github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/jupiterrider/ffi v0.1.1 h1:Z26CrkPiO/D+BXlBGPkSETDJLYdCBGrGZ7T8EsWCJVY= +github.com/jupiterrider/ffi v0.1.1/go.mod h1:YJJHJXXhzDlLYSUbeN2mvfmyx/97z98B+1oW/fsk304= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/purego_darwin.go b/purego_darwin.go new file mode 100644 index 0000000..47452ad --- /dev/null +++ b/purego_darwin.go @@ -0,0 +1,23 @@ +//go:build (!cgo || nocgo) && darwin + +package fitz + +import ( + "fmt" + + "github.com/ebitengine/purego" +) + +const ( + libname = "libmupdf.dylib" +) + +// loadLibrary loads the so and panics on error. +func loadLibrary() uintptr { + handle, err := purego.Dlopen(libname, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(fmt.Errorf("cannot load library: %w", err)) + } + + return uintptr(handle) +} diff --git a/purego_linux.go b/purego_linux.go new file mode 100644 index 0000000..fcee063 --- /dev/null +++ b/purego_linux.go @@ -0,0 +1,22 @@ +//go:build (!cgo || nocgo) && unix && !darwin + +package fitz + +import ( + "fmt" + "github.com/ebitengine/purego" +) + +const ( + libname = "libmupdf.so" +) + +// loadLibrary loads the so and panics on error. +func loadLibrary() uintptr { + handle, err := purego.Dlopen(libname, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(fmt.Errorf("cannot load library: %w", err)) + } + + return handle +} diff --git a/purego_windows.go b/purego_windows.go new file mode 100644 index 0000000..6985b25 --- /dev/null +++ b/purego_windows.go @@ -0,0 +1,22 @@ +//go:build (!cgo || nocgo) && windows + +package fitz + +import ( + "fmt" + "syscall" +) + +const ( + libname = "libmupdf.dll" +) + +// loadLibrary loads the dll and panics on error. +func loadLibrary() uintptr { + handle, err := syscall.LoadLibrary(libname) + if err != nil { + panic(fmt.Errorf("cannot load library %s: %w", libname, err)) + } + + return uintptr(handle) +}