|
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 |
|
} |