convert

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: BSD-3-Clause Imports: 19 Imported by: 0

README

convert

Pictures into a PDF, a PDF back into pictures, and a PDF redrawn. Pure Go, CGO-free, including GOOS=js.

pdfconv to-pdf   -dpi 300 -o scan.pdf page1.jpg page2.jpg page3.jpg
pdfconv from-pdf -dpi 150 -format png -pages 1,4-6 -o ./pages scan.pdf
pdfconv redraw   -greyscale -invert out.pdf grey.pdf
pdfconv redraw   -scanner -skew 0.6 clean.pdf looks-scanned.pdf
pdfconv formats
pdf, err := convert.ToPDF(images, convert.Options{DPI: 300})
err = convert.WriteTo(f, images, convert.Options{DPI: 300, Page: pdfkit.A4})
pages, err := convert.FromPDF(pdf, convert.RasterOptions{DPI: 150, Format: "png"})
out, err := convert.Redraw(pdf, convert.RedrawOptions{Greyscale: true})

Why it is small

Both ends were already here. go-pdfkit/pdfkit writes a PDF and can place an image on a page; go-pdfkit/render draws a page into a raster; go-images/images already had Invert, Grayscale, AdjustBrightness, AdjustContrast and Rotate. What was missing was the join — so this is wiring and codecs, not a new engine.

redraw RASTERISES, and says so everywhere

⛔ What comes out has no text in it: no selection, no search, no copy, no screen reader, and a much larger file. That is not a shortcoming — changing the colours a page is painted in means painting it — but it is a thing a caller has to have decided on purpose, which is why the verb is named for what it does rather than for what it is for.

Rotating, cropping, stamping and reordering keep the text. Those live in go-pdfkit/ops, not here, and redraw with no change asked for says so on stderr before it does anything.

-greyscale drop the colour
-invert turn light into dark
-brightness −1 (black) to 1 (white)
-contrast 1 leaves it, 2 doubles it, 0.5 halves it
-background #rgb, #rrggbb or #rrggbbaa, painted by the renderer
-scanner skew, grain and a worn lamp

The order of the transforms is written down rather than left to chance: colour, then tone, then the scanner's damage last. Greyscale after a contrast change is not the same picture as contrast after greyscale, and a caller who sets both would otherwise be guessing.

The scanner's grain is reproducible: a zero Seed is a fixed seed, not a random one, because two runs over the same file must produce the same bytes or nothing downstream can be compared, cached or checksummed. ⛔ The page index is mixed into it, or every page of a document gets identical grain — the one thing a scanner never does.

Two defects this found, both invisible to a byte count
-background painted nothing it was composited under the finished raster, and the renderer had already filled the page with opaque white. The option was documented, shipped and inert.
-brightness changed nothing images.AdjustBrightness adds its delta in channel units, 0–255, not in the −1..1 this option is documented in. A brightness of 0.4 added 0.4 of a level out of 255.

Both were found by tests that read pixels. The page count, the byte count and the exit status were right throughout.

Formats

read PNG, JPEG, GIF (standard library); BMP, TIFF, WebP (golang.org/x/image)
write PNG, JPEG, GIF, BMP, TIFF

⛔ WebP is refused by name, not as an unknown format. There is no WebP encoder in pure Go. "I do not know that format" would be a lie — it reads here perfectly well — and writing a PNG under a .webp name would be worse than either. The two refusals are different sentences and a test asserts that neither reads like the other.

⛔ The content decides the format, never the name. A scanner writing JPEG into page.png is ordinary; a converter that believed the extension would hand the decoder the wrong reader, or refuse a file it can read.

What decides the page

A picture has pixels, a page has points, and only the DPI says which page a picture is:

pdfconv to-pdf -dpi 300 scan.jpg   # 2480x3508 comes out A4
pdfconv to-pdf -dpi 72  scan.jpg   # the same file comes out a metre tall

With no -page, each page is sized to its own picture — which is what a set of scans of different sizes needs, and why an empty page size is an instruction rather than a missing value. With -page a4 the picture is fitted inside the paper:

  • ⛔ one scale for both axes. Scaling them separately fills the page and stretches the picture, and a stretched scan is a defect nobody reports because the page looks full;
  • ⛔ only ever shrink. "Fit on this paper" is not "make it as large as this paper": a 32-pixel logo blown up to A4 is a poster nobody asked for;
  • centred, and a margin wider than the paper gives way rather than erasing the picture — the picture is the content, the margin is a preference.

What it refuses

no pictures an empty PDF is a file, and a caller who passed an empty slice sees one and believes it worked
a picture past MaxPixels the size is decided by the input; the default ceiling is a hundred megapixels
-margin with no -page a page sized to its picture has no room to leave, and ignoring it lets somebody believe their pages have one
a page number past the end and it says how many there are
a PDF that opens and holds no pages said out loud, because an empty result otherwise reads like a selection that matched nothing

A page that cannot be written stops the run: a directory that looks like a complete conversion and is one page short is not something anybody checks. For the same reason the output directory is created after the conversion succeeds, so a failure leaves nothing behind that could be mistaken for "no pages".

Checks

100 % statement coverage on both packages, go vet and gofmt clean, and builds for nine targets including js/wasm, linux/loong64 and linux/s390x.

The end-to-end witness is a picture, not a number: a four-quadrant swatch goes in, comes back through a real rasteriser, and each quadrant is sampled in its own corner — ⛔ a flat fill survives being drawn upside down, so the test has to be able to see an orientation it does not expect.

Licence

BSD-3-Clause.

Documentation

Overview

Package convert turns pictures into a PDF and a PDF back into pictures.

Everything either side of the PDF was already here: github.com/go-pdfkit/pdfkit writes one and can place an image on a page, and github.com/go-pdfkit/render draws a page into a raster. What was missing was the join — which is why this package is wiring and codecs rather than a new engine.

Formats

Reading: PNG, JPEG and GIF from the standard library; BMP, TIFF and WebP from golang.org/x/image. Writing: PNG, JPEG, GIF, BMP and TIFF.

⛔ There is no WebP ENCODER in pure Go, so FromPDF refuses "webp" by name instead of quietly writing something else. A converter that answers a request it did not carry out is worse than one that says no.

What decides the page

A picture has pixels and a DPI; a PDF page has points. ToPDF sizes each page to its picture at the DPI given, so a 300-dpi scan comes back the size of the paper it came off. Asking for a fixed page size instead fits the picture inside it, centred, never cropping and never stretching: see Options.Page.

Pure Go

CGO-free, including GOOS=js. No C anywhere in the chain.

Index

Constants

View Source
const DefaultMaxPixels = 100_000_000

DefaultMaxPixels is how large a picture Decode will read when no ceiling is given. A hundred megapixels is A4 at a thousand dots to the inch.

⛔ Said in bytes, because "a hundred megapixels" does not tell anybody what it permits: a decoded picture is four bytes a pixel, so this default lets a file of a few dozen bytes cost FOUR HUNDRED MEGABYTES. That is bounded, which is the property that matters and the one this package did not have — but a service taking pictures from strangers should set Options.MaxPixels to what its own pages actually need rather than inherit this.

Variables

This section is empty.

Functions

func Decode

func Decode(r io.Reader) (image.Image, string, error)

Decode reads a picture and says what format it turned out to be.

⛔ It reads the CONTENT. A file's name is a claim its bytes do not have to honour: a scanner that writes JPEG into "page.png" is ordinary, and a converter that believed the extension would hand the encoder the wrong picture or refuse a file it can read perfectly well. It refuses a picture of more than DefaultMaxPixels pixels, and it refuses it by its HEADER — see decodeWithin for why that distinction is the guard.

func DecodeBytes

func DecodeBytes(b []byte) (image.Image, string, error)

DecodeBytes is Decode over a slice.

func Encoders

func Encoders() []string

Encoders names every format FromPDF can write, in a stable order.

func PageSizes

func PageSizes() []string

PageSizes names every paper size this package knows, in a stable order.

func ParsePageSize

func ParsePageSize(s string) (pdfkit.PageSize, error)

ParsePageSize reads a paper size: a name such as "a4", optionally with "-landscape", or "WIDTHxHEIGHT" in points.

⛔ "" is NOT an error and NOT a default size: it means "size each page to its own picture", which is a different instruction from "use A4". A set of scans of different sizes must not all be forced onto one paper because the caller said nothing.

func ReadersToPDF

func ReadersToPDF(rs []io.Reader, opt Options) ([]byte, error)

ReadersToPDF decodes each reader and lays the pictures out as ToPDF does. The format of each is decided by its CONTENT, never by a name.

func Redraw added in v0.3.0

func Redraw(pdf []byte, opt RedrawOptions) ([]byte, error)

Redraw draws every page, changes the pixels, and lays the results back into a new PDF.

⛔ IT RASTERISES. What comes out has no text in it: no selection, no search, no copy, no screen reader, and a file many times larger. That is not a shortcoming of this implementation — changing the colours a page is PAINTED in means painting it — but it is a thing a caller has to have decided on purpose, which is why the function is named for what it does rather than for what it is for.

Where the operation can be done without redrawing, it belongs somewhere else: rotating, cropping, stamping and reordering are github.com/go-pdfkit/ops, and they keep the text.

func ToPDF

func ToPDF(images []image.Image, opt Options) ([]byte, error)

ToPDF lays each picture on a page of its own, in the order given.

func WriteTo

func WriteTo(w io.Writer, images []image.Image, opt Options) error

WriteTo is ToPDF straight to a writer, so a conversion of a hundred scans does not hold the finished PDF in memory as well as the pictures.

Types

type Encoder

type Encoder func(w io.Writer, m image.Image, quality int) error

Encoder is a picture format this package can WRITE. Reading is wider than writing — WebP decodes and does not encode — so the two are separate tables rather than one list with exceptions in it.

type Options

type Options struct {
	// DPI is how many pixels of the picture go to an inch of paper. Zero means
	// 72, at which one pixel is one point and a picture comes out at the size
	// a PDF viewer calls 100%.
	//
	// It is what turns pixels into a page: a 2480x3508 scan at 300 is A4, and
	// the same scan at 72 is a page a metre tall. There is no right answer this
	// package could pick for you, only a sane default.
	DPI float64

	// Page, when set, is the page size every picture is placed on, and the
	// picture is FITTED inside it: scaled down to fit if it is too big, centred
	// either way, never cropped and never stretched out of proportion.
	//
	// When it is the zero value each page is sized to its own picture, which is
	// what a set of scans of different sizes needs.
	Page pdfkit.PageSize

	// Margin is how much paper is left around a fitted picture, in points. It
	// is only consulted when Page is set, because a page sized to its picture
	// has no room to leave.
	Margin float64

	// Title goes in the document information dictionary.
	Title string

	// MaxPixels refuses a picture larger than this, so that a file cannot
	// decide how much memory this package uses. Zero means a hundred million,
	// which is A4 at a thousand dots to the inch and well past any scanner.
	MaxPixels int
}

Options says how pictures become pages.

type Page

type Page struct {
	Number int    // one-based, as the file numbers it
	Format string // the format actually written
	Data   []byte
}

Page is one drawn page and the bytes it encoded to.

func FromPDF

func FromPDF(pdf []byte, opt RasterOptions) ([]Page, error)

FromPDF draws the pages of a PDF and encodes each one.

func FromPDFReader

func FromPDFReader(r io.Reader, opt RasterOptions) ([]Page, error)

FromPDFReader is FromPDF over a reader, for a file on disk.

type RasterOptions

type RasterOptions struct {
	// DPI is dots per inch. Zero means 150, which is legible on a screen and
	// not so large that a hundred-page file fills a disk.
	DPI float64

	// Pages selects which pages to draw, one-based, in the order given. Nil
	// means every page in the file's own order.
	Pages []int

	// Format is what to encode as: png, jpeg, gif, bmp or tiff. Empty means
	// png, which is lossless and what a page of text should be.
	Format string

	// Quality is the JPEG quality, 1 to 100. Zero means 85. It is ignored by
	// every other format.
	Quality int

	// Password opens an encrypted file.
	Password string

	// MaxPixels refuses a page that would come out larger than this. Zero
	// leaves it to the renderer, whose own default is a little over A4 at 600
	// dots to the inch.
	MaxPixels int
}

RasterOptions says how pages become pictures.

type RedrawOptions added in v0.3.0

type RedrawOptions struct {
	RasterOptions

	// Title goes in the new document's information dictionary.
	Title string

	// Greyscale drops the colour.
	Greyscale bool

	// Invert turns light into dark. It is what a reader wants at night, and
	// what a plotter wants of a dark-background figure.
	Invert bool

	// Brightness shifts every channel, from -1 (black) to +1 (white). Zero
	// leaves it alone.
	Brightness float64

	// Contrast multiplies the distance from mid-grey: 1 leaves it alone, 2
	// doubles it, 0.5 halves it. Zero means 1 — ⛔ a zero that meant "no
	// contrast at all" would turn every page into a flat grey rectangle for
	// every caller who set only the other fields.
	Contrast float64

	// Background is the colour the page is painted on. nil leaves the
	// renderer's own white.
	//
	// ⛔ It is handed to the RENDERER rather than composited afterwards, and
	// the difference is the whole option. A first version painted the colour
	// under the finished raster — which does nothing at all, because the
	// renderer has already filled the page with opaque white and there is no
	// transparency left to show through. The option was documented, shipped,
	// and had no effect; a screenshot found it, a byte count did not.
	Background *color.RGBA

	// Scanner, when set, makes the page look like it went through one.
	Scanner *ScannerEffect
}

RedrawOptions says what to do to the pixels. Everything is off by default: the zero value redraws the pages unchanged, which is `rasterize-pdf`.

type ScannerEffect added in v0.3.0

type ScannerEffect struct {
	// Skew is how far the sheet sits off square, in degrees. A tenth to a
	// degree is what a careless hand produces.
	Skew float64

	// Noise is how much grain, from 0 to 1.
	Noise float64

	// Fade lightens the page, as a worn lamp does, from 0 to 1.
	Fade float64

	// Seed makes the grain reproducible. ⛔ Zero means a FIXED seed, not a
	// random one: two runs over the same file must produce the same bytes, or
	// nothing downstream can be compared, cached or checksummed — and an
	// effect that is different every time is one nobody can test.
	Seed uint64
}

ScannerEffect is the small damage a flatbed does: the sheet is never quite straight, the lamp is never quite even, and the sensor adds grain.

Directories

Path Synopsis
cmd
pdfconv command
pdfconv turns pictures into a PDF and a PDF back into pictures.
pdfconv turns pictures into a PDF and a PDF back into pictures.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL