Skip to main content

src/sparklinekit/internal/png.gleam

//// Pure-Gleam PNG encoder. Emits 8-bit RGBA truecolor (PNG colour
//// type 6) with an uncompressed-DEFLATE payload — the same approach
//// `qrkit/render/png` uses, generalised from 1-bit indexed colour to
//// 32-bit-per-pixel RGBA so we can carry the configurable colour
//// schemes the sparkline renderers expose.
////
//// This module is intentionally private. Anything here may change
//// between minor versions.

import gleam/bit_array
import gleam/int
import gleam/list
import sparklinekit/internal/color.{type Rgba, Rgba}

const png_signature = [137, 80, 78, 71, 13, 10, 26, 10]

const crc32_polynomial = 0xEDB88320

const adler32_modulus = 65_521

/// Encode a 2-D pixel grid (row-major, each row of length `width`,
/// `height` rows) into PNG byte data.
///
/// Pixels are written verbatim; alpha is preserved. The caller owns
/// the background — if you want a transparent canvas you fill the
/// grid with `color.transparent`; if you want an opaque background
/// you blend it before calling here.
pub fn encode(
  pixels: List(List(Rgba)),
  width width: Int,
  height height: Int,
) -> BitArray {
  let actual_width = case width < 1 {
    True -> 1
    False -> width
  }
  let actual_height = case height < 1 {
    True -> 1
    False -> height
  }
  let scanlines = build_scanlines(pixels)
  let ihdr =
    list.append(
      u32_bytes(actual_width),
      list.append(u32_bytes(actual_height), [8, 6, 0, 0, 0]),
    )
  let idat = zlib_store(scanlines)

  list.append(
    png_signature,
    list.append(
      chunk(type_bytes: [73, 72, 68, 82], data_bytes: ihdr),
      list.append(
        chunk(type_bytes: [73, 68, 65, 84], data_bytes: idat),
        chunk(type_bytes: [73, 69, 78, 68], data_bytes: []),
      ),
    ),
  )
  |> byte_list_to_bit_array
}

fn build_scanlines(rows: List(List(Rgba))) -> List(Int) {
  do_build_scanlines(rows, [])
}

fn do_build_scanlines(rows: List(List(Rgba)), acc: List(Int)) -> List(Int) {
  case rows {
    [] -> list.reverse(acc)
    [row, ..rest] -> do_build_scanlines(rest, prepend_row(row, [0, ..acc]))
  }
}

fn prepend_row(row: List(Rgba), acc: List(Int)) -> List(Int) {
  case row {
    [] -> acc
    [Rgba(r, g, b, a), ..rest] -> prepend_row(rest, [a, b, g, r, ..acc])
  }
}

fn zlib_store(data: List(Int)) -> List(Int) {
  let checksum = adler32(data)
  list.append([120, 1], deflate_store_blocks(data))
  |> list.append(u32_bytes(checksum))
}

fn deflate_store_blocks(data: List(Int)) -> List(Int) {
  do_deflate_store_blocks(data, [])
}

fn do_deflate_store_blocks(data: List(Int), acc: List(Int)) -> List(Int) {
  case data {
    [] ->
      case acc {
        [] -> [1, 0, 0, 255, 255]
        _ -> list.reverse(acc)
      }
    _ -> {
      let #(chunk_bytes, rest) = take_bytes(data, 65_535, [])
      let final_flag = case rest {
        [] -> 1
        _ -> 0
      }
      let length = list.length(chunk_bytes)
      let complement = 65_535 - length
      let header = [
        final_flag,
        low_byte(length),
        high_byte(length),
        low_byte(complement),
        high_byte(complement),
      ]
      do_deflate_store_blocks(
        rest,
        prepend_reversed(list.append(header, chunk_bytes), acc),
      )
    }
  }
}

fn take_bytes(
  values: List(Int),
  count: Int,
  acc: List(Int),
) -> #(List(Int), List(Int)) {
  case values, count {
    rest, 0 -> #(list.reverse(acc), rest)
    [value, ..rest], _ -> take_bytes(rest, count - 1, [value, ..acc])
    [], _ -> #(list.reverse(acc), [])
  }
}

fn chunk(
  type_bytes type_bytes: List(Int),
  data_bytes data_bytes: List(Int),
) -> List(Int) {
  let crc = crc32(list.append(type_bytes, data_bytes))
  list.append(
    u32_bytes(list.length(data_bytes)),
    list.append(type_bytes, list.append(data_bytes, u32_bytes(crc))),
  )
}

fn crc32(bytes: List(Int)) -> Int {
  do_crc32(bytes, 0xFFFF_FFFF)
  |> int.bitwise_exclusive_or(0xFFFF_FFFF)
}

fn do_crc32(bytes: List(Int), crc: Int) -> Int {
  case bytes {
    [] -> crc
    [byte, ..rest] ->
      do_crc32(rest, crc32_byte(int.bitwise_exclusive_or(crc, byte), 8))
  }
}

fn crc32_byte(crc: Int, remaining: Int) -> Int {
  case remaining <= 0 {
    True -> crc
    False -> {
      let next = case int.bitwise_and(crc, 1) == 1 {
        True ->
          int.bitwise_exclusive_or(
            int.bitwise_shift_right(crc, 1),
            crc32_polynomial,
          )
        False -> int.bitwise_shift_right(crc, 1)
      }
      crc32_byte(next, remaining - 1)
    }
  }
}

fn adler32(bytes: List(Int)) -> Int {
  do_adler32(bytes, 1, 0)
}

fn do_adler32(bytes: List(Int), s1: Int, s2: Int) -> Int {
  case bytes {
    [] -> s2 * 65_536 + s1
    [byte, ..rest] -> {
      let next_s1 = { s1 + byte } % adler32_modulus
      let next_s2 = { s2 + next_s1 } % adler32_modulus
      do_adler32(rest, next_s1, next_s2)
    }
  }
}

fn byte_list_to_bit_array(bytes: List(Int)) -> BitArray {
  bytes
  |> list.map(fn(byte) { <<byte>> })
  |> bit_array.concat
}

fn prepend_reversed(values: List(a), acc: List(a)) -> List(a) {
  case values {
    [] -> acc
    [value, ..rest] -> prepend_reversed(rest, [value, ..acc])
  }
}

fn u32_bytes(value: Int) -> List(Int) {
  [
    int.bitwise_and(int.bitwise_shift_right(value, 24), 0xFF),
    int.bitwise_and(int.bitwise_shift_right(value, 16), 0xFF),
    int.bitwise_and(int.bitwise_shift_right(value, 8), 0xFF),
    int.bitwise_and(value, 0xFF),
  ]
}

fn low_byte(value: Int) -> Int {
  int.bitwise_and(value, 0xFF)
}

fn high_byte(value: Int) -> Int {
  int.bitwise_and(int.bitwise_shift_right(value, 8), 0xFF)
}