123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- #+vet !using-stmt
- package netpbm
- import "core:bytes"
- import "core:fmt"
- import "core:image"
- import "core:mem"
- import "core:strconv"
- import "core:strings"
- import "core:unicode"
- import "base:runtime"
- Image :: image.Image
- Format :: image.Netpbm_Format
- Header :: image.Netpbm_Header
- Info :: image.Netpbm_Info
- Error :: image.Error
- Format_Error :: image.Netpbm_Error
- Formats :: bit_set[Format]
- PBM :: Formats{.P1, .P4}
- PGM :: Formats{.P2, .P5}
- PPM :: Formats{.P3, .P6}
- PNM :: PBM + PGM + PPM
- PAM :: Formats{.P7}
- PFM :: Formats{.Pf, .PF}
- ASCII :: Formats{.P1, .P2, .P3}
- BINARY :: Formats{.P4, .P5, .P6} + PAM + PFM
- load_from_bytes :: proc(data: []byte, allocator := context.allocator) -> (img: ^Image, err: Error) {
- context.allocator = allocator
- img = new(Image)
- img.which = .NetPBM
- header: Header; defer header_destroy(&header)
- header_size: int
- header, header_size = parse_header(data) or_return
- img_data := data[header_size:]
- decode_image(img, header, img_data) or_return
- info := new(Info)
- info.header = header
- if header.format == .P7 && header.tupltype != "" {
- info.header.tupltype = strings.clone(header.tupltype)
- }
- img.metadata = info
- return img, nil
- }
- save_to_buffer :: proc(img: ^Image, custom_info: Info = {}, allocator := context.allocator) -> (buffer: []byte, err: Error) {
- context.allocator = allocator
- info: Info = {}
- if custom_info.header.width > 0 {
- // Custom info has been set, use it.
- info = custom_info
- } else {
- img_info, ok := img.metadata.(^image.Netpbm_Info)
- if !ok {
- // image doesn't have .Netpbm info, guess it
- auto_info, auto_info_found := autoselect_pbm_format_from_image(img)
- if auto_info_found {
- info = auto_info
- } else {
- return {}, .Invalid_Input_Image
- }
- } else {
- // use info as stored on image
- info = img_info^
- }
- }
- // using info so we can just talk about the header
- using info
- // validation
- if header.format in (PBM + PGM + Formats{.Pf}) && img.channels != 1 \
- || header.format in (PPM + Formats{.PF}) && img.channels != 3 {
- err = .Invalid_Number_Of_Channels
- return
- }
- if header.format in (PNM + PAM) {
- if header.maxval <= int(max(u8)) && img.depth != 8 \
- || header.maxval > int(max(u8)) && header.maxval <= int(max(u16)) && img.depth != 16 {
- err = .Invalid_Image_Depth
- return
- }
- } else if header.format in PFM && img.depth != 32 {
- err = .Invalid_Image_Depth
- return
- }
- // we will write to a string builder
- data: strings.Builder
- strings.builder_init(&data)
- // all PNM headers start with the format
- fmt.sbprintf(&data, "%s\n", header.format)
- if header.format in PNM {
- fmt.sbprintf(&data, "%i %i\n", img.width, img.height)
- if header.format not_in PBM {
- fmt.sbprintf(&data, "%i\n", header.maxval)
- }
- } else if header.format in PAM {
- if len(header.tupltype) > 0 {
- fmt.sbprintf(&data, "WIDTH %i\nHEIGHT %i\nMAXVAL %i\nDEPTH %i\nTUPLTYPE %s\nENDHDR\n",
- img.width, img.height, header.maxval, img.channels, header.tupltype)
- } else {
- fmt.sbprintf(&data, "WIDTH %i\nHEIGHT %i\nMAXVAL %i\nDEPTH %i\nENDHDR\n",
- img.width, img.height, header.maxval, img.channels)
- }
- } else if header.format in PFM {
- scale := -header.scale if header.little_endian else header.scale
- fmt.sbprintf(&data, "%i %i\n%f\n", img.width, img.height, scale)
- }
- switch header.format {
- // Compressed binary
- case .P4:
- header_buf := data.buf[:]
- pixels := img.pixels.buf[:]
- p4_buffer_size := (img.width / 8 + 1) * img.height
- reserve(&data.buf, len(header_buf) + p4_buffer_size)
- // we build up a byte value until it is completely filled
- // or we reach the end the row
- for y in 0 ..< img.height {
- b: byte
- for x in 0 ..< img.width {
- i := y * img.width + x
- bit := byte(7 - (x % 8))
- v : byte = 0 if pixels[i] == 0 else 1
- b |= (v << bit)
- if bit == 0 {
- append(&data.buf, b)
- b = 0
- }
- }
- if b != 0 {
- append(&data.buf, b)
- b = 0
- }
- }
- // Simple binary
- case .P5, .P6, .P7, .Pf, .PF:
- header_buf := data.buf[:]
- pixels := img.pixels.buf[:]
- resize(&data.buf, len(header_buf) + len(pixels))
- mem.copy(raw_data(data.buf[len(header_buf):]), raw_data(pixels), len(pixels))
- // convert from native endianness
- if img.depth == 16 {
- pixels := mem.slice_data_cast([]u16be, data.buf[len(header_buf):])
- for &p in pixels {
- p = u16be(transmute(u16) p)
- }
- } else if header.format in PFM {
- if header.little_endian {
- pixels := mem.slice_data_cast([]f32le, data.buf[len(header_buf):])
- for &p in pixels {
- p = f32le(transmute(f32) p)
- }
- } else {
- pixels := mem.slice_data_cast([]f32be, data.buf[len(header_buf):])
- for &p in pixels {
- p = f32be(transmute(f32) p)
- }
- }
- }
- // If-it-looks-like-a-bitmap ASCII
- case .P1:
- pixels := img.pixels.buf[:]
- for y in 0 ..< img.height {
- for x in 0 ..< img.width {
- i := y * img.width + x
- append(&data.buf, '0' if pixels[i] == 0 else '1')
- }
- append(&data.buf, '\n')
- }
- // Token ASCII
- case .P2, .P3:
- switch img.depth {
- case 8:
- pixels := img.pixels.buf[:]
- for y in 0 ..< img.height {
- for x in 0 ..< img.width {
- i := y * img.width + x
- for c in 0 ..< img.channels {
- j := i * img.channels + c
- fmt.sbprintf(&data, "%i ", pixels[j])
- }
- fmt.sbprint(&data, "\n")
- }
- fmt.sbprint(&data, "\n")
- }
- case 16:
- pixels := mem.slice_data_cast([]u16, img.pixels.buf[:])
- for y in 0 ..< img.height {
- for x in 0 ..< img.width {
- i := y * img.width + x
- for c in 0 ..< img.channels {
- j := i * img.channels + c
- fmt.sbprintf(&data, "%i ", pixels[j])
- }
- fmt.sbprint(&data, "\n")
- }
- fmt.sbprint(&data, "\n")
- }
- case:
- return data.buf[:], .Invalid_Image_Depth
- }
- case:
- return data.buf[:], .Invalid_Format
- }
- return data.buf[:], Format_Error.None
- }
- parse_header :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) {
- context.allocator = allocator
- // we need the signature and a space
- if len(data) < 3 {
- err = Format_Error.Incomplete_Header
- return
- }
- if data[0] == 'P' {
- switch data[1] {
- case '1' ..= '6':
- return _parse_header_pnm(data)
- case '7':
- return _parse_header_pam(data, allocator)
- case 'F', 'f':
- return _parse_header_pfm(data)
- }
- }
- err = .Invalid_Signature
- return
- }
- @(private)
- _parse_header_pnm :: proc(data: []byte) -> (header: Header, length: int, err: Error) {
- SIG_LENGTH :: 2
- {
- header_formats := []Format{.P1, .P2, .P3, .P4, .P5, .P6}
- header.format = header_formats[data[1] - '0' - 1]
- }
- // have a list of fielda for easy iteration
- header_fields: []^int
- if header.format in PBM {
- header_fields = {&header.width, &header.height}
- header.maxval = 1 // we know maxval for a bitmap
- } else {
- header_fields = {&header.width, &header.height, &header.maxval}
- }
- // we're keeping track of the header byte length
- length = SIG_LENGTH
- // loop state
- in_comment := false
- already_in_space := true
- current_field := 0
- current_value := header_fields[0]
- parse_loop: for d in data[SIG_LENGTH:] {
- length += 1
- // handle comments
- if in_comment {
- switch d {
- // comments only go up to next carriage return or line feed
- case '\r', '\n':
- in_comment = false
- }
- continue
- } else if d == '#' {
- in_comment = true
- continue
- }
- // handle whitespace
- in_space := unicode.is_white_space(rune(d))
- if in_space {
- if already_in_space {
- continue
- }
- already_in_space = true
- // switch to next value
- current_field += 1
- if current_field == len(header_fields) {
- // header byte length is 1-index so we'll increment again
- length += 1
- break parse_loop
- }
- current_value = header_fields[current_field]
- } else {
- already_in_space = false
- if !unicode.is_digit(rune(d)) {
- err = Format_Error.Invalid_Header_Token_Character
- return
- }
- val := int(d - '0')
- current_value^ = current_value^ * 10 + val
- }
- }
- // set extra info
- header.channels = 3 if header.format in PPM else 1
- header.depth = 16 if header.maxval > int(max(u8)) else 8
- // limit checking
- if current_field < len(header_fields) {
- err = Format_Error.Incomplete_Header
- return
- }
- if header.width < 1 \
- || header.height < 1 \
- || header.maxval < 1 || header.maxval > int(max(u16)) {
- fmt.printf("[pnm] Header: {{width = %v, height = %v, maxval: %v}}\n", header.width, header.height, header.maxval)
- err = .Invalid_Header_Value
- return
- }
- length -= 1
- err = Format_Error.None
- return
- }
- @(private)
- _parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) {
- context.allocator = allocator
- // the spec needs the newline apparently
- if string(data[0:3]) != "P7\n" {
- err = .Invalid_Signature
- return
- }
- header.format = .P7
- SIGNATURE_LENGTH :: 3
- HEADER_END :: "ENDHDR\n"
- // we can already work out the size of the header
- header_end_index := strings.index(string(data), HEADER_END)
- if header_end_index == -1 {
- err = Format_Error.Incomplete_Header
- return
- }
- length = header_end_index + len(HEADER_END)
- runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(ignore = context.temp_allocator == allocator)
- // string buffer for the tupltype
- tupltype: strings.Builder
- strings.builder_init(&tupltype, context.temp_allocator); defer strings.builder_destroy(&tupltype)
- fmt.sbprint(&tupltype, "")
- // PAM uses actual lines, so we can iterate easily
- line_iterator := string(data[SIGNATURE_LENGTH : header_end_index])
- parse_loop: for line in strings.split_lines_iterator(&line_iterator) {
- line := line
- if len(line) == 0 || line[0] == '#' {
- continue
- }
- field, ok := strings.fields_iterator(&line)
- value := strings.trim_space(line)
- // the field will change, but the logic stays the same
- current_field: ^int
- switch field {
- case "WIDTH": current_field = &header.width
- case "HEIGHT": current_field = &header.height
- case "DEPTH": current_field = &header.channels
- case "MAXVAL": current_field = &header.maxval
- case "TUPLTYPE":
- if len(value) == 0 {
- err = .Invalid_Header_Value
- return
- }
- if len(tupltype.buf) == 0 {
- fmt.sbprint(&tupltype, value)
- } else {
- fmt.sbprint(&tupltype, "", value)
- }
- continue
- case:
- continue
- }
- if current_field^ != 0 {
- err = Format_Error.Duplicate_Header_Field
- return
- }
- current_field^, ok = strconv.parse_int(value)
- if !ok {
- err = Format_Error.Invalid_Header_Value
- return
- }
- }
- // extra info
- header.depth = 16 if header.maxval > int(max(u8)) else 8
- // limit checking
- if header.width < 1 \
- || header.height < 1 \
- || header.maxval < 1 \
- || header.maxval > int(max(u16)) {
- fmt.printf("[pam] Header: {{width = %v, height = %v, maxval: %v}}\n", header.width, header.height, header.maxval)
- err = Format_Error.Invalid_Header_Value
- return
- }
- header.tupltype = strings.clone(strings.to_string(tupltype))
- err = Format_Error.None
- return
- }
- @(private)
- _parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Error) {
- // we can just cycle through tokens for PFM
- field_iterator := string(data)
- field, ok := strings.fields_iterator(&field_iterator)
- switch field {
- case "Pf":
- header.format = .Pf
- header.channels = 1
- case "PF":
- header.format = .PF
- header.channels = 3
- case:
- err = .Invalid_Signature
- return
- }
- // floating point
- header.depth = 32
- // width
- field, ok = strings.fields_iterator(&field_iterator)
- if !ok {
- err = Format_Error.Incomplete_Header
- return
- }
- header.width, ok = strconv.parse_int(field)
- if !ok {
- err = Format_Error.Invalid_Header_Value
- return
- }
- // height
- field, ok = strings.fields_iterator(&field_iterator)
- if !ok {
- err = Format_Error.Incomplete_Header
- return
- }
- header.height, ok = strconv.parse_int(field)
- if !ok {
- err = Format_Error.Invalid_Header_Value
- return
- }
- // scale (sign is endianness)
- field, ok = strings.fields_iterator(&field_iterator)
- if !ok {
- err = Format_Error.Incomplete_Header
- return
- }
- header.scale, ok = strconv.parse_f32(field)
- if !ok {
- err = Format_Error.Invalid_Header_Value
- return
- }
- if header.scale < 0.0 {
- header.little_endian = true
- header.scale = -header.scale
- }
- // pointer math to get header size
- length = int((uintptr(raw_data(field_iterator)) + 1) - uintptr(raw_data(data)))
- // limit checking
- if header.width < 1 \
- || header.height < 1 \
- || header.scale == 0.0 {
- fmt.printf("[pfm] Header: {{width = %v, height = %v, scale: %v}}\n", header.width, header.height, header.scale)
- err = .Invalid_Header_Value
- return
- }
- err = Format_Error.None
- return
- }
- decode_image :: proc(img: ^Image, header: Header, data: []byte, allocator := context.allocator) -> (err: Error) {
- assert(img != nil)
- context.allocator = allocator
- img.width = header.width
- img.height = header.height
- img.channels = header.channels
- img.depth = header.depth
- buffer_size := image.compute_buffer_size(img.width, img.height, img.channels, img.depth)
- // we can check data size for binary formats
- if header.format in BINARY {
- if len(data) < buffer_size {
- fmt.printf("len(data): %v, buffer size: %v\n", len(data), buffer_size)
- return .Buffer_Too_Small
- }
- }
- // for ASCII and P4, we use length for the termination condition, so start at 0
- // BINARY will be a simple memcopy so the buffer length should also be initialised
- if header.format in ASCII || header.format == .P4 {
- bytes.buffer_init_allocator(&img.pixels, 0, buffer_size)
- } else {
- bytes.buffer_init_allocator(&img.pixels, buffer_size, buffer_size)
- }
- switch header.format {
- // Compressed binary
- case .P4:
- for d in data {
- for b in 1 ..= 8 {
- bit := byte(8 - b)
- pix := (d >> bit) & 1
- bytes.buffer_write_byte(&img.pixels, pix)
- if len(img.pixels.buf) % img.width == 0 {
- break
- }
- }
- if len(img.pixels.buf) == cap(img.pixels.buf) {
- break
- }
- }
- // Simple binary
- case .P5, .P6, .P7, .Pf, .PF:
- copy(img.pixels.buf[:], data[:])
- // convert to native endianness
- if header.format in PFM {
- pixels := mem.slice_data_cast([]f32, img.pixels.buf[:])
- if header.little_endian {
- for &p in pixels {
- p = f32(transmute(f32le) p)
- }
- } else {
- for &p in pixels {
- p = f32(transmute(f32be) p)
- }
- }
- } else {
- if img.depth == 16 {
- pixels := mem.slice_data_cast([]u16, img.pixels.buf[:])
- for &p in pixels {
- p = u16(transmute(u16be) p)
- }
- }
- }
- // If-it-looks-like-a-bitmap ASCII
- case .P1:
- for c in data {
- switch c {
- case '0', '1':
- bytes.buffer_write_byte(&img.pixels, c - '0')
- }
- if len(img.pixels.buf) == cap(img.pixels.buf) {
- break
- }
- }
- if len(img.pixels.buf) < cap(img.pixels.buf) {
- err = Format_Error.Buffer_Too_Small
- return
- }
- // Token ASCII
- case .P2, .P3:
- field_iterator := string(data)
- for field in strings.fields_iterator(&field_iterator) {
- value, ok := strconv.parse_int(field)
- if !ok {
- err = Format_Error.Invalid_Buffer_ASCII_Token
- return
- }
- //? do we want to enforce the maxval, the limit, or neither
- if value > int(max(u16)) /*header.maxval*/ {
- err = Format_Error.Invalid_Buffer_Value
- return
- }
- switch img.depth {
- case 8:
- bytes.buffer_write_byte(&img.pixels, u8(value))
- case 16:
- vb := transmute([2]u8) u16(value)
- bytes.buffer_write(&img.pixels, vb[:])
- }
- if len(img.pixels.buf) == cap(img.pixels.buf) {
- break
- }
- }
- if len(img.pixels.buf) < cap(img.pixels.buf) {
- err = Format_Error.Buffer_Too_Small
- return
- }
- }
- err = Format_Error.None
- return
- }
- // Automatically try to select an appropriate format to save to based on `img.channel` and `img.depth`
- autoselect_pbm_format_from_image :: proc(img: ^Image, prefer_binary := true, force_black_and_white := false, pfm_scale := f32(1.0)) -> (res: Info, ok: bool) {
- /*
- PBM (P1, P4): Portable Bit Map, stores black and white images (1 channel)
- PGM (P2, P5): Portable Gray Map, stores greyscale images (1 channel, 1 or 2 bytes per value)
- PPM (P3, P6): Portable Pixel Map, stores colour images (3 channel, 1 or 2 bytes per value)
- PAM (P7 ): Portable Arbitrary Map, stores arbitrary channel images (1 or 2 bytes per value)
- PFM (Pf, PF): Portable Float Map, stores floating-point images (Pf: 1 channel, PF: 3 channel)
- ASCII :: Formats{.P1, .P2, .P3}
- */
- using res.header
- width = img.width
- height = img.height
- channels = img.channels
- depth = img.depth
- maxval = 255 if img.depth == 8 else 65535
- little_endian = true if ODIN_ENDIAN == .Little else false
- // Assume we'll find a suitable format
- ok = true
- switch img.channels {
- case 1:
- // Must be Portable Float Map
- if img.depth == 32 {
- format = .Pf
- return
- }
- if force_black_and_white {
- // Portable Bit Map
- format = .P4 if prefer_binary else .P1
- maxval = 1
- return
- } else {
- // Portable Gray Map
- format = .P5 if prefer_binary else .P2
- return
- }
- case 3:
- // Must be Portable Float Map
- if img.depth == 32 {
- format = .PF
- return
- }
- // Portable Pixel Map
- format = .P6 if prefer_binary else .P3
- return
- case:
- // Portable Arbitrary Map
- if img.depth == 8 || img.depth == 16 {
- format = .P7
- scale = pfm_scale
- return
- }
- }
- // We couldn't find a suitable format
- return {}, false
- }
- @(init, private)
- _register :: proc() {
- loader :: proc(data: []byte, options: image.Options, allocator: mem.Allocator) -> (img: ^Image, err: Error) {
- return load_from_bytes(data, allocator)
- }
- destroyer :: proc(img: ^Image) {
- _ = destroy(img)
- }
- image.register(.NetPBM, loader, destroyer)
- }
|