pngleam/src/pngleam.gleam

552 lines
14 KiB
Gleam

import gleam/bit_array
import gleam/bool
import gleam/float
import gleam/int
import gleam/list
import gleam/option
import gleam/result
import gzlib
pub const no_compression = gzlib.no_compression
pub const min_compression = gzlib.min_compression
pub const max_compression = gzlib.max_compression
pub const default_compression = gzlib.default_compression
pub const compression_level = gzlib.compression_level
pub type CompressionLevel =
gzlib.CompressionLevel
pub type ColorType {
Greyscale
Color
Indexed
GreyscaleWithAlpha
ColorWithAlpha
}
fn color_type_to_int(color_type: ColorType) -> Int {
case color_type {
Greyscale -> 0b000
Color -> 0b010
Indexed -> 0b011
GreyscaleWithAlpha -> 0b100
ColorWithAlpha -> 0b110
}
}
fn int_to_color_type(color_type: Int) -> Result(ColorType, Nil) {
case color_type {
0b000 -> Ok(Greyscale)
0b010 -> Ok(Color)
0b011 -> Ok(Indexed)
0b100 -> Ok(GreyscaleWithAlpha)
0b110 -> Ok(ColorWithAlpha)
_ -> Error(Nil)
}
}
pub opaque type ColorInfo {
ColorInfo(color_type: ColorType, bit_depth: Int)
}
pub const greyscale_8bit = ColorInfo(Greyscale, 8)
pub const rgb_8bit = ColorInfo(Color, 8)
pub const rgb_16bit = ColorInfo(Color, 16)
pub const rgba_8bit = ColorInfo(ColorWithAlpha, 8)
pub const rgba_16bit = ColorInfo(ColorWithAlpha, 16)
pub fn color_info(
color_type color_type: ColorType,
bit_depth bit_depth: Int,
) -> Result(ColorInfo, Nil) {
let color_info = Ok(ColorInfo(color_type, bit_depth))
case color_type, bit_depth {
Greyscale, 1 | Greyscale, 2 | Greyscale, 4 | Greyscale, 8 | Greyscale, 16 ->
color_info
Color, 8 | Color, 16 -> color_info
Indexed, 1 | Indexed, 2 | Indexed, 4 | Indexed, 8 -> color_info
GreyscaleWithAlpha, 8 | GreyscaleWithAlpha, 16 -> color_info
ColorWithAlpha, 8 | ColorWithAlpha, 16 -> color_info
_, _ -> Error(Nil)
}
}
pub fn color_info_bits(color_info: ColorInfo) -> Int {
case color_info.color_type {
Greyscale -> color_info.bit_depth
Color -> 3 * color_info.bit_depth
Indexed -> color_info.bit_depth
GreyscaleWithAlpha -> 2 * color_info.bit_depth
ColorWithAlpha -> 4 * color_info.bit_depth
}
}
fn partition_bit_array(
data: BitArray,
at bytes: Int,
) -> Result(#(BitArray, BitArray), Nil) {
use left <- result.try(bit_array.slice(data, 0, bytes))
use right <- result.try(bit_array.slice(
data,
bytes,
bit_array.byte_size(data) - bytes,
))
Ok(#(left, right))
}
const chunk_size = 8192
fn do_chunk_bit_array(data: BitArray, chunks: List(BitArray)) -> List(BitArray) {
case partition_bit_array(data, chunk_size) {
Ok(#(chunk, rest)) -> do_chunk_bit_array(rest, [chunk, ..chunks])
_ -> [data, ..chunks]
}
}
fn chunk_bit_array(data: BitArray) -> List(BitArray) {
do_chunk_bit_array(data, []) |> list.reverse
}
fn create_chunk(tag: String, data: BitArray) -> BitArray {
let data_size = bit_array.byte_size(data)
let tag_bits = bit_array.from_string(tag)
let checksum = gzlib.continue_crc32(gzlib.crc32(tag_bits), data)
<<data_size:size(32), tag_bits:bits, data:bits, checksum:size(32)>>
}
const signature = <<137, 80, 78, 71, 13, 10, 26, 10>>
pub type BinaryRowData =
List(BitArray)
pub fn from_packed(
row_data row_data: BinaryRowData,
width width: Int,
height height: Int,
color_info color_info: ColorInfo,
compression_level compression_level: gzlib.CompressionLevel,
) -> BitArray {
let ihdr =
create_chunk("IHDR", <<
width:size(32),
height:size(32),
color_info.bit_depth:size(8),
color_type_to_int(color_info.color_type):size(8),
0:size(8),
0:size(8),
0:size(8),
>>)
let no_filter_int = filter_type_to_int(None)
let idats =
row_data
|> list.map(fn(d) { <<no_filter_int:size(8), d:bits>> })
|> bit_array.concat
|> gzlib.compress_with_level(compression_level)
|> chunk_bit_array
|> list.map(create_chunk("IDAT", _))
|> bit_array.concat
let iend = create_chunk("IEND", <<>>)
<<signature:bits, ihdr:bits, idats:bits, iend:bits>>
}
pub type ParseError {
InvalidSignature
InvalidChunkTag
ChecksumMismatch
InvalidChunkOrder
MissingHeaderChunk
InvalidChunkData
InvalidColorType
InvalidBitDepth
InvalidCompressionType
InvalidFilterMethod
InvalidInterlaceMethod
UnsupportedInterlaceMethod
InvalidRowFilterType
InvalidRowData
}
fn parse_chunk(
data: BitArray,
) -> Result(#(String, BitArray, BitArray), ParseError) {
case data {
<<data_size:size(32), tag_bits:bytes-size(4), rest:bytes>> -> {
use tag <- result.try(
bit_array.to_string(tag_bits)
|> result.replace_error(InvalidChunkTag),
)
case partition_bit_array(rest, data_size) {
Ok(#(data, <<checksum:size(32), rest:bytes>>)) -> {
let computed_checksum =
gzlib.continue_crc32(gzlib.crc32(tag_bits), data)
use <- bool.guard(
computed_checksum != checksum,
Error(ChecksumMismatch),
)
Ok(#(tag, data, rest))
}
_ -> Error(InvalidChunkData)
}
}
_ -> Error(InvalidChunkData)
}
}
fn parse_signature(data: BitArray) -> Result(BitArray, ParseError) {
case data {
<<137, 80, 78, 71, 13, 10, 26, 10, rest:bytes>> -> Ok(rest)
_ -> Error(InvalidSignature)
}
}
pub type PngMetadata {
PngMetadata(width: Int, height: Int, color_info: ColorInfo)
}
fn parse_header(header_data: BitArray) -> Result(PngMetadata, ParseError) {
case header_data {
<<
width:size(32),
height:size(32),
bit_depth:size(8),
color_type:size(8),
compression_method:size(8),
filter_method:size(8),
interlace_method:size(8),
>> -> {
use col_type <- result.try(
int_to_color_type(color_type)
|> result.replace_error(InvalidColorType),
)
use col_info <- result.try(
color_info(col_type, bit_depth)
|> result.replace_error(InvalidBitDepth),
)
use <- bool.guard(compression_method != 0, Error(InvalidCompressionType))
use <- bool.guard(filter_method != 0, Error(InvalidFilterMethod))
use <- bool.guard(
interlace_method != 0 && interlace_method != 1,
Error(InvalidInterlaceMethod),
)
use <- bool.guard(
interlace_method == 1,
Error(UnsupportedInterlaceMethod),
)
Ok(PngMetadata(width, height, col_info))
}
_ -> Error(InvalidChunkData)
}
}
pub fn parse_metadata(data: BitArray) -> Result(PngMetadata, ParseError) {
use chunk_data <- result.try(parse_signature(data))
use #(tag, chunk_data, _) <- result.try(parse_chunk(chunk_data))
use <- bool.guard(tag != "IHDR", Error(MissingHeaderChunk))
parse_header(chunk_data)
}
pub type RawPalette =
BitArray
type PngDataState {
PngDataState(palette: option.Option(RawPalette), image_parts: BinaryRowData)
}
fn do_parse_image_data(
data: BitArray,
state: PngDataState,
) -> Result(PngDataState, ParseError) {
use #(tag, chunk_data, rest) <- result.try(parse_chunk(data))
case tag, state.image_parts {
"PLTE", [] ->
do_parse_image_data(
rest,
PngDataState(..state, palette: option.Some(chunk_data)),
)
"PLTE", _ -> Error(InvalidChunkOrder)
"IDAT", parts ->
do_parse_image_data(
rest,
PngDataState(..state, image_parts: [chunk_data, ..parts]),
)
"IEND", _ -> Ok(state)
_, [] -> do_parse_image_data(rest, state)
_, _ -> Ok(state)
}
}
fn parse_image_data(data: BitArray) -> Result(PngDataState, ParseError) {
do_parse_image_data(data, PngDataState(option.None, []))
}
pub type FilterType {
None
Sub
Up
Average
Paeth
}
pub fn int_to_filter_type(filter_type: Int) -> Result(FilterType, Nil) {
case filter_type {
0 -> Ok(None)
1 -> Ok(Sub)
2 -> Ok(Up)
3 -> Ok(Average)
4 -> Ok(Paeth)
_ -> Error(Nil)
}
}
pub fn filter_type_to_int(filter_type: FilterType) -> Int {
case filter_type {
None -> 0
Sub -> 1
Up -> 2
Average -> 3
Paeth -> 4
}
}
@external(erlang, "pngleam_erl", "subUnfilter")
@external(javascript, "./pngleam_js.mjs", "subUnfilter")
fn sub_unfilter(row: BitArray, bytes_per_pixel: Int) -> BitArray
@external(erlang, "pngleam_erl", "upUnfilter")
@external(javascript, "./pngleam_js.mjs", "upUnfilter")
fn up_unfilter(row: BitArray, row_above: BitArray) -> BitArray
@external(erlang, "pngleam_erl", "avgUnfilter")
@external(javascript, "./pngleam_js.mjs", "avgUnfilter")
fn avg_unfilter(
row: BitArray,
row_above: BitArray,
bytes_per_pixel: Int,
) -> BitArray
@external(erlang, "pngleam_erl", "paethUnfilter")
@external(javascript, "./pngleam_js.mjs", "paethUnfilter")
fn paeth_unfilter(
row: BitArray,
row_above: BitArray,
bytes_per_pixel: Int,
) -> BitArray
fn do_parse_image_rows(
data: BitArray,
bytes_per_row: Int,
bytes_per_pixel: Int,
rows: BinaryRowData,
) -> Result(BinaryRowData, ParseError) {
let bits_per_row = bytes_per_row * 8
case data {
<<>> -> Ok(rows)
<<filter_type:size(8), rest:bytes>> -> {
use filter_type <- result.try(
int_to_filter_type(filter_type)
|> result.replace_error(InvalidRowFilterType),
)
use #(row, rest) <- result.try(
partition_bit_array(rest, bytes_per_row)
|> result.replace_error(InvalidRowData),
)
let row = case filter_type {
None -> row
Sub -> sub_unfilter(row, bytes_per_pixel)
Up ->
up_unfilter(
row,
result.unwrap(list.first(rows), <<0:size(bits_per_row)>>),
)
Average ->
avg_unfilter(
row,
result.unwrap(list.first(rows), <<0:size(bits_per_row)>>),
bytes_per_pixel,
)
Paeth ->
paeth_unfilter(
row,
result.unwrap(list.first(rows), <<0:size(bits_per_row)>>),
bytes_per_pixel,
)
}
do_parse_image_rows(rest, bytes_per_row, bytes_per_pixel, [row, ..rows])
}
_ -> Error(InvalidRowData)
}
}
fn parse_image_rows(
data: BitArray,
bytes_per_row: Int,
bytes_per_pixel: Int,
) -> Result(BinaryRowData, ParseError) {
do_parse_image_rows(data, bytes_per_row, bytes_per_pixel, [])
|> result.map(list.reverse)
}
fn bits_to_bytes(bits: Int) -> Int {
bits
|> int.to_float
|> fn(x) { x /. 8.0 }
|> float.ceiling
|> float.round
}
pub type PngData(p, i) {
PngData(metadata: PngMetadata, palette: option.Option(p), image_data: i)
}
pub type PngBitArrayData =
PngData(RawPalette, BinaryRowData)
pub fn parse_to_bit_arrays(
data: BitArray,
) -> Result(PngBitArrayData, ParseError) {
use rest <- result.try(parse_signature(data))
use #(tag, chunk_data, rest) <- result.try(parse_chunk(rest))
use <- bool.guard(tag != "IHDR", Error(MissingHeaderChunk))
use metadata <- result.try(parse_header(chunk_data))
use PngDataState(palette, image_parts) <- result.try(parse_image_data(rest))
let image_data =
image_parts
|> list.reverse
|> bit_array.concat
|> gzlib.uncompress
let bits_per_pixel = color_info_bits(metadata.color_info)
let bytes_per_row = bits_to_bytes(metadata.width * bits_per_pixel)
let bytes_per_pixel = bits_to_bytes(bits_per_pixel)
use idat_rows <- result.try(parse_image_rows(
image_data,
bytes_per_row,
bytes_per_pixel,
))
Ok(PngData(metadata, palette, idat_rows))
}
@external(erlang, "pngleam_erl", "bitArrayToInts")
@external(javascript, "./pngleam_js.mjs", "bitArrayToInts")
fn bit_array_to_ints(data: BitArray, int_size: Int) -> List(Int)
fn do_chunk2(
values: List(a),
chunks: List(#(a, a)),
) -> Result(List(#(a, a)), Nil) {
case values {
[] -> Ok(chunks)
[a, b, ..rest] -> do_chunk2(rest, [#(a, b), ..chunks])
_ -> Error(Nil)
}
}
fn chunk2(values: List(a)) -> Result(List(#(a, a)), Nil) {
do_chunk2(values, []) |> result.map(list.reverse)
}
fn do_chunk3(
values: List(a),
chunks: List(#(a, a, a)),
) -> Result(List(#(a, a, a)), Nil) {
case values {
[] -> Ok(chunks)
[a, b, c, ..rest] -> do_chunk3(rest, [#(a, b, c), ..chunks])
_ -> Error(Nil)
}
}
fn chunk3(values: List(a)) -> Result(List(#(a, a, a)), Nil) {
do_chunk3(values, []) |> result.map(list.reverse)
}
fn do_chunk4(
values: List(a),
chunks: List(#(a, a, a, a)),
) -> Result(List(#(a, a, a, a)), Nil) {
case values {
[] -> Ok(chunks)
[a, b, c, d, ..rest] -> do_chunk4(rest, [#(a, b, c, d), ..chunks])
_ -> Error(Nil)
}
}
fn chunk4(values: List(a)) -> Result(List(#(a, a, a, a)), Nil) {
do_chunk4(values, []) |> result.map(list.reverse)
}
pub type GreyscaleValue =
Int
pub type ColorValue =
#(Int, Int, Int)
pub type PalleteIndex =
Int
pub type GreyscaleWithAlphaValue =
#(Int, Int)
pub type ColorWithAlphaValue =
#(Int, Int, Int, Int)
pub type Grid(a) =
List(List(a))
pub type PixelData {
GreyscaleData(Grid(GreyscaleValue))
ColorData(Grid(ColorValue))
IndexedData(Grid(PalleteIndex))
GreyscaleWithAlphaData(Grid(GreyscaleWithAlphaValue))
ColorWithAlphaData(Grid(ColorWithAlphaValue))
}
pub type PaletteColor =
#(Int, Int, Int)
pub type PaletteColors =
List(PaletteColor)
pub type PngPixelData =
PngData(PaletteColors, PixelData)
pub fn parse_to_pixel_data(data: BitArray) -> Result(PngPixelData, ParseError) {
use PngData(metadata, palette, row_data) <- result.try(parse_to_bit_arrays(
data,
))
use palette <- result.try(case palette {
option.Some(p) ->
case bit_array_to_ints(p, 8) |> chunk3 {
Ok(p) -> Ok(option.Some(p))
Error(_) -> Error(InvalidChunkData)
}
option.None -> Ok(option.None)
})
let bit_depth = metadata.color_info.bit_depth
let num_values =
metadata.width * color_info_bits(metadata.color_info) / bit_depth
let values =
list.map(row_data, fn(row) {
bit_array_to_ints(row, bit_depth) |> list.take(num_values)
})
use image_data <- result.try(
case metadata.color_info.color_type {
Greyscale -> Ok(GreyscaleData(values))
Color -> list.try_map(values, chunk3) |> result.map(ColorData)
Indexed -> Ok(IndexedData(values))
GreyscaleWithAlpha ->
list.try_map(values, chunk2) |> result.map(GreyscaleWithAlphaData)
ColorWithAlpha ->
list.try_map(values, chunk4) |> result.map(ColorWithAlphaData)
}
|> result.replace_error(InvalidRowData),
)
Ok(PngData(metadata, palette, image_data))
}