Skip to content

Instantly share code, notes, and snippets.

@amtoine
Last active March 19, 2023 16:10
Show Gist options
  • Save amtoine/a2d45c6a804d61280a31a9ffc4b69910 to your computer and use it in GitHub Desktop.
Save amtoine/a2d45c6a804d61280a31a9ffc4b69910 to your computer and use it in GitHub Desktop.
a PNG parser for `nushell`

a PNG parser for nushell

💡 Note
greatly inspired by Reading binary data with Nushell

get the code

you can run something like this

git clone [email protected]:a2d45c6a804d61280a31a9ffc4b69910.git ~/gists/a2d45c6a804d61280a31a9ffc4b69910

and then

cd ~/gists/a2d45c6a804d61280a31a9ffc4b69910

use the command

use mod.nu *
png parse --help
def PNG_HEADER [] { 0x[89 50 4e 47 0d 0a 1a 0a] }
def "bytes slice" [index: int, nb_bytes: int] {
bytes at [$index ($index + $nb_bytes)]
}
def parse-chunks [] {
let bytes = $in
mut chunks = []
mut index = 0
mut name = ""
while $name != "IEND" {
let length = ($bytes | bytes slice $index 4 | into int)
$name = ($bytes | bytes slice ($index + 4) 4 | decode utf-8)
let data = ($bytes | bytes slice ($index + 8) $length)
$index = (
$index
+ 4 # the length is on 4 bytes
+ 4 # the name is a four-letter case-sensitive ASCII string
+ $length # the whole data
+ 4 # the CRC checksum is on 4 bytes
)
$chunks = ($chunks | append {
name: $name
size: $length
data: $data
})
}
$chunks
}
def compress-idat [] {
group-by name
| update IDAT {|all|
let all_bytes = ($all.IDAT.data | bytes collect)
{
name: "IDAT"
data: $all_bytes
size: ($all_bytes | bytes length)
}
}
| transpose
| get column1
| flatten
}
# parse binary data interpreted with the PNG format
#
# > :bulb: **Note**
# > see the [wikipedia page about PNG](https://en.wikipedia.org/wiki/PNG) for the specification used to parse the bytes
#
# Errors:
# `png parse` will throw a
# - `png::file_not_found` error if the file path is not valid
# - `png::invalid_header` error if the first 8 bytes are not the PNG header
#
# Examples:
# > i found this image of a [little ghost](https://cdn0.iconfinder.com/data/icons/shift-free/32/Pacman_Ghost-48.png),
# > let's play with it a bit.
# >
# > first we define a tool function to reduce the output of the `png parse` command
# > ```nushell
# > def only-16-data [] {
# > update data {|| get data | first 16}
# > }
# > ```
#
# parse the PNG image from a file
# > http get https://cdn0.iconfinder.com/data/icons/shift-free/32/Pacman_Ghost-48.png | save ghost.png
# > png parse ghost.png | only-16-data
# ╭───┬──────┬───────┬───────────────────────────────────────────────────────────────────────╮
# │ # │ name │ size │ data │
# ├───┼──────┼───────┼───────────────────────────────────────────────────────────────────────┤
# │ 0 │ IHDR │ 13 B │ [0, 0, 0, 48, 0, 0, 0, 48, 8, 3, 0, 0, 0] │
# │ 1 │ PLTE │ 240 B │ [0, 0, 0, 85, 170, 85, 63, 191, 127, 63, 191, 127, 56, 170, 113, 51] │
# │ 2 │ tRNS │ 49 B │ [0, 3, 4, 8, 9, 10, 21, 22, 33, 35, 36, 70, 72, 73, 88, 90] │
# │ 3 │ IDAT │ 278 B │ [120, 218, 237, 146, 217, 82, 194, 64, 16, 69, 7, 1, 69, 80, 145, 45] │
# │ 4 │ IEND │ 0 B │ [] │
# ╰───┴──────┴───────┴───────────────────────────────────────────────────────────────────────╯
#
# parse the PNG image by giving raw bytes to `png parse`
# > http get https://cdn0.iconfinder.com/data/icons/shift-free/32/Pacman_Ghost-48.png | png parse | only-16-data
#
# get the IHDR metadata of the image, e.g. it's size in pixels
# > http get https://cdn0.iconfinder.com/data/icons/shift-free/32/Pacman_Ghost-48.png | png parse --ihdr
# ╭─────────────┬────╮
# │ width │ 48 │
# │ height │ 48 │
# │ depth │ 8 │
# │ color │ 3 │
# │ compression │ 0 │
# │ filter │ 0 │
# │ interlace │ 0 │
# ╰─────────────┴────╯
#
# give invalid PNG bytes
# > open README.md | into binary | png parse
# Error:
# × png::invalid_header
# ╭─[entry #27:1:1]
# 1 │ open README.md | into binary | png parse
# · ────┬────
# · ╰── The header should be [137, 80, 78, 71, 13, 10, 26, 10], found [35, 32, 97, 32, 80, 78, 71, 32].
# ╰────
export def "png parse" [
image?: path # the path to a local PNG image
--ihdr: bool # only return the IHDR metadata of the PNG image
] {
let input = $in
mut bytes = (if ($input | is-empty) {
if not ($image | path exists) {
let span = (metadata $image | get span)
error make {
msg: $"(ansi red)png::file_not_found(ansi reset)"
label: {
text: "No such file or directory (os error)"
start: $span.start
end: $span.end
}
}
}
open $image | into binary
} else {
$input
})
# check the PNG header
let header = ($bytes | first 8)
$bytes = ($bytes | skip 8)
if $header != (PNG_HEADER) {
let span = (metadata $image | get span)
error make {
msg: $"(ansi red)png::invalid_header(ansi reset)"
label: {
text: $"(ansi purple)The header should be (ansi green)(PNG_HEADER)(ansi purple), found (ansi red)($header)(ansi purple)."
start: $span.start
end: $span.end
}
}
}
let chunks = ($bytes | parse-chunks | compress-idat)
if $ihdr {
let ihdr = ($chunks | where name == IHDR | get data.0)
return {
width: ($ihdr | bytes at 0,4 | into int)
height: ($ihdr | bytes at 4,8 | into int)
depth: ($ihdr | bytes at 8,9 | into int)
color: ($ihdr | bytes at 9,10 | into int)
compression: ($ihdr | bytes at 10,11 | into int)
filter: ($ihdr | bytes at 11,12 | into int)
interlace: ($ihdr | bytes at 12,13 | into int)
}
}
$chunks | into filesize size
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment