Various changes to TGA reader

- Style changes
- Change ptr usage to slice indexing
- Add TGA Footer

Also, add `peek_data` with offset to `compress`.
This commit is contained in:
Jeroen van Rijn
2022-08-28 18:25:07 +02:00
parent 6363013dd8
commit f74e281efa
3 changed files with 159 additions and 54 deletions
+56 -1
View File
@@ -294,6 +294,24 @@ peek_data_from_memory :: #force_inline proc(z: ^Context_Memory_Input, $T: typeid
} }
} }
@(optimization_mode="speed")
peek_data_at_offset_from_memory :: #force_inline proc(z: ^Context_Memory_Input, $T: typeid, #any_int offset: int) -> (res: T, err: io.Error) {
size :: size_of(T)
#no_bounds_check {
if len(z.input_data) >= size + offset {
buf := z.input_data[offset:][:size]
return (^T)(&buf[0])^, .None
}
}
if len(z.input_data) == 0 {
return T{}, .EOF
} else {
return T{}, .Short_Buffer
}
}
@(optimization_mode="speed") @(optimization_mode="speed")
peek_data_from_stream :: #force_inline proc(z: ^Context_Stream_Input, $T: typeid) -> (res: T, err: io.Error) { peek_data_from_stream :: #force_inline proc(z: ^Context_Stream_Input, $T: typeid) -> (res: T, err: io.Error) {
size :: size_of(T) size :: size_of(T)
@@ -321,7 +339,44 @@ peek_data_from_stream :: #force_inline proc(z: ^Context_Stream_Input, $T: typeid
return res, .None return res, .None
} }
peek_data :: proc{peek_data_from_memory, peek_data_from_stream} @(optimization_mode="speed")
peek_data_at_offset_from_stream :: #force_inline proc(z: ^Context_Stream_Input, $T: typeid, #any_int offset: int) -> (res: T, err: io.Error) {
size :: size_of(T)
// Get current position to return to.
cur_pos, e1 := z.input->impl_seek(0, .Current)
if e1 != .None {
return T{}, e1
}
// Seek to offset.
pos, e2 := z.input->impl_seek(offset, .Start)
if e2 != .None {
return T{}, e2
}
r, e3 := io.to_reader_at(z.input)
if !e3 {
return T{}, .Empty
}
when size <= 128 {
b: [size]u8
} else {
b := make([]u8, size, context.temp_allocator)
}
_, e4 := io.read_at(r, b[:], pos)
if e4 != .None {
return T{}, .Empty
}
// Return read head to original position.
z.input->impl_seek(cur_pos, .Start)
res = (^T)(&b[0])^
return res, .None
}
peek_data :: proc{peek_data_from_memory, peek_data_from_stream, peek_data_at_offset_from_memory, peek_data_at_offset_from_stream}
+14 -3
View File
@@ -46,7 +46,7 @@ Image :: struct {
height: int, height: int,
channels: int, channels: int,
depth: int, // Channel depth in bits, typically 8 or 16 depth: int, // Channel depth in bits, typically 8 or 16
pixels: bytes.Buffer, pixels: bytes.Buffer `fmt:"-"`,
/* /*
Some image loaders/writers can return/take an optional background color. Some image loaders/writers can return/take an optional background color.
For convenience, we return them as u16 so we don't need to switch on the type For convenience, we return them as u16 so we don't need to switch on the type
@@ -380,7 +380,7 @@ QOI_Info :: struct {
TGA_Data_Type :: enum u8 { TGA_Data_Type :: enum u8 {
Uncompressed_RGB = 2, Uncompressed_RGB = 2,
Compressed_RBB = 10, Compressed_RBB = 10,
} }
TGA_Header :: struct #packed { TGA_Header :: struct #packed {
@@ -397,8 +397,19 @@ TGA_Header :: struct #packed {
} }
#assert(size_of(TGA_Header) == 18) #assert(size_of(TGA_Header) == 18)
New_TGA_Signature :: "TRUEVISION-XFILE.\x00"
TGA_Footer :: struct #packed {
extension_area_offset: u32le,
developer_directory_offset: u32le,
signature: [18]u8 `fmt:"s"`, // Should match signature if New TGA.
}
#assert(size_of(TGA_Footer) == 26)
TGA_Info :: struct { TGA_Info :: struct {
header: TGA_Header, header: TGA_Header,
image_id: string,
footer: Maybe(TGA_Footer),
} }
// Function to help with image buffer calculations // Function to help with image buffer calculations
+89 -50
View File
@@ -16,10 +16,12 @@ import "core:image"
import "core:bytes" import "core:bytes"
import "core:os" import "core:os"
import "core:compress" import "core:compress"
import "core:strings"
import "core:fmt"
_ :: fmt
// TODO: alpha_premultiply support // TODO: alpha_premultiply support
Error :: image.Error Error :: image.Error
Image :: image.Image Image :: image.Image
Options :: image.Options Options :: image.Options
@@ -122,6 +124,21 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
options -= {.return_header} options -= {.return_header}
} }
// First check for a footer.
filesize := compress.input_size(ctx) or_return
footer: image.TGA_Footer
have_valid_footer := false
if filesize >= size_of(image.TGA_Header) + size_of(image.TGA_Footer) {
if f, f_err := compress.peek_data(ctx, image.TGA_Footer, filesize - i64(size_of(image.TGA_Footer))); f_err == .None {
if string(f.signature[:]) == image.New_TGA_Signature {
have_valid_footer = true
footer = f
}
}
}
header := image.read_data(ctx, image.TGA_Header) or_return header := image.read_data(ctx, image.TGA_Header) or_return
// Header checks // Header checks
@@ -132,14 +149,16 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
case .Uncompressed_RGB: case .Uncompressed_RGB:
case: return nil, .Unsupported_Format case: return nil, .Unsupported_Format
} }
if header.bits_per_pixel!=24 && header.bits_per_pixel!=32 {
return nil, .Unsupported_Format if header.bits_per_pixel != 24 && header.bits_per_pixel != 32 {
}
if ( header.image_descriptor & IMAGE_DESCRIPTOR_INTERLEAVING_MASK ) != 0 {
return nil, .Unsupported_Format return nil, .Unsupported_Format
} }
if (int(header.dimensions[0])*int(header.dimensions[1])) > image.MAX_DIMENSIONS { if header.image_descriptor & IMAGE_DESCRIPTOR_INTERLEAVING_MASK != 0 {
return nil, .Unsupported_Format
}
if int(header.dimensions[0]) * int(header.dimensions[1]) > image.MAX_DIMENSIONS {
return nil, .Image_Dimensions_Too_Large return nil, .Image_Dimensions_Too_Large
} }
@@ -147,88 +166,104 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
img = new(Image) img = new(Image)
} }
defer if err != nil {
destroy(img)
}
src_channels := int(header.bits_per_pixel) / 8
img.which = .TGA
img.channels = 4 if .alpha_add_if_missing in options else src_channels
img.channels = 3 if .alpha_drop_if_present in options else img.channels
img.depth = 8
img.width = int(header.dimensions[0])
img.height = int(header.dimensions[1])
// Read Image ID if present
image_id := ""
if _id, e := compress.read_slice(ctx, int(header.id_length)); e != .None {
return nil, .Corrupt
} else {
if .return_metadata in options {
id := strings.trim_right_null(string(_id))
image_id = strings.clone(id)
}
}
if .return_metadata in options { if .return_metadata in options {
info := new(image.TGA_Info) info := new(image.TGA_Info)
info.header = header info.header = header
info.image_id = image_id
if have_valid_footer {
info.footer = footer
}
img.metadata = info img.metadata = info
} }
src_channels := int(header.bits_per_pixel)/8
img.which = .TGA
img.channels = .alpha_add_if_missing in options ? 4: src_channels
img.channels = .alpha_drop_if_present in options ? 3: img.channels
img.depth = 8
img.width = int(header.dimensions[0])
img.height = int(header.dimensions[1])
if .do_not_decompress_image in options { if .do_not_decompress_image in options {
return img, nil return img, nil
} }
// skip id
if _, e := compress.read_slice(ctx, int(header.id_length)); e!= .None {
destroy(img)
return nil, .Corrupt
}
if !resize(&img.pixels.buf, img.channels * img.width * img.height) { if !resize(&img.pixels.buf, img.channels * img.width * img.height) {
destroy(img) return img, .Unable_To_Allocate_Or_Resize
return nil, .Unable_To_Allocate_Or_Resize
} }
origin_is_topleft := (header.image_descriptor & IMAGE_DESCRIPTOR_TOPLEFT_MASK ) != 0 origin_is_topleft := header.image_descriptor & IMAGE_DESCRIPTOR_TOPLEFT_MASK != 0
rle_repetition_count := 0 rle_repetition_count := 0
read_pixel := true read_pixel := true
is_packet_rle := false is_packet_rle := false
pixel: [4]u8
for y in 0..<img.height {
line := origin_is_topleft ? y : img.height-y-1
dst := mem.ptr_offset(mem.raw_data(img.pixels.buf), line*img.width*img.channels)
for x in 0..<img.width {
pixel: [4]u8
stride := img.width * img.channels
line := 0 if origin_is_topleft else img.height - 1
for _ in 0..<img.height {
offset := line * stride
for _ in 0..<img.width {
// handle RLE decoding // handle RLE decoding
if rle_encoding { if rle_encoding {
if rle_repetition_count == 0 { if rle_repetition_count == 0 {
rle_cmd, err := compress.read_u8(ctx) rle_cmd, err := compress.read_u8(ctx)
if err!=.None { if err != .None {
destroy(img) return img, .Corrupt
return nil, .Corrupt
} }
is_packet_rle = (rle_cmd>>7) != 0 is_packet_rle = (rle_cmd >> 7) != 0
rle_repetition_count = 1 + int(rle_cmd & 0x7F) rle_repetition_count = 1 + int(rle_cmd & 0x7F)
read_pixel = true read_pixel = true
} else if is_packet_rle==false { } else if !is_packet_rle {
read_pixel = rle_repetition_count>0 read_pixel = rle_repetition_count > 0
} else { } else {
read_pixel = false read_pixel = false
} }
} }
// Read pixel // Read pixel
if read_pixel { if read_pixel {
src, err := compress.read_slice(ctx, src_channels) src, src_err := compress.read_slice(ctx, src_channels)
if err!=.None { if src_err != .None {
destroy(img) return img, .Corrupt
return nil, .Corrupt
} }
pixel[2] = src[0] pixel[2] = src[0]
pixel[1] = src[1] pixel[1] = src[1]
pixel[0] = src[2] pixel[0] = src[2]
pixel[3] = src_channels==4 ? src[3] : 255
if img.channels==4 { pixel[3] = src_channels == 4 ? src[3] : 255
if src_channels==4 { if img.channels == 4 {
dst[3] = src[3] if src_channels == 4 {
img.pixels.buf[offset:][3] = src[3]
} else { } else {
dst[3] = 255 img.pixels.buf[offset:][3] = 255
} }
} }
} }
// Write pixel // Write pixel
mem.copy(dst, mem.raw_data(&pixel), img.channels) copy(img.pixels.buf[offset:], pixel[:img.channels])
dst = mem.ptr_offset(dst, img.channels) offset += img.channels
rle_repetition_count -= 1 rle_repetition_count -= 1
} }
line += 1 if origin_is_topleft else -1
} }
return img, nil return img, nil
} }
@@ -258,15 +293,19 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont
load :: proc{load_from_file, load_from_bytes, load_from_context} load :: proc{load_from_file, load_from_bytes, load_from_context}
destroy :: proc(img: ^Image) { destroy :: proc(img: ^Image) {
if img == nil { if img == nil || img.width == 0 || img.height == 0 {
return return
} }
bytes.buffer_destroy(&img.pixels) bytes.buffer_destroy(&img.pixels)
if v, ok := img.metadata.(^image.TGA_Info); ok { if v, ok := img.metadata.(^image.TGA_Info); ok {
delete(v.image_id)
free(v) free(v)
} }
// Make destroy idempotent
img.width = 0
img.height = 0
free(img) free(img)
} }