screentone

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 9 Imported by: 0

README

screentone

Prepare comic and manga page images for e-ink devices.

Screentones are the adhesive dot and hatch tones manga artists shade with; rendered naively on a greyscale e-ink panel, scanned pages look washed out, band in gradients, and moiré in toned areas. screentone takes a decoded page image and produces one optimized for a specific panel: trimmed of scan margins, tone-corrected, split or marked when it's a double-page spread, and fitted to the panel's resolution.

  • Deterministic — the same input and options always produce the same output bytes, so results can be golden-tested and content-addressed.
  • Small — one Profile, one Process call with functional options, two encode helpers. Standard library plus golang.org/x/image only.

See SPEC.md for the full specification, including every stage's algorithm and thresholds.

Install

go get github.com/ophymx/screentone

Usage

src, _, err := image.Decode(f) // any decoded image
if err != nil {
	...
}

pages, err := screentone.Process(src, screentone.Elipsa)
if err != nil {
	...
}
for i, pg := range pages {
	out, _ := os.Create(fmt.Sprintf("page-%d.jpg", i))
	if err := screentone.EncodeJPEG(out, pg.Image, 85); err != nil {
		...
	}
	out.Close()
}

With default options a portrait page comes back as one Page; a landscape double-page spread is split at the midline into two pages, right half first (manga reading order). Each page carries a KindKindSingle, KindSpreadFirst, KindSpreadSecond, or KindSpread for an unsplit spread — so callers can emit page-spread metadata.

Options
Option Effect
WithSpreadPolicy(p) SplitRTL (default), SplitLTR, or Preserve (keep the spread whole, marked KindSpread)
WithoutTrim() keep scanned margins
WithTrimTolerance(n) how far a pixel must differ from the background to count as content (default 16)
WithoutAutoContrast() skip the histogram stretch
WithGamma(g) override the profile's gamma (1.0 = none)
WithoutResize() keep original dimensions
WithoutUpscale() pass pages smaller than the panel through at original size
WithQuantize() snap to the panel's grey levels (nearest level)
WithDither() quantize with Floyd–Steinberg error diffusion
Profiles
Var Device Resolution Grey levels
Elipsa Kobo Elipsa 1404×1872 16
ClaraHD Kobo Clara HD 1072×1448 16
Libra2 Kobo Libra 2 1264×1680 16
Sage Kobo Sage 1440×1920 16

Elipsa is the primary target. A Profile is a plain struct — build your own for other panels.

Out of scope

Archive formats (CBZ/EPUB), fetching and decoding, panel detection, webtoon layout, and color e-ink. This library does one thing: page image in, panel-ready image out.

Testing

go test ./...

Golden tests pin exact output bytes. Fixtures are generated deterministically in test code; only the golden outputs are committed. After an intentional behavior change (or a Go toolchain upgrade, which can change the standard library encoders' output), regenerate with:

go test -run Golden -update

License

MIT

Documentation

Overview

Package screentone prepares comic and manga page images for display on e-ink devices: fitting to a panel's resolution, tone correction for its contrast characteristics, spread splitting, and optional quantization to its native grey levels.

The same input and options always produce the same output bytes; the pipeline uses no clocks, randomness, or iteration-order-dependent state.

Index

Constants

This section is empty.

Variables

View Source
var (
	Elipsa  = Profile{Name: "elipsa", Width: 1404, Height: 1872, Levels: 16, Gamma: 1.0}
	ClaraHD = Profile{Name: "clara-hd", Width: 1072, Height: 1448, Levels: 16, Gamma: 1.0}
	Libra2  = Profile{Name: "libra-2", Width: 1264, Height: 1680, Levels: 16, Gamma: 1.0}
	Sage    = Profile{Name: "sage", Width: 1440, Height: 1920, Levels: 16, Gamma: 1.0}
)

Shipped profiles. Elipsa is the primary target; the others are secondary and untested on hardware.

Functions

func EncodeJPEG

func EncodeJPEG(w io.Writer, img image.Image, quality int) error

EncodeJPEG writes img as a baseline greyscale JPEG. Non-Gray images are converted first. quality is 1–100 (85 is a good default).

func EncodePNG

func EncodePNG(w io.Writer, img image.Image) error

EncodePNG writes img as an 8-bit greyscale PNG. Non-Gray images are converted first.

Types

type Option

type Option func(*config)

Option adjusts Process behavior.

func WithDither

func WithDither() Option

WithDither reduces output to the profile's grey levels with Floyd–Steinberg error diffusion. Implies quantization.

func WithGamma

func WithGamma(g float64) Option

WithGamma overrides the profile's gamma correction. 1.0 disables it.

func WithQuantize

func WithQuantize() Option

WithQuantize reduces output to the profile's grey levels by nearest-level thresholding.

func WithSpreadPolicy

func WithSpreadPolicy(sp SpreadPolicy) Option

WithSpreadPolicy sets how landscape spreads are handled.

func WithTrimTolerance

func WithTrimTolerance(levels uint8) Option

WithTrimTolerance sets how many luminance levels a pixel must differ from the background to count as content (default 16).

func WithoutAutoContrast

func WithoutAutoContrast() Option

WithoutAutoContrast disables the histogram stretch.

func WithoutResize

func WithoutResize() Option

WithoutResize disables fitting to the panel resolution.

func WithoutTrim

func WithoutTrim() Option

WithoutTrim disables margin trimming.

func WithoutUpscale

func WithoutUpscale() Option

WithoutUpscale passes pages smaller than the panel through at original size instead of scaling them up.

type Page

type Page struct {
	Image *image.Gray
	Kind  PageKind
}

Page is one output page.

func Process

func Process(src image.Image, p Profile, opts ...Option) ([]Page, error)

Process runs the pipeline on src for the given panel profile. It returns one Page, or two when a landscape spread is split.

type PageKind

type PageKind int

PageKind identifies a page's role in the output.

const (
	KindSingle       PageKind = iota // ordinary single page
	KindSpread                       // unsplit spread (Preserve policy or too wide to split)
	KindSpreadFirst                  // first reading page of a split spread
	KindSpreadSecond                 // second reading page of a split spread
)

type Profile

type Profile struct {
	Name   string  // stable identifier, e.g. "elipsa"
	Width  int     // panel width in pixels, portrait orientation
	Height int     // panel height in pixels, portrait orientation
	Levels int     // native grey levels, e.g. 16
	Gamma  float64 // default gamma correction; 1.0 = none
}

Profile bundles an e-ink panel's geometry and tuning.

type SpreadPolicy

type SpreadPolicy int

SpreadPolicy selects how landscape (double-page) images are handled.

const (
	// SplitRTL splits a spread at the vertical midline, right half first
	// (manga reading order). The default.
	SplitRTL SpreadPolicy = iota
	// SplitLTR splits a spread at the vertical midline, left half first.
	SplitLTR
	// Preserve passes a spread through unsplit, marked KindSpread.
	Preserve
)

Jump to

Keyboard shortcuts

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