Add initial functional (but slow) png parsing
This commit is contained in:
parent
1d98870ec1
commit
bdfffee3d7
|
@ -2,3 +2,4 @@
|
|||
*.ez
|
||||
/build
|
||||
erl_crash.dump
|
||||
*.png
|
|
@ -10,7 +10,8 @@ links = [
|
|||
|
||||
[dependencies]
|
||||
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
|
||||
gzlib = ">= 1.0.0 and < 2.0.0"
|
||||
gzlib = ">= 1.0.1 and < 2.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||
simplifile = ">= 2.0.0 and < 3.0.0"
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
|
||||
{ name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
|
||||
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
|
||||
{ name = "gzlib", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gzlib", source = "hex", outer_checksum = "EC6A3FAF20B8A707B5A550E1B622785685759991C9D13CFC4AAE8FE34FDDF3B8" },
|
||||
{ name = "gzlib", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gzlib", source = "hex", outer_checksum = "5E71EF6C973CB61CDF25D1C5CDBD129C481CE432D6FD089FBB4E30B95CCCE935" },
|
||||
{ name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
|
||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
gzlib = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
gzlib = { version = ">= 1.0.1 and < 2.0.0"}
|
||||
simplifile = { version = ">= 2.0.0 and < 3.0.0" }
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
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
|
||||
|
@ -33,6 +38,17 @@ fn color_type_to_int(color_type: ColorType) -> Int {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -63,36 +79,63 @@ pub fn color_info(
|
|||
}
|
||||
}
|
||||
|
||||
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 do_chunk_bit_array(data: BitArray, chunks: List(BitArray)) -> List(BitArray) {
|
||||
case data {
|
||||
<<chunk:bytes-size(8192), rest:bytes>> ->
|
||||
do_chunk_bit_array(rest, [chunk, ..chunks])
|
||||
chunk -> [chunk, ..chunks]
|
||||
}
|
||||
}
|
||||
|
||||
fn get_chunk(tag: String, data: BitArray) -> BitArray {
|
||||
fn create_chunk(tag: String, data: BitArray) -> BitArray {
|
||||
let data_size = bit_array.byte_size(data)
|
||||
let tag_bits = <<tag:utf8>>
|
||||
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, "PNG":utf8, "\r\n":utf8, 26, "\n":utf8>>
|
||||
const signature = <<137, 80, 78, 71, 13, 10, 26, 10>>
|
||||
|
||||
pub type BinaryRowData =
|
||||
List(BitArray)
|
||||
|
||||
pub fn from_packed(
|
||||
row_data row_data: List(BitArray),
|
||||
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 =
|
||||
get_chunk("IHDR", <<
|
||||
create_chunk("IHDR", <<
|
||||
width:size(32),
|
||||
height:size(32),
|
||||
color_info.bit_depth:size(8),
|
||||
|
@ -101,14 +144,409 @@ pub fn from_packed(
|
|||
0:size(8),
|
||||
0:size(8),
|
||||
>>)
|
||||
let no_filter_int = filter_type_to_int(None)
|
||||
let idats =
|
||||
row_data
|
||||
|> list.map(fn(d) { <<0, d:bits>> })
|
||||
|> 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(get_chunk("IDAT", _))
|
||||
|> list.map(create_chunk("IDAT", _))
|
||||
|> bit_array.concat
|
||||
let iend = get_chunk("IEND", <<>>)
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
-module(pngleam_erl).
|
||||
|
||||
-export([subUnfilter/2, upUnfilter/2, avgUnfilter/3, paethUnfilter/3, bitArrayToInts/1, bitArrayToInts/2]).
|
||||
|
||||
addBytewise(As, Bs) ->
|
||||
list_to_binary(lists:zipwith(fun(A, B) -> (A + B) rem 256 end, binary_to_list(As), binary_to_list(Bs), trim)).
|
||||
|
||||
avgBytewise(As, Bs) ->
|
||||
list_to_binary(lists:zipwith(fun(A, B) -> ((A + B) div 2) rem 256 end, binary_to_list(As), binary_to_list(Bs), trim)).
|
||||
|
||||
paethBytewise(As, Bs, Cs) ->
|
||||
list_to_binary(lists:zipwith3(fun(A, B, C) ->
|
||||
P = A + B - C, % initial estimate
|
||||
PA = abs(P - A), % distances to a, b, c
|
||||
PB = abs(P - B),
|
||||
PC = abs(P - C),
|
||||
% return nearest of a,b,c,
|
||||
% breaking ties in order a,b,c.
|
||||
case (PA =< PB) and (PA =< PC) of
|
||||
true -> A;
|
||||
false -> case (PB =< PC) of
|
||||
true -> B;
|
||||
false -> C
|
||||
end
|
||||
end
|
||||
end, binary_to_list(As), binary_to_list(Bs), binary_to_list(Cs), trim)).
|
||||
|
||||
doSubUnfilter(Row, BytesPerPixel, Acc, Prev) ->
|
||||
case Row of
|
||||
<<>> -> Acc;
|
||||
<<Curr:BytesPerPixel/binary, Rest/binary>> ->
|
||||
New = addBytewise(Curr, Prev),
|
||||
doSubUnfilter(Rest, BytesPerPixel, <<Acc/binary, New/binary>>, New)
|
||||
end.
|
||||
|
||||
subUnfilter(Row, BytesPerPixel) -> doSubUnfilter(Row, BytesPerPixel, <<>>, <<0:(BytesPerPixel * 8)>>).
|
||||
|
||||
upUnfilter(Row, Above) -> addBytewise(Row, Above).
|
||||
|
||||
doAvgUnfilter(Row, Above, BytesPerPixel, Acc, Prev) ->
|
||||
case Row of
|
||||
<<>> -> Acc;
|
||||
<<Curr:BytesPerPixel/binary, Rest/binary>> ->
|
||||
case Above of <<CurrAbove:BytesPerPixel/binary, RestAbove/binary>> ->
|
||||
Avg = avgBytewise(Prev, CurrAbove),
|
||||
New = addBytewise(Curr, Avg),
|
||||
doAvgUnfilter(Rest, RestAbove, BytesPerPixel, <<Acc/binary, New/binary>>, New)
|
||||
end
|
||||
end.
|
||||
|
||||
avgUnfilter(Row, Above, BytesPerPixel) -> doAvgUnfilter(Row, Above, BytesPerPixel, <<>>, <<0:(BytesPerPixel * 8)>>).
|
||||
|
||||
doPaethUnfilter(Row, Above, BytesPerPixel, Acc, Prev, PrevAbove) ->
|
||||
case Row of
|
||||
<<>> -> Acc;
|
||||
<<Curr:BytesPerPixel/binary, Rest/binary>> ->
|
||||
case Above of <<CurrAbove:BytesPerPixel/binary, RestAbove/binary>> ->
|
||||
Paeth = paethBytewise(Prev, CurrAbove, PrevAbove),
|
||||
New = addBytewise(Curr, Paeth),
|
||||
doPaethUnfilter(Rest, RestAbove, BytesPerPixel, <<Acc/binary, New/binary>>, New, CurrAbove)
|
||||
end
|
||||
end.
|
||||
|
||||
paethUnfilter(Row, Above, BytesPerPixel) -> doPaethUnfilter(Row, Above, BytesPerPixel, <<>>, <<0:(BytesPerPixel * 8)>>, <<0:(BytesPerPixel * 8)>>).
|
||||
|
||||
doBitArrayToInts(As, IntSize, Values) ->
|
||||
case As of
|
||||
<<V:IntSize, Rest/binary>> -> doBitArrayToInts(Rest, IntSize, [V | Values]);
|
||||
_ -> Values
|
||||
end.
|
||||
bitArrayToInts(As, IntSize) -> lists:reverse(doBitArrayToInts(As, IntSize, [])).
|
||||
bitArrayToInts(As) -> bitArrayToInts(As, 8).
|
|
@ -0,0 +1,82 @@
|
|||
import { BitArray, toList } from "./gleam.mjs"
|
||||
|
||||
export const subUnfilter = (row, bpp) => {
|
||||
const state = Buffer.alloc(bpp)
|
||||
return new BitArray(new Uint8Array(row.buffer.map((x, i) => {
|
||||
const j = i % bpp
|
||||
const y = x + state[j]
|
||||
state[j] = y
|
||||
return y
|
||||
})))
|
||||
}
|
||||
|
||||
export const upUnfilter = (row, above) => new BitArray(new Uint8Array(row.buffer.map((x, i) => x + above[i])))
|
||||
|
||||
const avg = (a, b) => Math.floor((a + b) / 2)
|
||||
|
||||
export const avgUnfilter = (row, above, bpp) => {
|
||||
const state = Buffer.alloc(bpp)
|
||||
return new BitArray(new Uint8Array(row.buffer.map((x, i) => {
|
||||
const j = i % bpp
|
||||
const y = x + avg(state[j], above[i])
|
||||
state[j] = y
|
||||
return y
|
||||
})))
|
||||
}
|
||||
|
||||
const paeth = (a, b, c) => {
|
||||
const p = a + b - c // initial estimate
|
||||
const pa = Math.abs(p - a) // distances to a, b, c
|
||||
const pb = Math.abs(p - b)
|
||||
const pc = Math.abs(p - c)
|
||||
// return nearest of a,b,c,
|
||||
// breaking ties in order a,b,c.
|
||||
if (pa <= pb && pa <= pc) return a
|
||||
else if (pb <= pc) return b
|
||||
else return c
|
||||
}
|
||||
|
||||
export const paethUnfilter = (row, above, bpp) => {
|
||||
const state = Buffer.alloc(bpp)
|
||||
return new BitArray(new Uint8Array(row.buffer.map((x, i) => {
|
||||
const j = i % bpp
|
||||
const y = x + paeth(state[j], above[i], above[i - bpp] ?? 0)
|
||||
state[j] = y
|
||||
return y
|
||||
})))
|
||||
}
|
||||
|
||||
const doAddBytewise = (as, bs) => as.map((x, i) => x + bs[i])
|
||||
export const addBytewise = (as, bs) => new BitArray(new Uint8Array(doAddBytewise(as.buffer, bs.buffer)))
|
||||
|
||||
const doSubBytewise = (as, bs) => as.map((a, i) => a - bs[i])
|
||||
export const subBytewise = (as, bs) => new BitArray(new Uint8Array(doSubBytewise(as.buffer, bs.buffer)))
|
||||
|
||||
const doAvgBytewise = (as, bs) => as.map(a => Math.floor((a + bs[i]) / 2))
|
||||
export const avgBytewise = (as, bs) => new BitArray(new Uint8Array(doAvgBytewise(as.buffer, bs.buffer)))
|
||||
|
||||
const doPaethBytewise = (as, bs, cs) => as.map((a, i) => {
|
||||
const b = bs[i]
|
||||
const c = cs[i]
|
||||
const p = a + b - c // initial estimate
|
||||
const pa = Math.abs(p - a) // distances to a, b, c
|
||||
const pb = Math.abs(p - b)
|
||||
const pc = Math.abs(p - c)
|
||||
// return nearest of a,b,c,
|
||||
// breaking ties in order a,b,c.
|
||||
if (pa <= pb && pa <= pc) return a
|
||||
else if (pb <= pc) return b
|
||||
else return c
|
||||
})
|
||||
export const paethBytewise = (as, bs, cs) => new BitArray(new Uint8Array(doPaethBytewise(as.buffer, bs.buffer, cs.buffer)))
|
||||
|
||||
const raise = message => { throw new Error(message) }
|
||||
|
||||
export const bitArrayToInts = (as, intSize = 8) => toList(
|
||||
intSize === 16 ? [...Array(as.buffer.length / 2)].map((_, i) => (as.buffer[i * 2] << 8) + as.buffer[i * 2 + 1]) :
|
||||
intSize === 8 ? [...as.buffer] :
|
||||
intSize === 4 ? [...as.buffer].flatMap(x => [x >> 4, x & 15]) :
|
||||
intSize === 2 ? [...as.buffer].flatMap(x => [x >> 6, x >> 4 & 3, x >> 2 & 3, x & 3]) :
|
||||
intSize === 1 ? [...as.buffer].flatMap(x => [x >> 7, x >> 6 & 1, x >> 5 & 1, x >> 4 & 1, x >> 3 & 1, x >> 2 & 1, x >> 1 & 1, x & 1]) :
|
||||
raise("Invalid int size")
|
||||
)
|
Loading…
Reference in New Issue