tilemask

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 18 Imported by: 0

README

go-tilemask

Go


Bounded PNG alpha masks, grid-cell projection and Tiled TMX/TMJ occupied-tile decoding for game art. Package tilemask, module github.com/Rethunk-Tech/go-tilemask.

Answers one question about game art: which cells of an image, or which tiles of a map, are used. A cell is a square of a size the caller picks. A PNG becomes a one-bit-per-pixel mask (alpha not zero) that projects onto any grid, and a Tiled map becomes the occupied tiles of each top-level layer. Both decoders bound what their input claims (pixels, tiles, inflation) before allocating, so a few hostile bytes cannot ask for gigabytes. The caller bounds the input's byte size. Stdlib only.

Quick start

go get github.com/Rethunk-Tech/go-tilemask

A worked example and the contracts: HUMANS.md.

Features

  • A pixel is set when its alpha is not zero, at any bit depth; Mask.At and Mask.Cells read it.
  • Cells projects any region onto any cell size with floor division, so negative offsets land in negative cells. cell = 1 costs one byte per pixel while it runs.
  • The PNG fast path reads 8-bit, non-interlaced images and checks no CRC or Adler-32, needs no PLTE, and needs no IDAT for an opaque grey or RGB image; each leniency errs toward more coverage. Everything else goes through image/png.
  • DecodeMap reads TMX and TMJ under one tile cap for the whole map, declared and decoded, and refuses a layer or chunk that holds more values than it declared.
  • WriteTo and ReadMask store a mask as gzip of W H\n and the packed bits, least-significant bit first; the reader holds the header to the pixel cap and verifies the gzip trailer.
  • Errors say why: ErrTooLarge, ErrUnsupported (zstd, unknown encodings) or ErrMalformed.

Documentation

Doc Audience
HUMANS.md Running and using the package
AGENTS.md Internals, invariants, and why
CONTRIBUTING.md Process — commits, tests, docs
SECURITY.md Vulnerability disclosure
CHANGELOG.md Release notes

License

MIT — see LICENSE. © 2026 Rethunk.Tech, LLC.

Documentation

Overview

Package tilemask answers which cells of an image, or which tiles of a Tiled map, a game asset uses.

DecodePNG keeps one bit per pixel (alpha not zero) under a pixel cap checked before any buffer is allocated, and Mask.Cells projects that onto a grid of any cell size. DecodeMap lists each top-level tile layer's occupied tiles under one tile cap for the whole map. Every decoder bounds what its input claims, not the input's byte size: the caller limits the reader.

Index

Constants

View Source
const (
	// DefaultMaxPixels is the image size cap when Options.MaxPixels is zero.
	DefaultMaxPixels int64 = 100_000_000
	// DefaultMaxTiles is the map tile cap when Options.MaxTiles is zero.
	DefaultMaxTiles int64 = 4_000_000
)

Variables

View Source
var (
	// ErrTooLarge means a declared or decoded size exceeds its cap.
	ErrTooLarge = errors.New("tilemask: size exceeds the cap")
	// ErrUnsupported means a well-formed input uses an encoding or compression this package does not read.
	ErrUnsupported = errors.New("tilemask: unsupported encoding or compression")
	// ErrMalformed means the input is not the format it claims, or contradicts its own header.
	ErrMalformed = errors.New("tilemask: malformed input")
)

Functions

func MapSize

func MapSize(r io.Reader, f Format) (w, h int, err error)

MapSize returns the map's width and height in tiles from its header alone. TMX stops reading at the root element; TMJ reads the top-level object. A missing or non-positive size is ErrMalformed.

Types

type Format

type Format uint8

Format is a Tiled map file format.

const (
	TMX Format = iota + 1 // XML
	TMJ                   // JSON
)

The formats DecodeMap and MapSize read.

type Layer

type Layer struct {
	// Name is verbatim: no case folding or trimming.
	Name string
	// Tiles are the occupied tiles (GID not zero, flip bits included), in the map's coordinates: an infinite map's
	// chunk offsets are applied.
	Tiles []image.Point
}

Layer is one tile layer.

type Map

type Map struct {
	// Width and Height are the header's size in tiles, grown to cover every occupied tile.
	Width, Height int
	// Layers are the top-level tile layers in document order. Group layers are not entered.
	Layers []Layer
}

Map is the occupied tiles of a Tiled map.

func DecodeMap

func DecodeMap(r io.Reader, f Format, opts Options) (Map, error)

DecodeMap reads r until the document ends and returns its occupied tiles.

The declared sizes of every layer and chunk together must be within opts.MaxTiles, checked before any data inflates (ErrTooLarge). A layer or chunk whose data holds more values than its declared width times height, or whose compressed data inflates past it, is ErrMalformed, so memory use does not depend on the size of r. An encoding or compression other than CSV, tile lists and base64 with none, zlib or gzip is ErrUnsupported, and fails the whole map.

type Mask

type Mask struct {
	Width, Height int
	// contains filtered or unexported fields
}

Mask is one bit per pixel, set when the pixel's alpha is not zero. It is ordinary garbage-collected memory and safe for concurrent reads.

func DecodePNG

func DecodePNG(r io.Reader, opts Options) (*Mask, error)

DecodePNG reads r to the end and returns the mask of pixels whose alpha is not zero.

The size the IHDR claims is checked against opts.MaxPixels before any pixel buffer exists. Eight-bit, non-interlaced images are read by a fast path that skips what a mask does not need: it checks no chunk CRC and no Adler-32 trailer, needs no PLTE for colour type 3, needs no IDAT for an 8-bit grey or RGB image without tRNS (every pixel is set), and ignores data after the rows it needs. Each of those errs toward more coverage, never less. Everything else, and an image the fast path cannot inflate, goes through image/png.

func ReadMask

func ReadMask(r io.Reader, opts Options) (*Mask, error)

ReadMask reads a mask WriteTo wrote. The size in the header is held to opts.MaxPixels before the bits are allocated, so a crafted header cannot claim more than a decode would have allowed, and every mask written under that cap reads back. The gzip trailer is verified, so a torn write is refused as ErrMalformed.

func (*Mask) At

func (m *Mask) At(x, y int) bool

At reports whether the pixel is set. It is false outside the bounds.

func (*Mask) Cells

func (m *Mask) Cells(src image.Rectangle, dst image.Point, cell int) iter.Seq[image.Point]

Cells yields each cell that holds a set pixel of src, row-major and each once. A cell is cell by cell pixels, src is clipped to the mask, and src's top-left lands at dst. Cell coordinates use floor division, so pixels left of or above the origin fall in cell -1, not cell 0. It yields nothing when cell <= 0, when src is empty after clipping, or when a coordinate of src or dst is beyond a quarter of the int range.

Cells holds one bool per cell of the covered range while it runs, so cell = 1 costs a byte per pixel.

func (*Mask) WriteTo

func (m *Mask) WriteTo(w io.Writer) (int64, error)

WriteTo writes the mask as gzip (best speed) of "W H\n" followed by the packed bits, and returns the compressed bytes written. A mask with a non-positive size is refused, since ReadMask would refuse it.

type Options

type Options struct {
	// MaxPixels caps an image's width times height, checked before any pixel buffer is allocated.
	MaxPixels int64
	// MaxTiles caps a map's declared tiles, summed over every layer and chunk before any data inflates. No
	// layer or chunk may then hold more values than it declared, so it also caps the tiles decoded.
	MaxTiles int64
}

Options holds the caps. A zero or negative field means its default.

Jump to

Keyboard shortcuts

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