scr

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

ZCUT asset collections.

A ZCUT file is a container holding any number of independent image assets - icons, widgets, sprites, fragments - of differing sizes. Each asset carries its own dimensions, a name, and either a colour bitmap with per-cell attributes or a plain bitmap (which doubles as a 1-bit transparency mask).

Unlike .scr, a ZCUT stores bitmaps in LINEAR row-major order, not the Spectrum display interleave. The interleave is a property of a pixel's position on the full screen and is undefined for a free-floating asset, so a linear layout is the only well-defined choice. The Spectrum byte conventions (1 bit per pixel, one attribute byte per 8x8 cell) are preserved; only the ordering is linearised.

Layout (all multi-byte fields little-endian):

file preamble (8 bytes)
  +0  "ZCUT"      magic
  +4  version u8  = 1
  +5  flags   u8  reserved (must be 0)
  +6  reserved u8 (must be 0)
  +7  numAssets u8
then numAssets chunks, each:
  chunk header (16 bytes)
    +0  "IMAG"    tag
    +4  payloadLen u32   exact payload size
    +8  chunkLen   u32   payload size padded up to a multiple of 8 (stride)
    +12 reserved   u32   (must be 0)
  payload (chunkLen bytes; payloadLen meaningful, remainder zero pad)
    +0  widthPx  u16
    +2  heightPx u16
    +4  flags    u8   bit0 = has attributes, bit1 = is mask
    +5  reserved [2]  (must be 0)
    +7  nameLen  u8
    +8  name     [roundUp8(nameLen)]  ASCII, zero-padded
    ..  bitmap   [ceil(w/8)*h]        linear, row-major
    ..  attrs    [cellsW*cellsH]      only if flags bit0

Package scr reads and writes ZX Spectrum SCR screen files.

An SCR file is a verbatim 6912-byte dump of the Spectrum's display memory: a 6144-byte pixel bitmap followed by a 768-byte attribute map. It carries no machine state - no registers, no entry point - so it is a pure framebuffer format, closer in spirit to a tape block than to a snapshot.

The bitmap is not stored in linear row order. The Spectrum interleaves screen rows so that a row's byte address is built from three separate fields of the y coordinate. Writing rows linearly produces the classic scrambled-thirds image; this package handles the interleave in both directions.

The core of this package works in memory. A Screen is a plain 256x192 grid of palette indices plus per-cell attributes; Encode turns it into the 6912 bytes of an SCR file and Decode reverses that. Conversion from ordinary images (PNG, JPEG, GIF) lives in convert.go.

Index

Constants

View Source
const (
	// Width and Height are the Spectrum display dimensions in pixels.
	Width  = 256
	Height = 192

	// Cols and Rows are the attribute grid dimensions, in 8x8 character cells.
	Cols = 32
	Rows = 24

	// CellSize is the side of one attribute cell, in pixels.
	CellSize = 8

	// FileLen is the total size of an SCR file.
	FileLen = bitmapLen + attrLen
)
View Source
const (
	Black uint8 = iota
	Blue
	Red
	Magenta
	Green
	Cyan
	Yellow
	White
)

Palette colour indices, in the order the attribute byte encodes them.

Variables

View Source
var NativeGeometry = Geometry{OriginX: 0, OriginY: 0, Scale: 1}

NativeGeometry is the geometry of an image that already *is* the raw 256x192 Spectrum display, pixel for pixel -- what ToImage produces, and what a correctly-cropped screenshot should be resized or cropped to before OCR if a caller does not want to specify Geometry directly.

Functions

func ApplyMask

func ApplyMask(a, mask *Asset) error

ApplyMask filters an asset's bitmap through a mask asset of identical dimensions: a target bit is cleared wherever the mask bit is unset. This is the standalone-mask filtering operation. Both assets must share dimensions.

func AssetToImage

func AssetToImage(a *Asset, ink, paper color.Color, paperTransparent bool) *image.RGBA

AssetToImage renders an asset to an RGBA image at its own dimensions. An attributed asset is drawn in its stored colours (ink on paper). A bitmap-only asset is drawn with the supplied ink and paper; if paperTransparent is true, its unset pixels are left fully transparent so it can be composited. The ink and paper arguments are ignored for an attributed asset.

func Crop

func Crop(src image.Image, r Rect) (*image.RGBA, error)

Crop returns the sub-image of src described by r. The result is a new RGBA image with its own pixel storage (not a view into src). r must lie within src's bounds and have positive dimensions.

func DecodeImage

func DecodeImage(r io.Reader) (image.Image, error)

DecodeImage reads a PNG, JPEG, or GIF from r and returns it as an image.Image. It is a thin wrapper over image.Decode, exposed so callers can inspect or resize an image before conversion.

func Encode

func Encode(s *Screen) []byte

Encode serialises a Screen into the 6912 bytes of an SCR file.

func EncodeCollection

func EncodeCollection(c *Collection) ([]byte, error)

EncodeCollection serialises a collection to ZCUT bytes.

func Fit

func Fit(src image.Image, mode ResizeMode, fill color.Color) (image.Image, error)

Fit returns a 256x192 image produced from src according to mode. The fill colour is used for padding in ResizeBestFit and ResizeCentre (it has no effect on ResizeStretch, which leaves no border). ResizeNone returns src unchanged when it is already 256x192 and a *SizeError otherwise.

The scaler used for ResizeStretch and ResizeBestFit is a simple bilinear resample. That is deliberate: the conversion that follows collapses every 8x8 cell to two colours, so a higher-order resampler's extra fidelity would be destroyed by the attribute reduction and is not worth a dependency.

func ParseBool

func ParseBool(s string) (bool, error)

ParseBool resolves a boolean token. It accepts 1/0, true/false, yes/no, on/off, and any non-zero/zero integer. Surrounding spaces are ignored.

func ParseColour

func ParseColour(s string) (uint8, error)

ParseColour resolves a colour token to a palette index (0-7). The token is either a colour name (case-insensitive) or a numeric index 0-7. Surrounding spaces are ignored.

func Paste

func Paste(s *Screen, a *Asset, x, y int, op PasteOp) error

Paste blits an asset onto a screen at pixel position (x, y), combining the asset's bitmap with the target according to op. If the asset carries attributes and (x, y) is cell-aligned, the attributes are written too; otherwise attributes are skipped. The asset must fit within the screen.

func RecognizeScreen added in v0.6.0

func RecognizeScreen(s *Screen) ([]string, error)

RecognizeScreen is RecognizeText specialised for an already-decoded Screen: no Geometry is needed, since a Screen is always exactly the native 256x192 pixels with no border or scaling to account for.

func RecognizeText added in v0.6.0

func RecognizeText(img image.Image, geom Geometry) ([]string, error)

RecognizeText reads every one of the 32x24 character cells of img at the given geometry, and returns the result as 24 lines of text, each right-trimmed of trailing spaces (matching how the real screen would print -- trailing blank cells carry no information). Text outside the standard 96-glyph ROM set (box-drawing borders, UDGs, decorative graphics) is matched to its nearest visual approximation rather than skipped; callers reading known-textual regions can ignore this, and callers who need to tell text from graphics should crop to the region that is actually text first.

func ToImage

func ToImage(s *Screen) *image.RGBA

ToImage renders a Screen back to an RGBA image, for previewing or for round-trip verification. The result is 256x192.

func ToImageBitmap

func ToImageBitmap(s *Screen, bits int, ink, paper color.Color, paperTransparent bool) *image.RGBA

ToImageBitmap renders a Screen's bitmap, ignoring its attributes, into a fresh 256x192 RGBA image. Set bits (per the bits argument: 1 means ink pixels, 0 means paper pixels) are painted with ink; the other pixels are painted with paper. If paperTransparent is true the non-set pixels are left fully transparent instead, so an extracted sprite can be composited over any background.

This is the rendering path for bitmap-only sprite extraction: the shape comes from the bitmap, and the colours come from the caller (typically a parsed attribute) rather than from the screen's own attributes.

Types

type Asset

type Asset struct {
	Name   string
	Width  int
	Height int
	IsMask bool
	// Ink is row-major [y][x], length Height x Width; true = set bit.
	Ink [][]bool
	// Attr is the per-cell grid [cellY][cellX]; nil if the asset has no
	// attributes (bitmap-only or mask).
	Attr [][]Attribute
}

Asset is one image in a collection: its dimensions, name, a linear bitmap, and (for colour assets) per-cell attributes. A mask asset is a bitmap-only asset flagged for use as a stencil.

func CutCells

func CutCells(s *Screen, cx, cy, cw, ch int, name string, keepAttrs bool) (*Asset, error)

CutCells extracts a cell-aligned region given in character cells.

func CutRegion

func CutRegion(s *Screen, r Rect, name string, keepAttrs bool) (*Asset, error)

CutRegion extracts a pixel region of a screen as an Asset. If keepAttrs is true and the region is cell-aligned, the asset carries the region's attributes; otherwise it is a bitmap-only asset. A region that is not cell-aligned always produces a bitmap-only asset regardless of keepAttrs.

func (*Asset) HasAttrs

func (a *Asset) HasAttrs() bool

HasAttrs reports whether the asset carries attributes.

func (*Asset) MapAttributes

func (a *Asset) MapAttributes(f func(Attribute) Attribute)

MapAttributes applies f to every attribute cell of an asset, leaving the bitmap untouched. It is a no-op on a bitmap-only asset.

type Attribute

type Attribute struct {
	Ink    uint8
	Paper  uint8
	Bright bool
	Flash  bool
}

Attribute is one attribute cell: an ink and paper colour (each 0..7), a bright flag, and a flash flag. It maps to a single byte as ink | paper<<3 | bright<<6 | flash<<7.

func AttributeFromByte

func AttributeFromByte(b byte) Attribute

AttributeFromByte unpacks a stored attribute byte.

func ParseAttribute

func ParseAttribute(s string) (Attribute, error)

ParseAttribute parses a CSS-like attribute spec into an Attribute. See the package-level syntax description above. An empty (or all-whitespace) spec yields the all-black default.

func (Attribute) Byte

func (a Attribute) Byte() byte

Byte packs the attribute into its stored form.

func (Attribute) InkRGBA

func (a Attribute) InkRGBA() color.RGBA

InkRGBA returns the colour this attribute's ink renders as, at its brightness.

func (Attribute) PaperRGBA

func (a Attribute) PaperRGBA() color.RGBA

PaperRGBA returns the colour this attribute's paper renders as, at its brightness. It lets callers map an attribute to a concrete colour without reaching into the conversion internals - for instance to use an attribute as a fill colour when resizing an image before conversion.

func (Attribute) String

func (a Attribute) String() string

String renders an Attribute back to its CSS-like form, the inverse of ParseAttribute. Colours are emitted as names. Useful for round-tripping and for echoing a parsed attribute back to a user.

type Collection

type Collection struct {
	Assets []Asset
}

Collection is an ordered set of named assets.

func DecodeCollection

func DecodeCollection(data []byte) (*Collection, error)

DecodeCollection parses ZCUT bytes into a collection, validating structure.

func (*Collection) Find

func (c *Collection) Find(name string) *Asset

Find returns the first asset with the given name, or nil.

type Geometry added in v0.6.0

type Geometry struct {
	OriginX int // pixel x, within the source image, of the display's top-left corner
	OriginY int // pixel y, within the source image, of the display's top-left corner
	Scale   int // pixels per emulated pixel (an 8x8 font cell is Scale*8 square)
}

Geometry describes where a 256x192 Spectrum display sits within a larger captured image, and at what scale it was rendered. A plain .scr file (or any image already cropped to exactly the native resolution) needs Geometry{0, 0, 1}; an emulator window screenshot usually needs both a non-zero origin (past the window's own border/chrome) and a scale factor greater than 1.

type PasteOp

type PasteOp int

PasteOp selects how an asset's bitmap bits combine with the target screen's existing bits, following the classic Spectrum sprite blit operations.

const (
	// PasteOR sets target bits where the asset bit is set and leaves the target
	// unchanged elsewhere (paint the sprite on). This is the default and the
	// natural operation for a bitmask or data blit.
	PasteOR PasteOp = iota
	// PasteAND clears target bits where the asset bit is clear and leaves the
	// target unchanged where the asset bit is set (punch a hole with a mask).
	PasteAND
	// PasteCOPY overwrites every target bit with the asset bit, set or clear.
	PasteCOPY
	// PasteXOR toggles target bits where the asset bit is set (cheap reversible
	// draw/erase).
	PasteXOR
)

type Rect

type Rect struct {
	X, Y, W, H int
}

Rect is a crop rectangle in pixels: origin (X, Y) with width W and height H. W and H need not be multiples of 8.

func AutoExtent

func AutoExtent(src image.Image, bg color.Color, tol int) (Rect, error)

AutoExtent returns the smallest Rect covering every pixel of src that differs from the background colour. If bg is nil the background is inferred as the image's most common edge colour (sampling the four borders), which handles the usual case of a sprite on a flat field without the caller naming the colour.

A pixel "differs" when any RGB channel is more than tol (0-255) away from the background; tol absorbs antialiasing and minor compression noise. If no pixel differs, AutoExtent returns an error rather than an empty rectangle.

func BitmapExtent

func BitmapExtent(s *Screen, bits int) (Rect, error)

BitmapExtent returns the smallest Rect covering every set bitmap bit of a Screen. "Set" means an ink pixel (Ink[y][x] == true) when bits is 1, or a paper pixel (Ink[y][x] == false) when bits is 0 - the latter for sprites drawn as paper-on-ink. Attributes are ignored entirely: a sprite's extent is defined by its bitmap, not by what colours its cells happen to carry.

If no pixel matches, BitmapExtent returns an error rather than an empty rectangle.

func CellRect

func CellRect(cx, cy, cw, ch int) Rect

CellRect converts a rectangle expressed in 8x8 character cells to a pixel Rect. A cell rectangle at cell (cx, cy) spanning cw by ch cells covers (cx*8, cy*8) with size (cw*8, ch*8).

type ResizeMode

type ResizeMode int

ResizeMode selects how Fit brings a source image to the 256x192 screen size.

const (
	// ResizeNone requires the source to be exactly 256x192 and errors otherwise.
	// It is the default, so a caller that does not opt into a resize is told
	// which modes exist rather than having a policy chosen for it.
	ResizeNone ResizeMode = iota
	// ResizeStretch scales to fill the whole screen, ignoring aspect ratio.
	ResizeStretch
	// ResizeBestFit scales to fit within the screen preserving aspect ratio,
	// centring the result and padding the remaining border with fill.
	ResizeBestFit
	// ResizeCentre does not scale: it centres the source on the screen, cropping
	// any overflow and padding any shortfall with fill.
	ResizeCentre
)

type Screen

type Screen struct {
	// Ink is row-major, Ink[y][x]; true means the pixel shows its cell's ink
	// colour, false its paper colour.
	Ink [Height][Width]bool
	// Attr is the attribute grid, Attr[cellY][cellX].
	Attr [Rows][Cols]Attribute
}

Screen is an in-memory ZX Spectrum screen: a pixel grid where each pixel is either ink (true) or paper (false), plus one Attribute per 8x8 cell. This is the neutral representation the codec encodes from and decodes to; callers adapt their own pixel data to a Screen rather than to the SCR byte layout.

func Decode

func Decode(data []byte) (*Screen, error)

Decode parses the 6912 bytes of an SCR file into a Screen.

func FromImage

func FromImage(img image.Image) (*Screen, error)

FromImage reduces img to a Spectrum Screen.

The image must already be 256x192; FromImage does not resize, because the right resizing strategy (fit, fill, letterbox) is a caller decision and a naive resize here would silently distort logos and screenshots. Callers that need scaling should resize to 256x192 first. An image of the wrong size returns an error.

For each 8x8 cell, the two colours are chosen by luminance: the darkest distinct colour present becomes paper, the brightest becomes ink. A cell holds a single bright bit, decided from the ink colour (the visually dominant foreground); a black ink or paper, being brightness-agnostic, does not force the bit. Every pixel is then set to ink or paper by whichever it is nearer.

type SizeError

type SizeError struct {
	Got image.Point
}

SizeError reports that an image passed to FromImage was not 256x192.

func (*SizeError) Error

func (e *SizeError) Error() string

Jump to

Keyboard shortcuts

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