Documentation
¶
Overview ¶
Package decant converts fixed-layout, text-layer PDF into semantic, reflowable EPUB 3.
PDF stores positioned glyphs; EPUB needs paragraphs, headings, and reading order. Recovering the second from the first is the whole problem, and it is heuristic: decant infers structure that the source file does not record. Every inference it makes is tunable through Heuristics, inspectable through Converter.Probe, and reported through Report.
The pipeline ¶
Conversion runs in eight stages:
parse → glyphs → lines → blocks → furniture → classify → assemble → serialize
Stages 1 through 6 produce a Document, an editable block tree. Stage 7 and 8 turn that tree into an EPUB container. The split is deliberate and is the main reason this is a library rather than only a command: a caller can inspect and correct the inferred structure before committing to output.
Basic use ¶
Converter.Convert runs the whole pipeline:
conv, err := decant.New(decant.DefaultOptions())
if err != nil {
return err
}
rep, err := conv.Convert(ctx, in, size, out)
A Converter holds no mutable state, so one instance is safe to reuse across documents and across goroutines.
Correcting structure before writing ¶
To review or edit what was inferred, call Converter.Analyze, modify the returned Document, then call Converter.Write:
doc, err := conv.Analyze(ctx, in, size)
if err != nil {
return err
}
for i := range doc.Blocks {
if doc.Blocks[i].Text == "Appendix A" {
doc.Blocks[i].Kind = decant.KindHeading
doc.Blocks[i].Level = 1
}
}
rep, err := conv.Write(ctx, doc, out)
Edits reach the output: heading levels drive chapter splitting and the navigation document, so promoting a paragraph to a level-1 heading starts a new chapter. Levels outside 1 through 6 are clamped rather than rejected, because XHTML has no other heading elements. Write does not mutate the Document, so it may be called more than once on the same tree.
A block carries more than its text. Block.Styles holds the bold and italic runs as character ranges, and Block.Links the internal cross-references, both editable the same way: narrowing a range or clearing a target suppresses the markup without touching the words.
Device profiles ¶
Options.ApplyProfileDefaults applies one of the built-in profiles. For a device decant has never seen, LoadProfileDoc reads a profile document and Options.ApplyProfileDoc layers it over a chosen base, so a profile can be shared as a file rather than compiled in. WriteProfileDoc emits a built-in as a starting point.
Determinism ¶
Identical input and options produce byte-identical output. Anchor IDs are content hashes rather than counters, the package identifier is a UUIDv5 over the input's SHA-256, and ZIP entries carry a fixed timestamp taken from Options.Deterministic, the PDF ModDate, or the Unix epoch, in that order. Reconverting a file therefore yields the same bytes, which is what makes output diffable and cacheable.
Failure ¶
decant fails loudly rather than emitting silently corrupt EPUB. Four conditions are reported as typed errors, each of which the command maps to a distinct exit code:
- EncryptedError, for a PDF carrying an /Encrypt dictionary
- NoTextLayerError, for a scan. decant does not OCR, ever
- MalformedError, for damage beyond what xref reconstruction recovers. A table that can be rebuilt is repaired instead, and the report says so
- UsageError, for invalid options
Everything short of those degrades gracefully and records a Diagnostic in the Report instead. A conversion that emitted warnings still produced a valid EPUB; Report.QualityScore summarizes how much to trust it.
Stability ¶
Every package below this one is internal, so the surface here is the whole supported API. See the repository README for what the v1 compatibility promise does and does not cover.
Example ¶
Convert a PDF to EPUB with the default settings.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sroberts/decant"
)
// openPDF is the boilerplate every example needs: decant reads through
// io.ReaderAt and needs the size, so a caller passes both.
func openPDF(path string) (*os.File, int64, error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
}
return f, st.Size(), nil
}
func main() {
in, size, err := openPDF("book.pdf")
if err != nil {
log.Fatal(err)
}
defer in.Close()
out, err := os.Create("book.epub")
if err != nil {
log.Fatal(err)
}
defer out.Close()
conv, err := decant.New(decant.DefaultOptions())
if err != nil {
log.Fatal(err)
}
rep, err := conv.Convert(context.Background(), in, size, out)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d chapters, quality %d/100\n", rep.Chapters, rep.QualityScore)
}
Output:
Index ¶
- func WriteProfileDoc(w io.Writer, p Profile) error
- type Block
- type BlockKind
- type Converter
- func (c *Converter) Analyze(ctx context.Context, r io.ReaderAt, size int64) (*Document, error)
- func (c *Converter) Convert(ctx context.Context, r io.ReaderAt, size int64, w io.Writer) (*Report, error)
- func (c *Converter) Options() Options
- func (c *Converter) Probe(ctx context.Context, r io.ReaderAt, size int64, stage ProbeStage, page int) (*ProbeResult, error)
- func (c *Converter) Write(ctx context.Context, doc *Document, w io.Writer) (*Report, error)
- type CrossRef
- type Diagnostic
- type Document
- type DocumentInfo
- type EncryptedError
- type Heuristics
- type HyphenDecision
- type HyphenationReport
- type Image
- type ImageMode
- type MalformedError
- type Metadata
- type NoTextLayerError
- type Options
- type OutlineItem
- type PageMetrics
- type PageRange
- type PageSize
- type ProbeBlock
- type ProbeGlyph
- type ProbeLine
- type ProbePage
- type ProbeResult
- type ProbeStage
- type Profile
- type ProfileDoc
- type Rect
- type Report
- type Severity
- type SplitMode
- type StyleRun
- type TableCell
- type TableMode
- type UsageError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Block ¶
type Block struct {
Kind BlockKind
// Level is the heading rank 1 through 6, and 0 for non-headings.
Level int
// Text is the block's plain text content.
Text string
// Page is the zero-based source page index.
Page int
// Bounds is the block's bounding box in page space.
Bounds Rect
// ID is the anchor identifier. It derives from a content hash rather than
// a counter so it stays stable across runs, which internal cross-
// reference rewriting in spec section 4.9 depends on.
ID string
// Size is the block's median font size in points.
Size float64
// Font is the block's dominant font family name.
Font string
// ImageID names the image a figure block carries, matching Document.Images.
// Empty for every other kind.
ImageID string
// Caption is a figure's caption text, empty when it has none.
Caption string
// InlineImage marks a figure narrow enough, and inside a paragraph, to
// flow in the text rather than stand as a block. Spec 4.7.
InlineImage bool
// ListItems holds a list block's items, one per entry. Empty for every
// other kind.
ListItems []string
// ListOrdered selects ol over ul.
ListOrdered bool
// ListStart is the first item's number, inferred from its marker. Zero
// means the list starts at one.
ListStart int
// Styles are the bold and italic runs of Text, as character ranges.
// Spec section 4.6.
Styles []StyleRun
// Links are internal cross-references originating in this block, as
// character ranges of Text. Spec section 4.9.
Links []CrossRef
// Superscripts are the superscript run labels found in Text, in order.
Superscripts []string
// NoteRefs maps a superscript label to the ID of the footnote block it
// references. Spec 4.6 links these with epub:type noteref and footnote.
NoteRefs map[string]string
// NoteLabel is a footnote block's own marker, e.g. "1" or "†".
NoteLabel string
// TableRows holds a table block's cells, row by row. Empty for every
// other kind.
TableRows [][]TableCell
// TableConfidence records which detection signals fired, which
// --table-mode=auto keys on.
TableConfidence string
}
type BlockKind ¶
type BlockKind string
BlockKind classifies a block after structure classification.
const ( // KindParagraph is flowed body text. KindParagraph BlockKind = "paragraph" // KindHeading is a heading; see Block.Level for its rank. KindHeading BlockKind = "heading" // KindList is a bullet or numbered list. KindList BlockKind = "list" // KindQuote is a blockquote. KindQuote BlockKind = "blockquote" // KindCode is a fixed-pitch code block. KindCode BlockKind = "code" // KindCaption is a figure or table caption. KindCaption BlockKind = "caption" // KindFootnote is a footnote body. KindFootnote BlockKind = "footnote" // KindFigure is an image with optional caption. KindFigure BlockKind = "figure" // KindTable is a detected table. KindTable BlockKind = "table" )
type Converter ¶
type Converter struct {
// contains filtered or unexported fields
}
Converter runs the conversion pipeline. It holds no mutable state, so one Converter is safe to reuse across documents.
func (*Converter) Analyze ¶
Analyze runs stages 1 through 6 and returns the intermediate model.
The returned Document exposes the block tree so a caller can correct structure before Write serializes it.
Example ¶
Correct the inferred structure before writing.
This is why Analyze and Write are separate. Headings drive chapter splitting and the navigation document, so promoting a block to a level-1 heading starts a new chapter in the output.
package main
import (
"context"
"log"
"os"
"github.com/sroberts/decant"
)
// openPDF is the boilerplate every example needs: decant reads through
// io.ReaderAt and needs the size, so a caller passes both.
func openPDF(path string) (*os.File, int64, error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
}
return f, st.Size(), nil
}
func main() {
in, size, err := openPDF("book.pdf")
if err != nil {
log.Fatal(err)
}
defer in.Close()
conv, err := decant.New(decant.DefaultOptions())
if err != nil {
log.Fatal(err)
}
doc, err := conv.Analyze(context.Background(), in, size)
if err != nil {
log.Fatal(err)
}
// An epigraph set in a large face is a classic false heading. Demote it,
// and promote an appendix the classifier read as body text.
for i := range doc.Blocks {
switch doc.Blocks[i].Text {
case "It was a bright cold day in April":
doc.Blocks[i].Kind = decant.KindParagraph
doc.Blocks[i].Level = 0
case "Appendix A":
doc.Blocks[i].Kind = decant.KindHeading
doc.Blocks[i].Level = 1
}
}
out, err := os.Create("book.epub")
if err != nil {
log.Fatal(err)
}
defer out.Close()
if _, err := conv.Write(context.Background(), doc, out); err != nil {
log.Fatal(err)
}
}
Output:
func (*Converter) Convert ¶
func (c *Converter) Convert(ctx context.Context, r io.ReaderAt, size int64, w io.Writer) (*Report, error)
Convert is Analyze followed by Write.
Example (Errors) ¶
Distinguish the four failure modes.
Each maps to a distinct exit code at the command layer. Everything short of these degrades gracefully and lands in the report as a diagnostic instead.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/sroberts/decant"
)
// openPDF is the boilerplate every example needs: decant reads through
// io.ReaderAt and needs the size, so a caller passes both.
func openPDF(path string) (*os.File, int64, error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
}
return f, st.Size(), nil
}
func main() {
in, size, err := openPDF("book.pdf")
if err != nil {
log.Fatal(err)
}
defer in.Close()
conv, err := decant.New(decant.DefaultOptions())
if err != nil {
log.Fatal(err)
}
_, err = conv.Convert(context.Background(), in, size, os.Stdout)
var encrypted *decant.EncryptedError
var noText *decant.NoTextLayerError
var malformed *decant.MalformedError
switch {
case errors.As(err, &encrypted):
fmt.Println("password-protected; decant does not decrypt")
case errors.As(err, &noText):
fmt.Println("scanned; decant does not OCR")
case errors.As(err, &malformed):
fmt.Println("damaged beyond repair")
case err != nil:
log.Fatal(err)
}
}
Output:
func (*Converter) Probe ¶
func (c *Converter) Probe(ctx context.Context, r io.ReaderAt, size int64, stage ProbeStage, page int) (*ProbeResult, error)
Probe runs the pipeline as far as the requested stage and returns the intermediate model. A page of -1 probes every selected page.
Example ¶
Inspect the intermediate model at one pipeline stage.
Probe is what makes the heuristics auditable: it dumps what decant saw at the point a decision was made, for one page or the whole document.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sroberts/decant"
)
// openPDF is the boilerplate every example needs: decant reads through
// io.ReaderAt and needs the size, so a caller passes both.
func openPDF(path string) (*os.File, int64, error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
}
return f, st.Size(), nil
}
func main() {
in, size, err := openPDF("book.pdf")
if err != nil {
log.Fatal(err)
}
defer in.Close()
conv, err := decant.New(decant.DefaultOptions())
if err != nil {
log.Fatal(err)
}
res, err := conv.Probe(context.Background(), in, size, decant.StageLines, 12)
if err != nil {
log.Fatal(err)
}
for _, p := range res.Pages {
for _, l := range p.Lines {
fmt.Printf("%.1f %s\n", l.Baseline, l.Text)
}
}
}
Output:
type CrossRef ¶
type CrossRef struct {
// Start and End are byte offsets into the owning block's Text, half-open.
Start, End int
// TargetPage is the zero-based destination page and TargetY its vertical
// position in page space, or NaN when the destination names none.
TargetPage int
TargetY float64
// TargetID is the ID of the block the destination resolved to. Empty
// means the destination pointed somewhere with no block, in which case
// the range renders as plain text and the report records it.
TargetID string
}
CrossRef is one internal cross-reference: a range of a block's text that links to somewhere else in the document.
Spec section 4.9 rewrites PDF /Link annotations into these. A caller may edit the range or clear TargetID to suppress the link, the same way it may edit any other field of the block tree.
type Diagnostic ¶
type Diagnostic struct {
Severity Severity `json:"severity"`
// Stage names the pipeline stage, e.g. "glyphs" or "assemble".
Stage string `json:"stage"`
// Page is the zero-based page index, or -1 for document-level entries.
Page int `json:"page"`
Message string `json:"message"`
}
Diagnostic records one heuristic firing or one quality problem. Spec principle 3 requires every heuristic that fires to leave one of these.
type Document ¶
type Document struct {
// Title, Author, and Language are resolved from PDF metadata and any
// caller overrides.
Title string
Author string
Language string
// Source is the input filename, recorded as dc:source.
Source string
// Blocks is the reconstructed content in reading order.
Blocks []Block
// Outline is the PDF bookmark tree, empty when the document has none.
Outline []OutlineItem
// Images are the pictures carried into the EPUB, referenced by
// Block.ImageID.
Images []Image
// PageCount is the number of pages in the source document, before any
// page range applies.
PageCount int
// Digest is the hex SHA-256 of the input file. The EPUB identifier is a
// UUIDv5 over it, which makes reconversion produce the same identifier.
Digest string
// Modified is the timestamp written to dcterms:modified and every ZIP
// header.
Modified time.Time
// contains filtered or unexported fields
}
Document is the intermediate model produced by Analyze: the block tree plus the metadata needed to serialize it.
type DocumentInfo ¶
type DocumentInfo struct {
Source string `json:"source"`
PageCount int `json:"page_count"`
Title string `json:"title,omitempty"`
Author string `json:"author,omitempty"`
Subject string `json:"subject,omitempty"`
Keywords string `json:"keywords,omitempty"`
Creator string `json:"creator,omitempty"`
Producer string `json:"producer,omitempty"`
Language string `json:"language,omitempty"`
Created time.Time `json:"created,omitempty"`
Modified time.Time `json:"modified,omitempty"`
// Digest is the hex SHA-256 of the input file.
Digest string `json:"digest"`
// Identifier is the EPUB dc:identifier a conversion would produce.
Identifier string `json:"identifier"`
// OutlineEntries is the number of top-level bookmarks.
OutlineEntries int `json:"outline_entries"`
Outline []OutlineItem `json:"outline,omitempty"`
// PageSizes lists the distinct page dimensions in points.
PageSizes []PageSize `json:"page_sizes,omitempty"`
}
DocumentInfo is the metadata the meta subcommand reports. Reading it does not run the conversion pipeline.
func Meta ¶
Meta reads document metadata without running the conversion pipeline.
Example ¶
Read metadata without converting.
Meta parses only the trailer and page tree, so it is cheap enough to run across a whole library to build an index.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sroberts/decant"
)
// openPDF is the boilerplate every example needs: decant reads through
// io.ReaderAt and needs the size, so a caller passes both.
func openPDF(path string) (*os.File, int64, error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
}
return f, st.Size(), nil
}
func main() {
in, size, err := openPDF("book.pdf")
if err != nil {
log.Fatal(err)
}
defer in.Close()
info, err := decant.Meta(context.Background(), in, size)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s by %s, %d pages\n", info.Title, info.Author, info.PageCount)
}
Output:
type EncryptedError ¶
type EncryptedError struct {
// Handler names the security handler, e.g. "Standard".
Handler string
// Revision is the /R value identifying the algorithm generation.
Revision int
}
EncryptedError reports a PDF carrying an /Encrypt dictionary. Spec section 1 puts decryption out of scope for v1; the CLI maps this to exit code 3.
func (*EncryptedError) Error ¶
func (e *EncryptedError) Error() string
type Heuristics ¶
type Heuristics struct {
// BaselineTolerance is the fraction of median glyph height within which
// glyphs share a baseline. Default 0.3.
BaselineTolerance float64
// SpaceGapRatio is the fraction of a font's space width that an
// inter-glyph gap must exceed to become a space. Default 0.25.
SpaceGapRatio float64
// RotationTolerance is the maximum baseline angle in degrees still
// treated as horizontal text. Default 5.
RotationTolerance float64
// KeepRotated retains rotated runs rather than dropping them with a
// warning. Default false.
KeepRotated bool
// BlockOverlapRatio is the horizontal overlap, as a fraction of the
// narrower line, required to merge a line into a block. Default 0.5.
BlockOverlapRatio float64
// BlockGapRatio is the multiple of running median leading beyond which a
// vertical gap breaks a block. Default 1.5.
BlockGapRatio float64
// BlockSizeChangeRatio is the relative font size change that breaks a
// block. Default 0.15.
BlockSizeChangeRatio float64
// ParagraphGapRatio is the fractional overshoot of the running leading
// that starts a new paragraph. Default 0.25.
ParagraphGapRatio float64
// ParagraphIndentEm is the indent above the block median, in em, that
// starts a new paragraph. Default 0.5.
ParagraphIndentEm float64
// ShortLineRatio is the fraction of block width below which a line
// ending in terminal punctuation ends a paragraph. Default 0.8.
ShortLineRatio float64
// ScanMedianGlyphs is the median glyphs per page below which a document
// is a candidate for the scanned classifier. Default 20.
ScanMedianGlyphs int
// ScanImagePageRatio is the fraction of sampled pages that must carry
// page-covering images to confirm a scan. Default 0.8.
ScanImagePageRatio float64
// ScanSamplePages is how many pages the scanned classifier samples.
// Default 20.
ScanSamplePages int
// ScanImageCoverRatio is the share of a page images must cover for it to
// count as image-covered by the scanned classifier. Spec 6 describes
// "full-page images"; 0.8 allows for the margins a scan usually keeps.
ScanImageCoverRatio float64
// MaxColumns caps automatic column detection. Default 3.
MaxColumns int
// GutterMinWidthSpaces is how many median space widths a whitespace band
// must span to count as a column gutter. Default 2.
GutterMinWidthSpaces float64
// GutterMinHeightRatio is the fraction of text-carrying rows a band must
// be empty across to count as a gutter. Default 0.6.
GutterMinHeightRatio float64
// ColumnMinGlyphRatio is the minimum share of a page's glyphs each
// detected column must hold for the split to be believed. Default 0.1.
//
// Not in the spec. Section 4.4 notes the column heuristic misfires on
// tables and figures; this guard rejects a split that would leave a
// column nearly empty.
ColumnMinGlyphRatio float64
// ColumnMinRows is the number of text-carrying rows a page needs before
// its projection profile is trusted at all. Default 8.
//
// Not in the spec, and the guard that matters most in practice. Asking
// whether a band is empty across 60% of rows is meaningless on a page
// with four rows, so title pages and figure pages otherwise produce
// phantom gutters.
ColumnMinRows int
// ColumnMinLines is the number of assembled lines each detected column
// must contain for the split to survive. Default 3.
//
// Not in the spec. Checked after lines are split at the gutters, which is
// stronger evidence than the glyph share.
ColumnMinLines int
// HeadingSizeRatio is how far a block's font size must exceed the body
// font to be a heading. Default 0.15, meaning 15% larger.
HeadingSizeRatio float64
// HeadingBoldMaxWords is the word count below which a bold block with no
// terminal punctuation is a heading. Default 15.
HeadingBoldMaxWords int
// HeadingMaxWords caps the size-based heading test. Default 50.
//
// Not in the spec. Section 4.6 makes size alone sufficient, which would
// turn a long epigraph or pull quote set slightly large into a heading
// and split the book at it.
HeadingMaxWords int
// BackgroundCoverRatio is the share of the page an image must cover,
// when painted beneath the text, to be dropped as a background or
// watermark. Default 0.95.
BackgroundCoverRatio float64
// MinImagePoints is the smallest edge an image may have before it is
// dropped. Default 16.
MinImagePoints float64
// MinImageAreaRatio is the share of page area below which an image is
// dropped. Default 0.02.
MinImageAreaRatio float64
// InlineImageWidthRatio is the fraction of the text column width below
// which an image inside a paragraph flows inline. Default 0.4.
InlineImageWidthRatio float64
// CaptionGapLines is how many line heights a caption may sit from its
// figure. Default 1.5.
CaptionGapLines float64
// CaptionSizeRatio is how far below the body font a caption is set.
// Default 0.05.
CaptionSizeRatio float64
// CaptionOverlapRatio is the horizontal overlap a block must share with
// an image to be treated as its caption. Default 0.3.
//
// Not in the spec: without it a sidebar level with a figure binds to it.
CaptionOverlapRatio float64
// FurnitureBandRatio is the fraction of page height at the top and bottom
// in which running heads and folios live. Default 0.08.
FurnitureBandRatio float64
// FurnitureRepeatRatio is the fraction of sampled pages a block must
// repeat on to be removed as furniture. Default 0.6.
FurnitureRepeatRatio float64
// FurnitureSamplePages is how many pages the furniture sampler examines.
// Default 20.
FurnitureSamplePages int
// FurnitureMinPages is the document length below which furniture removal
// is skipped. Default 5.
FurnitureMinPages int
// QuoteIndentEm is how far both margins must be inset beyond the body, in
// em, for a blockquote. Default 1.5.
QuoteIndentEm float64
// FootnoteBandRatio is the fraction of page height, from the bottom, in
// which a footnote may sit. Default 0.2.
FootnoteBandRatio float64
// FootnoteSizeRatio is how far below the body font a footnote is set.
// Default 0.1.
FootnoteSizeRatio float64
// SuperscriptRiseEm is the baseline offset, as a fraction of em, above
// which a glyph counts as a superscript. Default 0.2.
SuperscriptRiseEm float64
// SuperscriptSizeRatio is the size ratio below which a raised glyph
// counts as a superscript. Default 0.85.
SuperscriptSizeRatio float64
// RuleMaxThickness is the stroke width above which a painted segment is a
// bar rather than a ruling line. Default 2.
RuleMaxThickness float64
// RuleClusterTolerance is how far apart two rules may sit and still count
// as the same table boundary. Default 2.
RuleClusterTolerance float64
// RuleRowCoverRatio is the fraction of a row's height a vertical rule
// must span to separate its cells. Default 0.6.
RuleRowCoverRatio float64
// TableRegionGap is the vertical gap between ruling lines beyond which
// they belong to separate tables. Default 36.
TableRegionGap float64
// TableColumnTolerance is how close two column starts must be to count as
// the same boundary. Default 2.
TableColumnTolerance float64
// the alignment signal to fire. Default 2.
TableMinSharedColumns int
// TableMinRows is the number of consecutive tabulated lines the alignment
// signal needs. Default 3.
TableMinRows int
// TableMinFilledRatio is the fraction of a ruled grid's cells that must
// carry text for it to be a table. Default 0.5.
//
// Not in the spec. Diagrams draw axis-aligned lines that form apparent
// grids; without this guard a mathematics textbook yields phantom tables.
TableMinFilledRatio float64
// StyleMinLetters is the shortest bold or italic run emitted as emphasis,
// in letters. Default 2.
//
// Not in spec section 4.6, and a guard against its rule misfiring. A
// single italic letter mid-sentence is a variable or a symbol rather than
// emphasis, and a mathematics document is made of them.
StyleMinLetters int
// RTLLetterRatio is the fraction of a document's letters that must belong
// to a right-to-left script before spec section 1's scope warning fires.
// Default 0.2.
//
// A ratio rather than any occurrence at all: a Latin book quoting a line
// of Hebrew is not a bidirectional document, and warning on it would
// train the reader to ignore the warning that matters.
RTLLetterRatio float64
// VectorMinPaints is the number of painted paths a page must carry before
// its vector artwork is reported as dropped content. Default 24.
//
// Not in the spec. Almost every PDF paints some paths for rules,
// underlines, table borders, and form fields; reporting those as lost
// artwork would be noise a reader cannot act on. A page actually drawing
// a diagram paints far more, and the distributions do not overlap: across
// the sample corpus incidental decoration runs from 7 to 20 paths per
// page, while a mathematics textbook full of geometry figures paints
// about forty. The default sits in that gap.
//
// The bias is deliberate. Reporting a form border as lost artwork is
// noise; missing a dropped chart is the silent content loss this
// diagnostic exists to end, so the threshold is set to catch artwork
// rather than to catch every path.
VectorMinPaints int
}
Heuristics holds every tunable threshold from spec section 4. Each field documents its default; DefaultHeuristics returns them all.
Spec principle 5 requires these to be inspectable as well as tunable, which is what the probe subcommand dumps.
Example ¶
Tune a heuristic.
Every threshold decant infers structure from is exposed and documented. This one raises how far a block must exceed the body font to read as a heading, from the default 15% to 30%, which suppresses false headings in a document set slightly large throughout.
package main
import (
"log"
"github.com/sroberts/decant"
)
func main() {
opts := decant.DefaultOptions()
opts.Heuristics = decant.DefaultHeuristics()
opts.Heuristics.HeadingSizeRatio = 0.30
conv, err := decant.New(opts)
if err != nil {
log.Fatal(err)
}
_ = conv
}
Output:
func DefaultHeuristics ¶
func DefaultHeuristics() Heuristics
DefaultHeuristics returns the documented defaults from spec section 4.
type HyphenDecision ¶
type HyphenDecision struct {
Left string `json:"left"`
Right string `json:"right"`
Dropped bool `json:"dropped"`
Reason string `json:"reason"`
}
HyphenDecision records one line-break hyphen and the reasoning behind it.
type HyphenationReport ¶
type HyphenationReport struct {
// Language is the pattern set that was used, empty when dehyphenation
// was disabled.
Language string `json:"language,omitempty"`
// Patterns is the number of patterns in that set.
Patterns int `json:"patterns,omitempty"`
// Dropped counts hyphens removed as typesetting artifacts, Kept counts
// those judged lexical.
Dropped int `json:"dropped"`
Kept int `json:"kept"`
// Decisions is a bounded sample of the individual calls.
Decisions []HyphenDecision `json:"decisions,omitempty"`
}
HyphenationReport summarizes what dehyphenation did.
Spec section 4.6 asks for every decision to be recorded. A full-length book makes thousands, so the counts cover all of them and Decisions carries a bounded sample; the full trail would make a report file unreadable.
type Image ¶
type Image struct {
// ID is the manifest id and the basename of the file, e.g. "img001".
ID string
// Data is the encoded bytes.
Data []byte
// MediaType is the manifest media type.
MediaType string
// Ext is the filename extension without a dot.
Ext string
// Width and Height are the final pixel dimensions.
Width, Height int
// Passthrough reports that the original JPEG bytes were kept unmodified.
Passthrough bool
}
Image is one picture carried into the EPUB.
type MalformedError ¶
MalformedError reports a PDF damaged beyond what xref reconstruction can recover. The CLI maps this to exit code 6.
func (*MalformedError) Error ¶
func (e *MalformedError) Error() string
func (*MalformedError) Unwrap ¶
func (e *MalformedError) Unwrap() error
type NoTextLayerError ¶
type NoTextLayerError struct {
// MedianGlyphs is the median glyph count across sampled pages.
MedianGlyphs float64
// SampledPages is how many pages were examined.
SampledPages int
// ImagePageFraction is the fraction of sampled pages covered by
// page-scale images.
ImagePageFraction float64
}
NoTextLayerError reports a scanned document. decant does not OCR; spec section 6 fails fast and points at an external OCR pass. The CLI maps this to exit code 4.
func (*NoTextLayerError) Error ¶
func (e *NoTextLayerError) Error() string
type Options ¶
type Options struct {
Profile Profile
Metadata Metadata
Pages PageRange
Heuristics Heuristics
// SplitAt selects chapter boundaries. Default SplitAtH1.
SplitAt SplitMode
// MaxChunkBytes forces a split of oversized XHTML at a paragraph
// boundary. Default 262144; the minimal profile lowers it to 65536.
//
// The crosspoint profile keeps 262144: its firmware streams XHTML rather
// than parsing it into memory, so chapter size is not a memory constraint
// there. See ApplyProfileDefaults.
MaxChunkBytes int
// KeepHeaders retains running heads and folios. Default false.
KeepHeaders bool
// NoDehyphenate preserves line-break hyphens verbatim.
NoDehyphenate bool
// Columns forces a column count. Zero detects from the page's projection
// profile; 1, 2, or 3 override it. Spec 3: --columns.
Columns int
// Images selects image handling. Default ImagesKeep.
Images ImageMode
// KeepSmallImages retains images the size rules in spec 4.7 would drop.
KeepSmallImages bool
// Tables selects how detected tables are emitted. Default TableAuto; the
// crosspoint and minimal profiles lower it to TableText.
Tables TableMode
// ImageMaxWidth is the longest edge in pixels; 0 disables scaling.
ImageMaxWidth int
// Deterministic fixes the output timestamp. When zero, the PDF ModDate
// is used, then the Unix epoch.
Deterministic time.Time
// Strict makes quality threshold breaches an error at the CLI layer.
Strict bool
}
Options configures a Converter. The zero value is not usable; start from DefaultOptions.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions returns the documented defaults from spec section 3.
func (*Options) ApplyProfileDefaults ¶
func (o *Options) ApplyProfileDefaults()
ApplyProfileDefaults overwrites the image, chunk-size, and related fields with the device profile defaults from spec section 5.
It overwrites unconditionally, so a caller that wants an explicit value to win must re-apply it afterward. The CLI does exactly that, using flag.Visit to learn which flags the user actually passed.
Example ¶
Target a constrained reading device.
A profile sets image handling, chunk size, and table mode together. ApplyProfileDefaults overwrites those fields unconditionally, so set it before any value you want to keep.
package main
import (
"fmt"
"log"
"github.com/sroberts/decant"
)
func main() {
opts := decant.DefaultOptions()
opts.Profile = decant.ProfileCrossPoint
opts.ApplyProfileDefaults()
// Overrides go after, or the profile wins.
opts.ImageMaxWidth = 800
conv, err := decant.New(opts)
if err != nil {
log.Fatal(err)
}
fmt.Println(conv.Options().Tables)
}
Output:
func (*Options) ApplyProfileDoc ¶ added in v1.1.0
func (o *Options) ApplyProfileDoc(d *ProfileDoc) error
ApplyProfileDoc layers a document's overrides onto o.
It applies Options and Heuristics only. The document's Base is advisory and the caller chooses the starting point, because the caller is the only one that knows whether a profile was also named explicitly:
o := DefaultOptions() o.Profile = doc.Base // or an explicit choice, which wins o.ApplyProfileDefaults() err := o.ApplyProfileDoc(doc)
That order gives defaults, then base profile, then document, then whatever the caller sets afterwards.
type OutlineItem ¶
type OutlineItem struct {
Title string
// Page is the zero-based destination page, or -1 when unresolved.
Page int
// Y is the destination's vertical position in PDF user space, or NaN when
// the destination specifies none. It is not page space: user space runs
// y-up from the crop box's lower-left corner.
Y float64
Children []OutlineItem
}
OutlineItem is one node of the source PDF's bookmark tree.
type PageMetrics ¶
type PageMetrics struct {
Page int `json:"page"`
// Glyphs is the number of glyphs extracted after invisible-text
// filtering.
Glyphs int `json:"glyphs"`
// DecodeFailures counts glyphs that mapped to U+FFFD.
DecodeFailures int `json:"decode_failures"`
// Lines and Blocks count the stage 3 and stage 4 output.
Lines int `json:"lines"`
Blocks int `json:"blocks"`
// Columns is the number of text columns detected on the page.
Columns int `json:"columns"`
// Images is the number of images placed from the page, after the drop
// rules in spec section 4.7.
Images int `json:"images"`
// Tables counts tables detected on the page.
Tables int `json:"tables,omitempty"`
// VectorPaints counts painted path operations on the page. decant does
// not render vector artwork, so a high count means content was lost.
VectorPaints int `json:"vector_paints,omitempty"`
// RotatedDropped counts rotated runs discarded.
RotatedDropped int `json:"rotated_dropped"`
// Letters and RTLLetters count the page's letters and how many of them
// belong to a right-to-left script. Spec section 1 puts bidirectional
// layout out of scope and asks for it to be detected.
Letters int `json:"letters,omitempty"`
RTLLetters int `json:"rtl_letters,omitempty"`
// VerticalText marks a page using a font with a vertical writing mode.
VerticalText bool `json:"vertical_text,omitempty"`
// UsedInvisibleText marks a page whose only text was a mode-3 layer,
// which is the searchable-scan case.
UsedInvisibleText bool `json:"used_invisible_text"`
}
PageMetrics holds the per-page numbers the report surfaces.
func (PageMetrics) DecodeFailureRate ¶
func (m PageMetrics) DecodeFailureRate() float64
DecodeFailureRate returns failures as a fraction of glyphs.
type PageRange ¶
type PageRange struct {
// contains filtered or unexported fields
}
PageRange selects a subset of pages. The zero value selects every page.
func ParsePageRange ¶
ParsePageRange parses the CLI form, e.g. "5-200,210". Page numbers are one-based in the text form and stored zero-based.
type PageSize ¶
type PageSize struct {
Width float64 `json:"width"`
Height float64 `json:"height"`
Rotate int `json:"rotate"`
Pages int `json:"pages"`
}
PageSize is one distinct page geometry and how many pages share it.
type ProbeBlock ¶
type ProbeBlock struct {
Bounds Rect `json:"bounds"`
Lines int `json:"lines"`
Kind string `json:"kind,omitempty"`
Level int `json:"level,omitempty"`
Paragraphs []string `json:"paragraphs"`
}
ProbeBlock is one segmented block with its paragraphs.
type ProbeGlyph ¶
type ProbeGlyph struct {
Rune string `json:"rune"`
X float64 `json:"x"`
Y float64 `json:"y"`
Advance float64 `json:"advance"`
Size float64 `json:"size"`
Rise float64 `json:"rise,omitempty"`
Rotation float64 `json:"rotation,omitempty"`
Font string `json:"font,omitempty"`
Mode int `json:"render_mode,omitempty"`
Missing bool `json:"missing,omitempty"`
}
ProbeGlyph is one positioned character.
type ProbeLine ¶
type ProbeLine struct {
Text string `json:"text"`
Baseline float64 `json:"baseline"`
Bounds Rect `json:"bounds"`
Size float64 `json:"size"`
Font string `json:"font,omitempty"`
Glyphs int `json:"glyphs"`
}
ProbeLine is one assembled line.
type ProbePage ¶
type ProbePage struct {
Page int `json:"page"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Rotate int `json:"rotate"`
Glyphs []ProbeGlyph `json:"glyphs,omitempty"`
Lines []ProbeLine `json:"lines,omitempty"`
Blocks []ProbeBlock `json:"blocks,omitempty"`
}
ProbePage is one page of probe output.
type ProbeResult ¶
type ProbeResult struct {
Stage ProbeStage `json:"stage"`
Pages []ProbePage `json:"pages"`
Notes []string `json:"notes,omitempty"`
}
ProbeResult is the intermediate model dump, per spec principle 5.
type ProbeStage ¶
type ProbeStage string
ProbeStage names an inspectable point in the pipeline.
const ( // StageGlyphs dumps positioned glyphs straight out of the content stream // interpreter. StageGlyphs ProbeStage = "glyphs" // StageLines dumps assembled lines. StageLines ProbeStage = "lines" // StageBlocks dumps segmented blocks. StageBlocks ProbeStage = "blocks" // StageStructure dumps classified blocks with their kinds and levels. StageStructure ProbeStage = "structure" )
func (ProbeStage) Valid ¶
func (s ProbeStage) Valid() bool
Valid reports whether s names a known stage.
type Profile ¶
type Profile string
Profile constrains output for a class of reading device. See spec section 5.
const ( // ProfileStandard is the unconstrained default. ProfileStandard Profile = "standard" // ProfileCrossPoint targets the Xteink X4 running CrossPoint firmware: // a 480x800 E Ink panel on an ESP32-C3 with roughly 380 KB of usable RAM. ProfileCrossPoint Profile = "crosspoint" // ProfileMinimal drops images and stylesheets entirely. ProfileMinimal Profile = "minimal" )
type ProfileDoc ¶ added in v1.1.0
type ProfileDoc struct {
// Name identifies the profile. It is not matched against anything and
// exists so a file says what it is.
Name string `json:"name"`
// Description is free text for whoever reads the file next.
Description string `json:"description,omitempty"`
// Base names the built-in profile to start from. Empty means standard.
Base Profile `json:"base,omitempty"`
// Options overrides fields of [Options]; Heuristics overrides fields of
// [Heuristics]. Both are applied over the base profile's defaults.
Options json.RawMessage `json:"options,omitempty"`
Heuristics json.RawMessage `json:"heuristics,omitempty"`
}
ProfileDoc is a device profile in serializable form, so one can be written once and shared rather than compiled in.
The three built-in profiles in spec section 5 stay compiled in, because their values encode findings this repository is responsible for: the crosspoint numbers come from reading the firmware. A document extends that set for a device decant has never seen, without a rebuild.
Keys under Options and Heuristics are the field names of Options and Heuristics, matched without regard to case. That is deliberate: the field documentation is then the format documentation, and `go doc decant.Options` is the reference. Unknown keys are an error rather than a silent no-op, since a typo in a threshold would otherwise look like a threshold that did nothing.
A field the document omits keeps the value the base profile gave it, so a document states only what it changes.
func LoadProfileDoc ¶ added in v1.1.0
func LoadProfileDoc(r io.Reader) (*ProfileDoc, error)
LoadProfileDoc reads a profile document.
Only the document's own shape is checked here. Whether its values make a usable option set is decided by Options.ApplyProfileDoc and New.
type Rect ¶
type Rect struct {
MinX, MinY, MaxX, MaxY float64
}
Rect is an axis-aligned rectangle in page space, with y increasing downward from the top-left of the page.
type Report ¶
type Report struct {
// Source is the input filename.
Source string `json:"source"`
// PageCount is the source document's page count.
PageCount int `json:"page_count"`
// PagesConverted is how many pages the page range selected.
PagesConverted int `json:"pages_converted"`
Pages []PageMetrics `json:"pages"`
// Blocks counts blocks by kind.
Blocks map[BlockKind]int `json:"blocks"`
// Headings counts headings by level, indexed 1 through 6.
Headings map[int]int `json:"headings,omitempty"`
// BodyFont describes the document's computed body font, which every
// structure decision in spec section 4.6 is measured against.
BodyFont string `json:"body_font,omitempty"`
// MultiColumnPages counts pages where more than one column was detected.
MultiColumnPages int `json:"multi_column_pages"`
// ImagesPlaced counts distinct images carried into the EPUB, after
// deduplication.
ImagesPlaced int `json:"images_placed"`
// ImageBytes is the total encoded size of those images.
ImageBytes int `json:"image_bytes"`
// Hyphenation summarizes the dehyphenation decisions in spec 4.6.
Hyphenation HyphenationReport `json:"hyphenation"`
// Tables counts detected tables by confidence, which is what
// --table-mode=auto keys on.
Tables map[string]int `json:"tables,omitempty"`
// RTLLetterRatio is the fraction of the document's letters belonging to a
// right-to-left script, and VerticalTextPages counts pages using a
// vertical writing mode. Spec section 1 puts both out of scope beyond
// basic text extraction.
RTLLetterRatio float64 `json:"rtl_letter_ratio,omitempty"`
VerticalTextPages int `json:"vertical_text_pages,omitempty"`
// FurnitureRemoved counts blocks dropped as running heads or folios.
FurnitureRemoved int `json:"furniture_removed"`
// VectorPagesDropped counts pages carrying vector artwork that decant did
// not render, and VectorPaintsDropped the painted paths on them.
//
// Spec section 1 puts vector conversion out of scope for v1 and section
// 13 keeps rasterization open. Reporting the loss is what principle 3
// requires in the meantime: a chart drawn as paths otherwise disappears
// with no trace in the output or the report.
VectorPagesDropped int `json:"vector_pages_dropped,omitempty"`
VectorPaintsDropped int `json:"vector_paints_dropped,omitempty"`
// Chapters is the number of XHTML files written.
Chapters int `json:"chapters"`
// OutputBytes is the size of the EPUB.
OutputBytes int64 `json:"output_bytes"`
// LargestChapterBytes is the biggest single XHTML document, which is the
// dominant failure mode on the crosspoint profile.
LargestChapterBytes int `json:"largest_chapter_bytes"`
Diagnostics []Diagnostic `json:"diagnostics"`
// QualityScore is a 0 to 100 summary. See Finish for its derivation.
QualityScore int `json:"quality_score"`
}
Report describes one conversion. It is written as JSON by --report and surfaced by the CrossPoint TUI to flag conversions worth reviewing.
Example ¶
Act on what the conversion reported.
A warning does not mean failure: the EPUB is still valid. It means a heuristic fired somewhere worth reviewing.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sroberts/decant"
)
// openPDF is the boilerplate every example needs: decant reads through
// io.ReaderAt and needs the size, so a caller passes both.
func openPDF(path string) (*os.File, int64, error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
}
return f, st.Size(), nil
}
func main() {
in, size, err := openPDF("book.pdf")
if err != nil {
log.Fatal(err)
}
defer in.Close()
conv, err := decant.New(decant.DefaultOptions())
if err != nil {
log.Fatal(err)
}
out, err := os.Create("book.epub")
if err != nil {
log.Fatal(err)
}
defer out.Close()
rep, err := conv.Convert(context.Background(), in, size, out)
if err != nil {
log.Fatal(err)
}
if rep.QualityScore < 80 {
fmt.Printf("quality %d/100, worth reviewing\n", rep.QualityScore)
}
for _, d := range rep.Diagnostics {
if d.Severity == decant.SeverityWarning {
fmt.Printf("%s: %s\n", d.Stage, d.Message)
}
}
}
Output:
func (*Report) DecodeFailureRate ¶
DecodeFailureRate returns the document-wide rate.
func (*Report) Finish ¶
func (r *Report) Finish()
Finish computes the quality score. It runs at the end of Write.
The score starts at 100 and subtracts for the failure modes that most often indicate output worth reviewing by hand. It is a triage signal, not a measurement: the TUI uses it to decide which conversions to surface.
func (*Report) MedianGlyphsPerPage ¶
MedianGlyphsPerPage returns the median across converted pages, which the scanned-document classifier in spec section 6 keys on.
type SplitMode ¶
type SplitMode string
SplitMode selects where chapter files break.
const ( // SplitAtH1 breaks at top-level headings. This is the default. SplitAtH1 SplitMode = "h1" // SplitAtH2 breaks at second-level headings. SplitAtH2 SplitMode = "h2" // SplitAtPage breaks at every source page. SplitAtPage SplitMode = "page" // SplitAtNone emits one chapter, subject to MaxChunkBytes. SplitAtNone SplitMode = "none" )
type StyleRun ¶
type StyleRun struct {
// Start and End are byte offsets into the owning block's Text, half-open.
Start, End int
// Bold and Italic are derived from the font's /FontDescriptor flags and
// its family-name suffix, per spec section 4.6.
Bold, Italic bool
}
Block is one unit of reconstructed content in reading order.
Document exposes these so a caller can correct structure before serialization; the CrossPoint TUI uses that to let a reader fix heading levels. Mutating Kind, Level, or Text is supported. Page and Bounds are provenance and should be left alone. StyleRun is a range of a block's text set in bold, italic, or both.
A caller may drop or narrow a run the same way it may edit any other field of the block tree; the renderer emits strong and em from these alone.
type TableCell ¶
type TableCell struct {
Text string
// ColSpan is 1 unless the cell spans columns, which happens where a
// vertical ruling line is absent between two boundaries.
ColSpan int
}
TableCell is one cell of a detected table.
type TableMode ¶
type TableMode string
TableMode selects how a detected table is emitted, per spec section 4.8.
const ( // TableAuto picks by detection confidence: a real table when both // signals fire, a rasterized region at medium confidence, and // space-preserved text at low. TableAuto TableMode = "auto" // TableHTML always emits a table element. TableHTML TableMode = "html" // TableText emits space-preserved text inside a pre element. TableText TableMode = "text" // TableDrop discards detected tables, leaving their text as paragraphs. TableDrop TableMode = "drop" )
type UsageError ¶
type UsageError struct{ Err error }
UsageError reports an invalid option combination. The CLI maps this to exit code 2.
func (*UsageError) Error ¶
func (e *UsageError) Error() string
func (*UsageError) Unwrap ¶
func (e *UsageError) Unwrap() error
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
decant
command
Command decant converts fixed-layout PDF to reflowable EPUB 3.
|
Command decant converts fixed-layout PDF to reflowable EPUB 3. |
|
internal
|
|
|
epub
Package epub serializes an EPUB 3.3 container with an EPUB 2 NCX fallback.
|
Package epub serializes an EPUB 3.3 container with an EPUB 2 NCX fallback. |
|
hyphen
Package hyphen implements Liang's hyphenation algorithm over TeX pattern sets, which spec section 4.6 uses inverted to decide whether a line-break hyphen is a typesetting artifact or part of the word.
|
Package hyphen implements Liang's hyphenation algorithm over TeX pattern sets, which spec section 4.6 uses inverted to decide whether a line-break hyphen is a typesetting artifact or part of the word. |
|
images
Package images re-encodes extracted PDF images for EPUB delivery.
|
Package images re-encodes extracted PDF images for EPUB delivery. |
|
layout
Package layout turns positioned glyphs into lines, blocks, and structured content.
|
Package layout turns positioned glyphs into lines, blocks, and structured content. |
|
testpdf
Package testpdf builds small, valid PDFs in memory for tests.
|
Package testpdf builds small, valid PDFs in memory for tests. |