iyi

Standard library, 0.18.0

std/png

PNG images (ISO/IEC 15948:2004, and the W3C's Portable Network Graphics Specification, Third Edition, 2025, which is the same format with APNG and cICP written down), read and written in iyi over the zlib of std/compress and the CRC-32 of std/digest.

import std/png to use it. Source: src/std/png.iyi at v0.18.0.

Png.decode(bytes) reads a whole file held in a String and answers a PngFile: its image, and what else the file said. Every color type is read at every bit depth the format allows for it - grayscale at 1, 2, 4, 8 and 16 bits, RGB at 8 and 16, palette at 1, 2, 4 and 8, gray with alpha and RGBA at 8 and 16 - interlaced (Adam7) or not, through all five filters. The Image has 8-bit samples, or 16-bit ones where the file had 16: a grayscale sample of 1, 2 or 4 bits is scaled to 8 by replicating its bits (2 bits 10 is 10101010, 170), which is the scaling the specification recommends (section 12.5, "Sample depth scaling") and what libpng and Pillow do; a palette image is expanded to RGB, or to RGBA when a tRNS chunk gives its entries alpha. A tRNS chunk on a grayscale or RGB image names one color as transparent; the Image keeps it as transparent (scaled with the samples) and to_rgba8 gives that color alpha 0. to_rgba8 reduces a 16-bit sample to its high byte, as libpng's png_set_strip_16 and Pillow do, so the two agree with it byte for byte.

A chunk's CRC-32 is checked before the chunk is read. The critical chunks are held to their order (IHDR first, PLTE before IDAT, IDAT chunks one after another, IEND), and a critical chunk this module does not know is refused, as the specification requires (section 5.4); an ancillary one is skipped and its name kept in PngFile#skipped. gAMA, sRGB, iCCP (its profile inflated), tEXt, zTXt and iTXt are read and kept; a tEXt or zTXt's Latin-1 is answered as UTF-8. A tRNS that comes after IDAT, or whose length does not fit its color type, is refused rather than guessed at.

The image data's size is what IHDR says it is, so the pixel count is checked against max_pixels (Pillow's own warning threshold, 89,478,485, unless the caller says otherwise) before anything is allocated for it, and the zlib stream is inflated into exactly that many bytes: a stream that inflates further is refused after at most 64 KiB more, so a decompression bomb costs what its header admitted to and no more. A compressed iCCP, zTXt or iTXt inflates to at most 8 MiB.

What is wrong with a file is a PngError, a value: a signature that is not PNG's, a chunk cut short or failing its CRC-32, a chunk name that is not four letters, the chunks out of order, an IHDR whose depth and color type are not a pair the format has, a zlib stream that is corrupt or ends before the image does, a row whose filter type is past 4, a pixel whose palette index is past the palette. Asking for an image this module cannot build - a sample count that does not match the size, a palette index past the palette - is the caller's mistake, a panic.

Png.encode(image) writes an Image - gray, gray with alpha, RGB or RGBA, at 8 or 16 bits, with its transparent color as tRNS - and Png.encode_indexed writes a palette image at the smallest bit depth its palette fits, with tRNS where an entry is not opaque. Each row of a non-palette image is filtered by the heuristic the specification recommends (section 12.8): every filter is tried and the one whose output bytes, read as signed, have the least sum of absolute values is kept, the first of equals; a palette image is not filtered, as section 12.8 advises for one. interlace: true writes Adam7, and texts are written as tEXt, or as iTXt where the text is not Latin-1. The zlib stream is Deflater's at its default level, whose blocks are dynamic, fixed or stored by what each costs, split into 64 KiB IDAT chunks.

Left out: APNG's animation chunks (skipped as ancillary: the default image is what is read), colour management (gAMA, cHRM, sRGB, iCCP and cICP are answered, not applied), sBIT, bKGD, pHYs, tIME and the rest of the ancillary chunks (skipped, named), and writing a 1-, 2- or 4-bit grayscale file.

Imageclass

Pixels, row after row from the top, each pixel's samples in layout order; a sample is one byte at depth 8, two (big-endian, as PNG stores them) at depth 16. transparent, for a Gray or RGB image, is the one color that is to be read as transparent (a gray value, or red, green and blue), in the image's own depth.

std/pngiyi doc src/std/png.iyi
pub class Image
  def channels : Int32
  def self.channels_of(layout : Layout) : Int32
  def depth : Int32
  def height : Int32
  def initialize(width : Int32, height : Int32, layout : Layout, depth : Int32, samples : String, transparent : Array(Int32) | ::Nil = nil)
  def layout : Layout
  def self.layout_name(layout : Layout) : String
  # Sample *c* of the pixel at column *x*, row *y*: 0 to 255, or to 65535
  # at depth 16. Outside the image, a panic.
  def sample(x : Int32, y : Int32, c : Int32) : Int32
  def samples : String
  # Four bytes a pixel, red, green, blue and alpha. A gray sample is all
  # three colors; a pixel of the transparent color has alpha 0, any other
  # without an alpha sample 255; a 16-bit sample is its high byte.
  def to_rgba8 : String
  def transparent : Array(Int32) | ::Nil
  def width : Int32
end

Layoutenum

Which samples a pixel has, in the order they are stored.

std/pngiyi doc src/std/png.iyi
pub enum Layout : Int32
  Gray = 0
  GrayAlpha = 1
  RGB = 2
  RGBA = 3
end

Pngstruct

std/pngiyi doc src/std/png.iyi
pub struct Png
  def self.assemble(width : Int32, height : Int32, depth : Int32, ctype : Int32, interlace : Bool, plte : String, trns : String, texts : Array(::Tuple(String, String)), filtered : String) : String
  def self.chunk(io : String::Builder, name : String, data : String) : Nil
  # The file in *data*, decoded; or what is wrong with it. A file whose
  # IHDR claims more than *max_pixels* pixels is refused before anything
  # is allocated for them.
  def self.decode(data : String, max_pixels : Int64 = MAX_PIXELS) : PngFile | PngError
  # *image* as a PNG file. *texts* are written as tEXt chunks, keyword
  # and text, or as iTXt where the text is not Latin-1.
  def self.encode(image : Image, interlace : Bool = false, texts : Array(::Tuple(String, String)) = [] of ::Tuple(String, String)) : String
  # A palette image: *indices* one byte a pixel, row after row, each an
  # entry of *palette*, whose entries are red, green, blue and alpha. The
  # bit depth is the smallest of 1, 2, 4 and 8 the palette fits; tRNS is
  # written up to the last entry that is not opaque.
  def self.encode_indexed(width : Int32, height : Int32, indices : String, palette : Array(::Tuple(Int32, Int32, Int32, Int32)), interlace : Bool = false, texts : Array(::Tuple(String, String)) = [] of ::Tuple(String, String)) : String
  # A zlib stream in a metadata chunk, inflated to at most MAX_METADATA.
  def self.inflate_metadata(stream : String, name : String) : String | PngError
  def initialize
  # The image data inflated into exactly the bytes IHDR promised, the
  # filters undone, and the pixels laid out.
  def self.pixels(d : PngDecode, data : String, idats : Array(::Tuple(Int32, Int32))) : Image | PngError
  # tEXt, zTXt or iTXt (sections 11.3.3.2-4).
  def self.read_text(name : String, p : Pointer(UInt8), body : Int32, len : Int32) : PngText | PngError
  # A keyword is 1 to 79 Latin-1 printable bytes, no space at either end
  # and no two together (section 11.3.3.2): anything else is a panic.
  def self.text_chunk(keyword : String, text : String) : ::Tuple(String, String)
end

PngErrorstruct

What is wrong with a PNG file: message names the chunk, and its offset in the file where that helps.

std/pngiyi doc src/std/png.iyi
pub struct PngError
  def initialize(message : String)
  def to_s : String
end

PngFileclass

A decoded file: the image and what the file said about it.

std/pngiyi doc src/std/png.iyi
pub class PngFile
  def bit_depth : Int32
  # IHDR's color type (0, 2, 3, 4 or 6) and bit depth, as written.
  def color_type : Int32
  # gAMA's value, the gamma times 100,000; nil without one.
  def gamma : Int32 | ::Nil
  # iCCP's profile name and the profile inflated; nil without one.
  def icc_name : String | ::Nil
  def icc_profile : String | ::Nil
  def image : Image
  def initialize(image : Image, color_type : Int32, bit_depth : Int32, interlaced : Bool, palette : String, transparency : String, gamma : Int32 | ::Nil, srgb_intent : Int32 | ::Nil, icc_name : String | ::Nil, icc_profile : String | ::Nil, texts : Array(PngText), skipped : Array(String))
  def interlaced : Bool
  # PLTE's entries, red, green and blue a byte each; "" without one.
  def palette : String
  # The ancillary chunks skipped, by name, in file order.
  def skipped : Array(String)
  # sRGB's rendering intent, 0 to 3; nil without one.
  def srgb_intent : Int32 | ::Nil
  def texts : Array(PngText)
  # tRNS as written: an alpha byte per palette entry, or the transparent
  # gray or RGB sample(s) as two bytes each; "" without one.
  def transparency : String
end

PngTextclass

A tEXt, zTXt or iTXt chunk: chunk says which. language and translated_keyword are iTXt's, "" for the other two.

std/pngiyi doc src/std/png.iyi
pub class PngText
  def chunk : String
  def initialize(chunk : String, keyword : String, text : String, language : String = "", translated_keyword : String = "")
  def keyword : String
  def language : String
  def text : String
  def translated_keyword : String
end

Error for PngErrorimpl

std/pngiyi doc src/std/png.iyi
impl Error for PngError
  def message : String
end