Documentation
¶
Overview ¶
Package anydoc converts office documents to Markdown by running the anydoc Rust library as WebAssembly, with no cgo and no external processes.
The wasm module is embedded, so a binary built with this package is self-contained and cross-compiles anywhere Go does:
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build ./...
Basic use:
c, err := anydoc.New() defer c.Close(ctx) md, err := c.Convert(ctx, docBytes, "docx")
Index ¶
Constants ¶
const EmbeddedAnydocVersion = "0.2.4"
EmbeddedAnydocVersion is the upstream crate version this wasm was built from. Copied from rust/Cargo.lock by `task sync-version`; `task check-version` fails the build if the two drift apart.
Variables ¶
var ( // ErrUnsupported means the input was not a format anydoc can parse, either // because the content signature matched nothing or the hint named a format // that does not exist. ErrUnsupported = errors.New("anydoc: unsupported format") // ErrMalformed means the format was recognised but the bytes were corrupt. ErrMalformed = errors.New("anydoc: malformed document") // ErrEncrypted means the document is password-protected. anydoc does not // decrypt; obtain a decrypted copy first. ErrEncrypted = errors.New("anydoc: document is encrypted") // ErrResourceLimit means a guard inside anydoc tripped, e.g. a zip bomb or // an implausible declared size. Distinct from the host-side memory limit, // which surfaces as ErrWASM. ErrResourceLimit = errors.New("anydoc: resource limit exceeded") // ErrMissingPart means a required part of a container format was absent, // e.g. a .docx with no word/document.xml. ErrMissingPart = errors.New("anydoc: missing required part") // ErrNeedsOCR means the document carries pages that are images of text. // anydoc converts locally and does not do OCR, so it refuses rather than // returning a document with those pages missing. Detail names them, e.g. // "pages 2, 5-7 of 12 need OCR". // // A whole-document failure and a two-page-out-of-fifty failure arrive the // same way, which is deliberate: a caller that wants the rest has to ask // for it knowingly, by running the pages through OCR elsewhere or by // accepting a partial result explicitly. Before anydoc 0.2.4 the partial // case had no error at all -- those pages were simply absent from the // Markdown. ErrNeedsOCR = errors.New("anydoc: document needs OCR") // ErrWASM means the guest trapped, ran out of memory, or otherwise failed // in a way that is not a document problem. Treat as an internal error. ErrWASM = errors.New("anydoc: wasm execution failed") // ErrNoWASM means no module was available: the build used the // anydoc_nowasm tag and no WithWASM option was supplied. ErrNoWASM = errors.New("anydoc: no wasm module (build without anydoc_nowasm, or pass WithWASM)") )
Sentinel errors returned by Convert. Match them with errors.Is.
The numeric codes behind these live in rust/src/main.rs and are part of the module ABI: a wasm built from a different revision of that file may not agree with this table. That is what EmbeddedAnydocVersion is for.
Functions ¶
Types ¶
type ConvertError ¶
type ConvertError struct {
Kind error // one of the sentinels above
Detail string // guest stderr, may be empty
Code int // raw exit code, for debugging
}
ConvertError carries the guest's stderr alongside a sentinel, so callers can branch on the class of failure and still surface anydoc's own message.
func (*ConvertError) Error ¶
func (e *ConvertError) Error() string
func (*ConvertError) Unwrap ¶
func (e *ConvertError) Unwrap() error
type Converter ¶
type Converter struct {
// contains filtered or unexported fields
}
Converter runs conversions. It is safe for concurrent use and should be created once and reused: New compiles the module, which is the expensive part, and Convert then instantiates a cheap short-lived guest per document.
func New ¶
New compiles the wasm module and prepares the runtime.
Compilation happens once, here, and costs a few hundred milliseconds. Do it at startup, not per request.
func (*Converter) Close ¶
Close releases the runtime, and the compilation cache when there is one. The Converter is unusable afterwards. Cached entries stay on disk: that is the point of them.
func (*Converter) Convert ¶
Convert turns a document into Markdown.
hint is a bare extension such as "docx" that selects the parser. Pass "" to detect from content, which works for every format except CSV, which has no signature and must be named.
Each call gets a fresh guest instance with its own linear memory. A large document therefore cannot leave memory permanently claimed, and a malformed one cannot leak state into the next call.
func (*Converter) ConvertReader ¶
ConvertReader is a convenience wrapper that reads r fully before converting. The whole document has to be in memory regardless: it is copied into the guest's linear memory.
type Option ¶
type Option func(*config)
Option configures a Converter.
func WithCompilationCache ¶ added in v0.1.3
WithCompilationCache persists the compiler's output under dirname, so the cost of WithCompiler is paid once per machine instead of once per process.
It changes what WithCompiler costs more than it changes what it does. Measured on the real module, a hit turns New from 1.2s and 517 MB of peak RSS into 6ms and 37 MB, because the machine code is read back rather than produced -- the memory WithCompiler is expensive for is the compiler working, not the compiled module sitting there. The directory holds ~15 MB.
This only affects WithCompiler. The interpreter emits no machine code, so there is nothing to persist and this option does nothing.
An entry is keyed by the module, the CPU's feature bits, the runtime version and the target platform. Anything else is a miss, and a miss simply compiles again and writes a new entry -- it will not hand the host machine code it cannot run. That also makes the directory disposable: deleting it costs one recompilation and nothing else, and what it holds is machine code, never anything a caller passed to Convert.
Give it a directory the process owns and that survives restarts; a temporary directory wastes the point. It is created if missing, and Close releases it.
func WithCompiler ¶
func WithCompiler() Option
WithCompiler trades startup cost and memory for throughput, by having wazy compile the module to native code instead of interpreting it.
Measured on the real module, per document: a 1 KB docx goes from 1.4ms to 0.12ms, one with a 5 MB uncompressed body from 6.7s to 0.18s, and a 7.6 MB PDF from 12.0s to 0.33s. The price is paid once in New, which goes from ~84ms and 134 MB of RSS to ~1.3s and 530 MB. That 530 MB is an OOM kill in a 512 MB container, which is why this is opt-in rather than the default.
The compilation is paid once, in New, not per document: Convert only instantiates the already-compiled module. So this pays off in a long-lived process that reuses one Converter, and is a pure loss in a short-lived one that converts a single small document and exits -- there it buys ~1.3ms for ~1.3s. Leave it off for tight containers too, unless you can also give it WithCompilationCache, which removes both of those objections after the first run.
This is a request, not a guarantee. The compiler backend needs amd64 or arm64 (with SSE4.1 on amd64) on a mainstream OS, and needs the host to allow mmap'd executable pages -- macOS hardened runtime and some seccomp profiles refuse them. Where it is unavailable, wazy falls back to the interpreter silently: the conversion still happens, at interpreter speed. Nothing here affects cross-compilation, since wazy is pure Go.
func WithConcurrency ¶
WithConcurrency caps simultaneous guest instances. Each holds its own linear memory, so this is the main lever on peak memory. Default 4.
func WithMaxInputBytes ¶
WithMaxInputBytes rejects documents larger than n before any wasm runs. Zero disables the check. Default 64 MiB.
func WithMemoryLimitPages ¶
WithMemoryLimitPages caps one guest's linear memory in 64 KiB pages. Default 1024 (64 MiB).
What a guest needs tracks the uncompressed content rather than the input's size on disk, so a compact file can still be expensive. Measured against the real module: 64 pages is the module's own floor and converts nothing, a docx with a 0.4 MB body needs 192, a 2 MB body 576, a 5 MB body 1280, and a 7.5 MB PDF needs between 512 and 1024.
Raise this when conversions fail with ErrWASM on inputs that convert fine elsewhere; the error says so explicitly when the guest ran out of memory. Each concurrent guest can claim up to this much, so this multiplied by WithConcurrency is the real ceiling.
Validated at New time against the module's declared minimum, so too low a value fails immediately rather than at conversion.
func WithWASM ¶
WithWASM supplies the module instead of using the embedded one.
This is the counterpart to building with -tags anydoc_nowasm, where nothing is embedded and this option becomes mandatory. It is also useful without the tag: pinning a different anydoc build, loading from a shared volume, or keeping the payload in its own container layer.
The module is read immediately; r is not retained.
func WithWASMBytes ¶
WithWASMBytes is WithWASM for a module already in memory.