This commit is contained in:
gingerBill
2021-10-07 13:41:26 +01:00
11 changed files with 465 additions and 299 deletions
+4 -9
View File
@@ -1,5 +1,3 @@
package compress
/* /*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>. Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-3 license. Made available under Odin's BSD-3 license.
@@ -7,10 +5,11 @@ package compress
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation, optimization. Jeroen van Rijn: Initial implementation, optimization.
*/ */
package compress
import "core:io" import "core:io"
import "core:image"
import "core:bytes" import "core:bytes"
import "core:runtime"
/* /*
These settings bound how much compression algorithms will allocate for their output buffer. These settings bound how much compression algorithms will allocate for their output buffer.
@@ -51,11 +50,8 @@ Error :: union {
ZLIB_Error, ZLIB_Error,
GZIP_Error, GZIP_Error,
ZIP_Error, ZIP_Error,
/*
This is here because png.load will return a this type of error union, runtime.Allocator_Error,
as it may involve an I/O error, a Deflate error, etc.
*/
image.Error,
} }
General_Error :: enum { General_Error :: enum {
@@ -69,7 +65,6 @@ General_Error :: enum {
Incompatible_Options, Incompatible_Options,
Unimplemented, Unimplemented,
/* /*
Memory errors Memory errors
*/ */
+123 -12
View File
@@ -1,16 +1,17 @@
package image
/* /*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>. Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-2 license. Made available under Odin's BSD-3 license.
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation, optimization. Jeroen van Rijn: Initial implementation, optimization.
Ginger Bill: Cosmetic changes. Ginger Bill: Cosmetic changes.
*/ */
package image
import "core:bytes" import "core:bytes"
import "core:mem" import "core:mem"
import "core:compress"
import "core:runtime"
Image :: struct { Image :: struct {
width: int, width: int,
@@ -25,8 +26,11 @@ Image :: struct {
*/ */
background: Maybe([3]u16), background: Maybe([3]u16),
metadata_ptr: rawptr, metadata: Image_Metadata,
metadata_type: typeid, }
Image_Metadata :: union {
^PNG_Info,
} }
/* /*
@@ -112,31 +116,140 @@ Option :: enum {
} }
Options :: distinct bit_set[Option] Options :: distinct bit_set[Option]
Error :: enum { Error :: union {
General_Image_Error,
PNG_Error,
compress.Error,
compress.General_Error,
compress.Deflate_Error,
compress.ZLIB_Error,
runtime.Allocator_Error,
}
General_Image_Error :: enum {
None = 0,
Invalid_Image_Dimensions,
Image_Dimensions_Too_Large,
Image_Does_Not_Adhere_to_Spec,
}
PNG_Error :: enum {
Invalid_PNG_Signature, Invalid_PNG_Signature,
IHDR_Not_First_Chunk, IHDR_Not_First_Chunk,
IHDR_Corrupt, IHDR_Corrupt,
IDAT_Missing, IDAT_Missing,
IDAT_Must_Be_Contiguous, IDAT_Must_Be_Contiguous,
IDAT_Corrupt, IDAT_Corrupt,
PNG_Does_Not_Adhere_to_Spec, IDAT_Size_Too_Large,
PLTE_Encountered_Unexpectedly, PLTE_Encountered_Unexpectedly,
PLTE_Invalid_Length, PLTE_Invalid_Length,
TRNS_Encountered_Unexpectedly, TRNS_Encountered_Unexpectedly,
BKGD_Invalid_Length, BKGD_Invalid_Length,
Invalid_Image_Dimensions,
Unknown_Color_Type, Unknown_Color_Type,
Invalid_Color_Bit_Depth_Combo, Invalid_Color_Bit_Depth_Combo,
Unknown_Filter_Method, Unknown_Filter_Method,
Unknown_Interlace_Method, Unknown_Interlace_Method,
Requested_Channel_Not_Present, Requested_Channel_Not_Present,
Post_Processing_Error, Post_Processing_Error,
Invalid_Chunk_Length,
}
/*
PNG-specific structs
*/
PNG_Info :: struct {
header: PNG_IHDR,
chunks: [dynamic]PNG_Chunk,
}
PNG_Chunk_Header :: struct #packed {
length: u32be,
type: PNG_Chunk_Type,
}
PNG_Chunk :: struct #packed {
header: PNG_Chunk_Header,
data: []byte,
crc: u32be,
}
PNG_Chunk_Type :: enum u32be {
// IHDR must come first in a file
IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R',
// PLTE must precede the first IDAT chunk
PLTE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E',
bKGD = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D',
tRNS = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S',
IDAT = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T',
iTXt = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't',
tEXt = 't' << 24 | 'E' << 16 | 'X' << 8 | 't',
zTXt = 'z' << 24 | 'T' << 16 | 'X' << 8 | 't',
iCCP = 'i' << 24 | 'C' << 16 | 'C' << 8 | 'P',
pHYs = 'p' << 24 | 'H' << 16 | 'Y' << 8 | 's',
gAMA = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A',
tIME = 't' << 24 | 'I' << 16 | 'M' << 8 | 'E',
sPLT = 's' << 24 | 'P' << 16 | 'L' << 8 | 'T',
sRGB = 's' << 24 | 'R' << 16 | 'G' << 8 | 'B',
hIST = 'h' << 24 | 'I' << 16 | 'S' << 8 | 'T',
cHRM = 'c' << 24 | 'H' << 16 | 'R' << 8 | 'M',
sBIT = 's' << 24 | 'B' << 16 | 'I' << 8 | 'T',
/*
eXIf tags are not part of the core spec, but have been ratified
in v1.5.0 of the PNG Ext register.
We will provide unprocessed chunks to the caller if `.return_metadata` is set.
Applications are free to implement an Exif decoder.
*/
eXIf = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f',
// PNG files must end with IEND
IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D',
/*
XCode sometimes produces "PNG" files that don't adhere to the PNG spec.
We recognize them only in order to avoid doing further work on them.
Some tools like PNG Defry may be able to repair them, but we're not
going to reward Apple for producing proprietary broken files purporting
to be PNGs by supporting them.
*/
iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T',
CbGI = 'C' << 24 | 'b' << 16 | 'H' << 8 | 'I',
}
PNG_IHDR :: struct #packed {
width: u32be,
height: u32be,
bit_depth: u8,
color_type: PNG_Color_Type,
compression_method: u8,
filter_method: u8,
interlace_method: PNG_Interlace_Method,
}
PNG_IHDR_SIZE :: size_of(PNG_IHDR)
#assert (PNG_IHDR_SIZE == 13)
PNG_Color_Value :: enum u8 {
Paletted = 0, // 1 << 0 = 1
Color = 1, // 1 << 1 = 2
Alpha = 2, // 1 << 2 = 4
}
PNG_Color_Type :: distinct bit_set[PNG_Color_Value; u8]
PNG_Interlace_Method :: enum u8 {
None = 0,
Adam7 = 1,
} }
/* /*
Functions to help with image buffer calculations Functions to help with image buffer calculations
*/ */
compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes := int(0)) -> (size: int) { compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes := int(0)) -> (size: int) {
size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height
return return
@@ -145,7 +258,6 @@ compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes
/* /*
For when you have an RGB(A) image, but want a particular channel. For when you have an RGB(A) image, but want a particular channel.
*/ */
Channel :: enum u8 { Channel :: enum u8 {
R = 1, R = 1,
G = 2, G = 2,
@@ -207,8 +319,7 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok
res.depth = img.depth res.depth = img.depth
res.pixels = t res.pixels = t
res.background = img.background res.background = img.background
res.metadata_ptr = img.metadata_ptr res.metadata = img.metadata
res.metadata_type = img.metadata_type
return res, true return res, true
} }
+29 -32
View File
@@ -1,9 +1,6 @@
//+ignore
package png
/* /*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>. Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-2 license. Made available under Odin's BSD-3 license.
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation. Jeroen van Rijn: Initial implementation.
@@ -11,8 +8,9 @@ package png
An example of how to use `load`. An example of how to use `load`.
*/ */
//+ignore
package png
import "core:compress"
import "core:image" import "core:image"
// import "core:image/png" // import "core:image/png"
import "core:bytes" import "core:bytes"
@@ -41,8 +39,8 @@ main :: proc() {
demo :: proc() { demo :: proc() {
file: string file: string
options := image.Options{} // {.return_metadata}; options := image.Options{.return_metadata}
err: compress.Error err: image.Error
img: ^image.Image img: ^image.Image
file = "../../../misc/logo-slim.png" file = "../../../misc/logo-slim.png"
@@ -53,32 +51,33 @@ demo :: proc() {
if err != nil { if err != nil {
fmt.printf("Trying to read PNG file %v returned %v\n", file, err) fmt.printf("Trying to read PNG file %v returned %v\n", file, err)
} else { } else {
v: ^Info
fmt.printf("Image: %vx%vx%v, %v-bit.\n", img.width, img.height, img.channels, img.depth) fmt.printf("Image: %vx%vx%v, %v-bit.\n", img.width, img.height, img.channels, img.depth)
if img.metadata_ptr != nil && img.metadata_type == Info {
v = (^Info)(img.metadata_ptr)
if v, ok := img.metadata.(^image.PNG_Info); ok {
// Handle ancillary chunks as you wish. // Handle ancillary chunks as you wish.
// We provide helper functions for a few types. // We provide helper functions for a few types.
for c in v.chunks { for c in v.chunks {
#partial switch c.header.type { #partial switch c.header.type {
case .tIME: case .tIME:
t, _ := core_time(c) if t, t_ok := core_time(c); t_ok {
fmt.printf("[tIME]: %v\n", t) fmt.printf("[tIME]: %v\n", t)
}
case .gAMA: case .gAMA:
fmt.printf("[gAMA]: %v\n", gamma(c)) if gama, gama_ok := gamma(c); gama_ok {
fmt.printf("[gAMA]: %v\n", gama)
}
case .pHYs: case .pHYs:
phys := phys(c) if phys, phys_ok := phys(c); phys_ok {
if phys.unit == .Meter { if phys.unit == .Meter {
xm := f32(img.width) / f32(phys.ppu_x) xm := f32(img.width) / f32(phys.ppu_x)
ym := f32(img.height) / f32(phys.ppu_y) ym := f32(img.height) / f32(phys.ppu_y)
dpi_x, dpi_y := phys_to_dpi(phys) dpi_x, dpi_y := phys_to_dpi(phys)
fmt.printf("[pHYs] Image resolution is %v x %v pixels per meter.\n", phys.ppu_x, phys.ppu_y) fmt.printf("[pHYs] Image resolution is %v x %v pixels per meter.\n", phys.ppu_x, phys.ppu_y)
fmt.printf("[pHYs] Image resolution is %v x %v DPI.\n", dpi_x, dpi_y) fmt.printf("[pHYs] Image resolution is %v x %v DPI.\n", dpi_x, dpi_y)
fmt.printf("[pHYs] Image dimensions are %v x %v meters.\n", xm, ym) fmt.printf("[pHYs] Image dimensions are %v x %v meters.\n", xm, ym)
} else { } else {
fmt.printf("[pHYs] x: %v, y: %v pixels per unknown unit.\n", phys.ppu_x, phys.ppu_y) fmt.printf("[pHYs] x: %v, y: %v pixels per unknown unit.\n", phys.ppu_x, phys.ppu_y)
}
} }
case .iTXt, .zTXt, .tEXt: case .iTXt, .zTXt, .tEXt:
res, ok_text := text(c) res, ok_text := text(c)
@@ -93,8 +92,7 @@ demo :: proc() {
case .bKGD: case .bKGD:
fmt.printf("[bKGD] %v\n", img.background) fmt.printf("[bKGD] %v\n", img.background)
case .eXIf: case .eXIf:
res, ok_exif := exif(c) if res, ok_exif := exif(c); ok_exif {
if ok_exif {
/* /*
Other than checking the signature and byte order, we don't handle Exif data. Other than checking the signature and byte order, we don't handle Exif data.
If you wish to interpret it, pass it to an Exif parser. If you wish to interpret it, pass it to an Exif parser.
@@ -102,20 +100,17 @@ demo :: proc() {
fmt.printf("[eXIf] %v\n", res) fmt.printf("[eXIf] %v\n", res)
} }
case .PLTE: case .PLTE:
plte, plte_ok := plte(c) if plte, plte_ok := plte(c); plte_ok {
if plte_ok {
fmt.printf("[PLTE] %v\n", plte) fmt.printf("[PLTE] %v\n", plte)
} else { } else {
fmt.printf("[PLTE] Error\n") fmt.printf("[PLTE] Error\n")
} }
case .hIST: case .hIST:
res, ok_hist := hist(c) if res, ok_hist := hist(c); ok_hist {
if ok_hist {
fmt.printf("[hIST] %v\n", res) fmt.printf("[hIST] %v\n", res)
} }
case .cHRM: case .cHRM:
res, ok_chrm := chrm(c) if res, ok_chrm := chrm(c); ok_chrm {
if ok_chrm {
fmt.printf("[cHRM] %v\n", res) fmt.printf("[cHRM] %v\n", res)
} }
case .sPLT: case .sPLT:
@@ -147,6 +142,8 @@ demo :: proc() {
} }
} }
fmt.printf("Done parsing metadata.\n")
if err == nil && .do_not_decompress_image not_in options && .info not_in options { if err == nil && .do_not_decompress_image not_in options && .info not_in options {
if ok := write_image_as_ppm("out.ppm", img); ok { if ok := write_image_as_ppm("out.ppm", img); ok {
fmt.println("Saved decoded image.") fmt.println("Saved decoded image.")
+47 -47
View File
@@ -1,5 +1,3 @@
package png
/* /*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>. Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-2 license. Made available under Odin's BSD-2 license.
@@ -10,6 +8,7 @@ package png
These are a few useful utility functions to work with PNG images. These are a few useful utility functions to work with PNG images.
*/ */
package png
import "core:image" import "core:image"
import "core:compress/zlib" import "core:compress/zlib"
@@ -34,15 +33,14 @@ destroy :: proc(img: ^Image) {
} }
bytes.buffer_destroy(&img.pixels) bytes.buffer_destroy(&img.pixels)
// Clean up Info.
free(img.metadata_ptr)
/* if v, ok := img.metadata.(^image.PNG_Info); ok {
We don't need to do anything for the individual chunks. for chunk in &v.chunks {
They're allocated on the temp allocator, as is info.chunks delete(chunk.data)
}
See read_chunk. delete(v.chunks)
*/ free(v)
}
free(img) free(img)
} }
@@ -50,46 +48,50 @@ destroy :: proc(img: ^Image) {
Chunk helpers Chunk helpers
*/ */
gamma :: proc(c: Chunk) -> f32 { gamma :: proc(c: image.PNG_Chunk) -> (res: f32, ok: bool) {
assert(c.header.type == .gAMA) if c.header.type != .gAMA || len(c.data) != size_of(gAMA) {
res := (^gAMA)(raw_data(c.data))^ return {}, false
when true {
// Returns the wrong result on old backend
// Fixed for -llvm-api
return f32(res.gamma_100k) / 100_000.0
} else {
return f32(u32(res.gamma_100k)) / 100_000.0
} }
gama := (^gAMA)(raw_data(c.data))^
return f32(gama.gamma_100k) / 100_000.0, true
} }
INCHES_PER_METER :: 1000.0 / 25.4 INCHES_PER_METER :: 1000.0 / 25.4
phys :: proc(c: Chunk) -> pHYs { phys :: proc(c: image.PNG_Chunk) -> (res: pHYs, ok: bool) {
assert(c.header.type == .pHYs) if c.header.type != .pHYs || len(c.data) != size_of(pHYs) {
res := (^pHYs)(raw_data(c.data))^ return {}, false
return res }
return (^pHYs)(raw_data(c.data))^, true
} }
phys_to_dpi :: proc(p: pHYs) -> (x_dpi, y_dpi: f32) { phys_to_dpi :: proc(p: pHYs) -> (x_dpi, y_dpi: f32) {
return f32(p.ppu_x) / INCHES_PER_METER, f32(p.ppu_y) / INCHES_PER_METER return f32(p.ppu_x) / INCHES_PER_METER, f32(p.ppu_y) / INCHES_PER_METER
} }
time :: proc(c: Chunk) -> tIME { time :: proc(c: image.PNG_Chunk) -> (res: tIME, ok: bool) {
assert(c.header.type == .tIME) if c.header.type != .tIME || len(c.data) != size_of(tIME) {
res := (^tIME)(raw_data(c.data))^ return {}, false
return res }
return (^tIME)(raw_data(c.data))^, true
} }
core_time :: proc(c: Chunk) -> (t: coretime.Time, ok: bool) { core_time :: proc(c: image.PNG_Chunk) -> (t: coretime.Time, ok: bool) {
png_time := time(c) if png_time, png_ok := time(c); png_ok {
using png_time using png_time
return coretime.datetime_to_time( return coretime.datetime_to_time(
int(year), int(month), int(day), int(year), int(month), int(day),
int(hour), int(minute), int(second), int(hour), int(minute), int(second),
) )
} else {
return {}, false
}
} }
text :: proc(c: Chunk) -> (res: Text, ok: bool) { text :: proc(c: image.PNG_Chunk) -> (res: Text, ok: bool) {
assert(len(c.data) == int(c.header.length))
#partial switch c.header.type { #partial switch c.header.type {
case .tEXt: case .tEXt:
ok = true ok = true
@@ -191,7 +193,7 @@ text_destroy :: proc(text: Text) {
delete(text.text) delete(text.text)
} }
iccp :: proc(c: Chunk) -> (res: iCCP, ok: bool) { iccp :: proc(c: image.PNG_Chunk) -> (res: iCCP, ok: bool) {
ok = true ok = true
fields := bytes.split_n(s=c.data, sep=[]u8{0}, n=3, allocator=context.temp_allocator) fields := bytes.split_n(s=c.data, sep=[]u8{0}, n=3, allocator=context.temp_allocator)
@@ -227,10 +229,8 @@ iccp_destroy :: proc(i: iCCP) {
} }
srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) { srgb :: proc(c: image.PNG_Chunk) -> (res: sRGB, ok: bool) {
ok = true if c.header.type != .sRGB || len(c.data) != size_of(sRGB_Rendering_Intent) {
if c.header.type != .sRGB || len(c.data) != 1 {
return {}, false return {}, false
} }
@@ -238,10 +238,10 @@ srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) {
if res.intent > max(sRGB_Rendering_Intent) { if res.intent > max(sRGB_Rendering_Intent) {
ok = false; return ok = false; return
} }
return return res, true
} }
plte :: proc(c: Chunk) -> (res: PLTE, ok: bool) { plte :: proc(c: image.PNG_Chunk) -> (res: PLTE, ok: bool) {
if c.header.type != .PLTE { if c.header.type != .PLTE {
return {}, false return {}, false
} }
@@ -255,7 +255,7 @@ plte :: proc(c: Chunk) -> (res: PLTE, ok: bool) {
return return
} }
splt :: proc(c: Chunk) -> (res: sPLT, ok: bool) { splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) {
if c.header.type != .sPLT { if c.header.type != .sPLT {
return {}, false return {}, false
} }
@@ -306,7 +306,7 @@ splt_destroy :: proc(s: sPLT) {
delete(s.name) delete(s.name)
} }
sbit :: proc(c: Chunk) -> (res: [4]u8, ok: bool) { sbit :: proc(c: image.PNG_Chunk) -> (res: [4]u8, ok: bool) {
/* /*
Returns [4]u8 with the significant bits in each channel. Returns [4]u8 with the significant bits in each channel.
A channel will contain zero if not applicable to the PNG color type. A channel will contain zero if not applicable to the PNG color type.
@@ -324,7 +324,7 @@ sbit :: proc(c: Chunk) -> (res: [4]u8, ok: bool) {
} }
hist :: proc(c: Chunk) -> (res: hIST, ok: bool) { hist :: proc(c: image.PNG_Chunk) -> (res: hIST, ok: bool) {
if c.header.type != .hIST { if c.header.type != .hIST {
return {}, false return {}, false
} }
@@ -346,7 +346,7 @@ hist :: proc(c: Chunk) -> (res: hIST, ok: bool) {
return return
} }
chrm :: proc(c: Chunk) -> (res: cHRM, ok: bool) { chrm :: proc(c: image.PNG_Chunk) -> (res: cHRM, ok: bool) {
ok = true ok = true
if c.header.length != size_of(cHRM_Raw) { if c.header.length != size_of(cHRM_Raw) {
return {}, false return {}, false
@@ -364,7 +364,7 @@ chrm :: proc(c: Chunk) -> (res: cHRM, ok: bool) {
return return
} }
exif :: proc(c: Chunk) -> (res: Exif, ok: bool) { exif :: proc(c: image.PNG_Chunk) -> (res: Exif, ok: bool) {
ok = true ok = true
+160 -171
View File
@@ -1,13 +1,12 @@
package png
/* /*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>. Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-2 license. Made available under Odin's BSD-3 license.
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation. Jeroen van Rijn: Initial implementation.
Ginger Bill: Cosmetic changes. Ginger Bill: Cosmetic changes.
*/ */
package png
import "core:compress" import "core:compress"
import "core:compress/zlib" import "core:compress/zlib"
@@ -21,11 +20,29 @@ import "core:io"
import "core:mem" import "core:mem"
import "core:intrinsics" import "core:intrinsics"
Error :: compress.Error /*
E_General :: compress.General_Error 67_108_864 pixels max by default.
E_PNG :: image.Error Maximum allowed dimensions are capped at 65535 * 65535.
E_Deflate :: compress.Deflate_Error */
MAX_DIMENSIONS :: min(#config(PNG_MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535)
/*
Limit chunk sizes.
By default: IDAT = 8k x 8k x 16-bits + 8k filter bytes.
*/
_MAX_IDAT_DEFAULT :: ( 8192 /* Width */ * 8192 /* Height */ * 2 /* 16-bit */) + 8192 /* Filter bytes */
_MAX_IDAT :: (65535 /* Width */ * 65535 /* Height */ * 2 /* 16-bit */) + 65535 /* Filter bytes */
MAX_IDAT_SIZE :: min(#config(PNG_MAX_IDAT_SIZE, _MAX_IDAT_DEFAULT), _MAX_IDAT)
/*
For chunks other than IDAT with a variable size like `zTXT` and `eXIf`,
limit their size to 16 MiB each by default. Max of 256 MiB each.
*/
MAX_CHUNK_SIZE :: min(#config(PNG_MAX_CHUNK_SIZE, 16_777_216), 268_435_456)
Error :: image.Error
Image :: image.Image Image :: image.Image
Options :: image.Options Options :: image.Options
@@ -34,95 +51,6 @@ Signature :: enum u64be {
PNG = 0x89 << 56 | 'P' << 48 | 'N' << 40 | 'G' << 32 | '\r' << 24 | '\n' << 16 | 0x1a << 8 | '\n', PNG = 0x89 << 56 | 'P' << 48 | 'N' << 40 | 'G' << 32 | '\r' << 24 | '\n' << 16 | 0x1a << 8 | '\n',
} }
Info :: struct {
header: IHDR,
chunks: [dynamic]Chunk,
}
Chunk_Header :: struct #packed {
length: u32be,
type: Chunk_Type,
}
Chunk :: struct #packed {
header: Chunk_Header,
data: []byte,
crc: u32be,
}
Chunk_Type :: enum u32be {
// IHDR must come first in a file
IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R',
// PLTE must precede the first IDAT chunk
PLTE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E',
bKGD = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D',
tRNS = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S',
IDAT = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T',
iTXt = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't',
tEXt = 't' << 24 | 'E' << 16 | 'X' << 8 | 't',
zTXt = 'z' << 24 | 'T' << 16 | 'X' << 8 | 't',
iCCP = 'i' << 24 | 'C' << 16 | 'C' << 8 | 'P',
pHYs = 'p' << 24 | 'H' << 16 | 'Y' << 8 | 's',
gAMA = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A',
tIME = 't' << 24 | 'I' << 16 | 'M' << 8 | 'E',
sPLT = 's' << 24 | 'P' << 16 | 'L' << 8 | 'T',
sRGB = 's' << 24 | 'R' << 16 | 'G' << 8 | 'B',
hIST = 'h' << 24 | 'I' << 16 | 'S' << 8 | 'T',
cHRM = 'c' << 24 | 'H' << 16 | 'R' << 8 | 'M',
sBIT = 's' << 24 | 'B' << 16 | 'I' << 8 | 'T',
/*
eXIf tags are not part of the core spec, but have been ratified
in v1.5.0 of the PNG Ext register.
We will provide unprocessed chunks to the caller if `.return_metadata` is set.
Applications are free to implement an Exif decoder.
*/
eXIf = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f',
// PNG files must end with IEND
IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D',
/*
XCode sometimes produces "PNG" files that don't adhere to the PNG spec.
We recognize them only in order to avoid doing further work on them.
Some tools like PNG Defry may be able to repair them, but we're not
going to reward Apple for producing proprietary broken files purporting
to be PNGs by supporting them.
*/
iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T',
CbGI = 'C' << 24 | 'b' << 16 | 'H' << 8 | 'I',
}
IHDR :: struct #packed {
width: u32be,
height: u32be,
bit_depth: u8,
color_type: Color_Type,
compression_method: u8,
filter_method: u8,
interlace_method: Interlace_Method,
}
IHDR_SIZE :: size_of(IHDR)
#assert (IHDR_SIZE == 13)
Color_Value :: enum u8 {
Paletted = 0, // 1 << 0 = 1
Color = 1, // 1 << 1 = 2
Alpha = 2, // 1 << 2 = 4
}
Color_Type :: distinct bit_set[Color_Value; u8]
Interlace_Method :: enum u8 {
None = 0,
Adam7 = 1,
}
Row_Filter :: enum u8 { Row_Filter :: enum u8 {
None = 0, None = 0,
Sub = 1, Sub = 1,
@@ -135,22 +63,22 @@ PLTE_Entry :: [3]u8
PLTE :: struct #packed { PLTE :: struct #packed {
entries: [256]PLTE_Entry, entries: [256]PLTE_Entry,
used: u16, used: u16,
} }
hIST :: struct #packed { hIST :: struct #packed {
entries: [256]u16, entries: [256]u16,
used: u16, used: u16,
} }
sPLT :: struct #packed { sPLT :: struct #packed {
name: string, name: string,
depth: u8, depth: u8,
entries: union { entries: union {
[][4]u8, [][4]u8,
[][4]u16, [][4]u16,
}, },
used: u16, used: u16,
} }
// Other chunks // Other chunks
@@ -223,14 +151,14 @@ Exif :: struct {
} }
iCCP :: struct { iCCP :: struct {
name: string, name: string,
profile: []u8, profile: []u8,
} }
sRGB_Rendering_Intent :: enum u8 { sRGB_Rendering_Intent :: enum u8 {
Perceptual = 0, Perceptual = 0,
Relative_colorimetric = 1, Relative_colorimetric = 1,
Saturation = 2, Saturation = 2,
Absolute_colorimetric = 3, Absolute_colorimetric = 3,
} }
@@ -245,16 +173,30 @@ ADAM7_Y_SPACING := []int{ 8,8,8,4,4,2,2 }
// Implementation starts here // Implementation starts here
read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { read_chunk :: proc(ctx: ^$C) -> (chunk: image.PNG_Chunk, err: Error) {
ch, e := compress.read_data(ctx, Chunk_Header) ch, e := compress.read_data(ctx, image.PNG_Chunk_Header)
if e != .None { if e != .None {
return {}, E_General.Stream_Too_Short return {}, compress.General_Error.Stream_Too_Short
} }
chunk.header = ch chunk.header = ch
/*
Sanity check chunk size
*/
#partial switch ch.type {
case .IDAT:
if ch.length > MAX_IDAT_SIZE {
return {}, image.PNG_Error.IDAT_Size_Too_Large
}
case:
if ch.length > MAX_CHUNK_SIZE {
return {}, image.PNG_Error.Invalid_Chunk_Length
}
}
chunk.data, e = compress.read_slice(ctx, int(ch.length)) chunk.data, e = compress.read_slice(ctx, int(ch.length))
if e != .None { if e != .None {
return {}, E_General.Stream_Too_Short return {}, compress.General_Error.Stream_Too_Short
} }
// Compute CRC over chunk type + data // Compute CRC over chunk type + data
@@ -264,39 +206,68 @@ read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) {
crc, e3 := compress.read_data(ctx, u32be) crc, e3 := compress.read_data(ctx, u32be)
if e3 != .None { if e3 != .None {
return {}, E_General.Stream_Too_Short return {}, compress.General_Error.Stream_Too_Short
} }
chunk.crc = crc chunk.crc = crc
if chunk.crc != u32be(computed_crc) { if chunk.crc != u32be(computed_crc) {
return {}, E_General.Checksum_Failed return {}, compress.General_Error.Checksum_Failed
} }
return chunk, nil return chunk, nil
} }
read_header :: proc(ctx: ^$C) -> (IHDR, Error) { copy_chunk :: proc(src: image.PNG_Chunk, allocator := context.allocator) -> (dest: image.PNG_Chunk, err: Error) {
if int(src.header.length) != len(src.data) {
return {}, .Invalid_Chunk_Length
}
dest.header = src.header
dest.crc = src.crc
dest.data = make([]u8, dest.header.length, allocator) or_return
copy(dest.data[:], src.data[:])
return
}
append_chunk :: proc(list: ^[dynamic]image.PNG_Chunk, src: image.PNG_Chunk, allocator := context.allocator) -> (err: Error) {
if int(src.header.length) != len(src.data) {
return .Invalid_Chunk_Length
}
c := copy_chunk(src, allocator) or_return
length := len(list)
append(list, c)
if len(list) != length + 1 {
// Resize during append failed.
return mem.Allocator_Error.Out_Of_Memory
}
return
}
read_header :: proc(ctx: ^$C) -> (image.PNG_IHDR, Error) {
c, e := read_chunk(ctx) c, e := read_chunk(ctx)
if e != nil { if e != nil {
return {}, e return {}, e
} }
header := (^IHDR)(raw_data(c.data))^ header := (^image.PNG_IHDR)(raw_data(c.data))^
// Validate IHDR // Validate IHDR
using header using header
if width == 0 || height == 0 { if width == 0 || height == 0 || u128(width) * u128(height) > MAX_DIMENSIONS {
return {}, E_PNG.Invalid_Image_Dimensions return {}, .Invalid_Image_Dimensions
} }
if compression_method != 0 { if compression_method != 0 {
return {}, E_General.Unknown_Compression_Method return {}, compress.General_Error.Unknown_Compression_Method
} }
if filter_method != 0 { if filter_method != 0 {
return {}, E_PNG.Unknown_Filter_Method return {}, .Unknown_Filter_Method
} }
if interlace_method != .None && interlace_method != .Adam7 { if interlace_method != .None && interlace_method != .Adam7 {
return {}, E_PNG.Unknown_Interlace_Method return {}, .Unknown_Interlace_Method
} }
@@ -314,7 +285,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) {
} }
} }
if !allowed { if !allowed {
return {}, E_PNG.Invalid_Color_Bit_Depth_Combo return {}, .Invalid_Color_Bit_Depth_Combo
} }
case 2, 4, 6: case 2, 4, 6:
/* /*
@@ -322,7 +293,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) {
Allowed bit depths: 8 and 16 Allowed bit depths: 8 and 16
*/ */
if bit_depth != 8 && bit_depth != 16 { if bit_depth != 8 && bit_depth != 16 {
return {}, E_PNG.Invalid_Color_Bit_Depth_Combo return {}, .Invalid_Color_Bit_Depth_Combo
} }
case 3: case 3:
/* /*
@@ -337,17 +308,17 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) {
} }
} }
if !allowed { if !allowed {
return {}, E_PNG.Invalid_Color_Bit_Depth_Combo return {}, .Invalid_Color_Bit_Depth_Combo
} }
case: case:
return {}, E_PNG.Unknown_Color_Type return {}, .Unknown_Color_Type
} }
return header, nil return header, nil
} }
chunk_type_to_name :: proc(type: ^Chunk_Type) -> string { chunk_type_to_name :: proc(type: ^image.PNG_Chunk_Type) -> string {
t := transmute(^u8)type t := transmute(^u8)type
return strings.string_from_ptr(t, 4) return strings.string_from_ptr(t, 4)
} }
@@ -377,7 +348,7 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont
return load_from_slice(data, options) return load_from_slice(data, options)
} else { } else {
img = new(Image) img = new(Image)
return img, E_General.File_Not_Found return img, compress.General_Error.File_Not_Found
} }
} }
@@ -391,7 +362,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
} }
if .alpha_drop_if_present in options && .alpha_add_if_missing in options { if .alpha_drop_if_present in options && .alpha_add_if_missing in options {
return {}, E_General.Incompatible_Options return {}, compress.General_Error.Incompatible_Options
} }
if .do_not_expand_channels in options { if .do_not_expand_channels in options {
@@ -402,27 +373,25 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
img = new(Image) img = new(Image)
} }
info := new(Info) info := new(image.PNG_Info)
img.metadata_ptr = info img.metadata = info
img.metadata_type = typeid_of(Info)
signature, io_error := compress.read_data(ctx, Signature) signature, io_error := compress.read_data(ctx, Signature)
if io_error != .None || signature != .PNG { if io_error != .None || signature != .PNG {
return img, E_PNG.Invalid_PNG_Signature return img, .Invalid_PNG_Signature
} }
idat: []u8 idat: []u8
idat_b: bytes.Buffer idat_b: bytes.Buffer
idat_length := u32be(0)
defer bytes.buffer_destroy(&idat_b) defer bytes.buffer_destroy(&idat_b)
c: Chunk idat_length := u64(0)
ch: Chunk_Header
c: image.PNG_Chunk
ch: image.PNG_Chunk_Header
e: io.Error e: io.Error
header: IHDR header: image.PNG_IHDR
info.chunks.allocator = context.temp_allocator
// State to ensure correct chunk ordering. // State to ensure correct chunk ordering.
seen_ihdr := false; first := true seen_ihdr := false; first := true
@@ -433,7 +402,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
seen_iend := false seen_iend := false
_plte := PLTE{} _plte := PLTE{}
trns := Chunk{} trns := image.PNG_Chunk{}
final_image_channels := 0 final_image_channels := 0
@@ -443,16 +412,16 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
// Peek at next chunk's length and type. // Peek at next chunk's length and type.
// TODO: Some streams may not provide seek/read_at // TODO: Some streams may not provide seek/read_at
ch, e = compress.peek_data(ctx, Chunk_Header) ch, e = compress.peek_data(ctx, image.PNG_Chunk_Header)
if e != .None { if e != .None {
return img, E_General.Stream_Too_Short return img, compress.General_Error.Stream_Too_Short
} }
// name := chunk_type_to_name(&ch.type); // Only used for debug prints during development. // name := chunk_type_to_name(&ch.type); // Only used for debug prints during development.
#partial switch ch.type { #partial switch ch.type {
case .IHDR: case .IHDR:
if seen_ihdr || !first { if seen_ihdr || !first {
return {}, E_PNG.IHDR_Not_First_Chunk return {}, .IHDR_Not_First_Chunk
} }
seen_ihdr = true seen_ihdr = true
@@ -481,14 +450,14 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
} }
if img.channels == 0 || img.depth == 0 { if img.channels == 0 || img.depth == 0 {
return {}, E_PNG.IHDR_Corrupt return {}, .IHDR_Corrupt
} }
img.width = int(header.width) img.width = int(header.width)
img.height = int(header.height) img.height = int(header.height)
using header using header
h := IHDR{ h := image.PNG_IHDR{
width = width, width = width,
height = height, height = height,
bit_depth = bit_depth, bit_depth = bit_depth,
@@ -498,28 +467,30 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
interlace_method = interlace_method, interlace_method = interlace_method,
} }
info.header = h info.header = h
case .PLTE: case .PLTE:
seen_plte = true seen_plte = true
// PLTE must appear before IDAT and can't appear for color types 0, 4. // PLTE must appear before IDAT and can't appear for color types 0, 4.
ct := transmute(u8)info.header.color_type ct := transmute(u8)info.header.color_type
if seen_idat || ct == 0 || ct == 4 { if seen_idat || ct == 0 || ct == 4 {
return img, E_PNG.PLTE_Encountered_Unexpectedly return img, .PLTE_Encountered_Unexpectedly
} }
c = read_chunk(ctx) or_return c = read_chunk(ctx) or_return
if c.header.length % 3 != 0 || c.header.length > 768 { if c.header.length % 3 != 0 || c.header.length > 768 {
return img, E_PNG.PLTE_Invalid_Length return img, .PLTE_Invalid_Length
} }
plte_ok: bool plte_ok: bool
_plte, plte_ok = plte(c) _plte, plte_ok = plte(c)
if !plte_ok { if !plte_ok {
return img, E_PNG.PLTE_Invalid_Length return img, .PLTE_Invalid_Length
} }
if .return_metadata in options { if .return_metadata in options {
append(&info.chunks, c) append_chunk(&info.chunks, c) or_return
} }
case .IDAT: case .IDAT:
// If we only want image metadata and don't want the pixel data, we can early out. // If we only want image metadata and don't want the pixel data, we can early out.
if .return_metadata not_in options && .do_not_decompress_image in options { if .return_metadata not_in options && .do_not_decompress_image in options {
@@ -528,11 +499,11 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
} }
// There must be at least 1 IDAT, contiguous if more. // There must be at least 1 IDAT, contiguous if more.
if seen_idat { if seen_idat {
return img, E_PNG.IDAT_Must_Be_Contiguous return img, .IDAT_Must_Be_Contiguous
} }
if idat_length > 0 { if idat_length > 0 {
return img, E_PNG.IDAT_Must_Be_Contiguous return img, .IDAT_Must_Be_Contiguous
} }
next := ch.type next := ch.type
@@ -540,22 +511,29 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
c = read_chunk(ctx) or_return c = read_chunk(ctx) or_return
bytes.buffer_write(&idat_b, c.data) bytes.buffer_write(&idat_b, c.data)
idat_length += c.header.length idat_length += u64(c.header.length)
ch, e = compress.peek_data(ctx, Chunk_Header) if idat_length > MAX_IDAT_SIZE {
return {}, image.PNG_Error.IDAT_Size_Too_Large
}
ch, e = compress.peek_data(ctx, image.PNG_Chunk_Header)
if e != .None { if e != .None {
return img, E_General.Stream_Too_Short return img, compress.General_Error.Stream_Too_Short
} }
next = ch.type next = ch.type
} }
idat = bytes.buffer_to_bytes(&idat_b) idat = bytes.buffer_to_bytes(&idat_b)
if int(idat_length) != len(idat) { if int(idat_length) != len(idat) {
return {}, E_PNG.IDAT_Corrupt return {}, .IDAT_Corrupt
} }
seen_idat = true seen_idat = true
case .IEND: case .IEND:
c = read_chunk(ctx) or_return c = read_chunk(ctx) or_return
seen_iend = true seen_iend = true
case .bKGD: case .bKGD:
// TODO: Make sure that 16-bit bKGD + tRNS chunks return u16 instead of u16be // TODO: Make sure that 16-bit bKGD + tRNS chunks return u16 instead of u16be
@@ -563,14 +541,14 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
c = read_chunk(ctx) or_return c = read_chunk(ctx) or_return
seen_bkgd = true seen_bkgd = true
if .return_metadata in options { if .return_metadata in options {
append(&info.chunks, c) append_chunk(&info.chunks, c) or_return
} }
ct := transmute(u8)info.header.color_type ct := transmute(u8)info.header.color_type
switch ct { switch ct {
case 3: // Indexed color case 3: // Indexed color
if c.header.length != 1 { if c.header.length != 1 {
return {}, E_PNG.BKGD_Invalid_Length return {}, .BKGD_Invalid_Length
} }
col := _plte.entries[c.data[0]] col := _plte.entries[c.data[0]]
img.background = [3]u16{ img.background = [3]u16{
@@ -580,26 +558,27 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
} }
case 0, 4: // Grayscale, with and without Alpha case 0, 4: // Grayscale, with and without Alpha
if c.header.length != 2 { if c.header.length != 2 {
return {}, E_PNG.BKGD_Invalid_Length return {}, .BKGD_Invalid_Length
} }
col := u16(mem.slice_data_cast([]u16be, c.data[:])[0]) col := u16(mem.slice_data_cast([]u16be, c.data[:])[0])
img.background = [3]u16{col, col, col} img.background = [3]u16{col, col, col}
case 2, 6: // Color, with and without Alpha case 2, 6: // Color, with and without Alpha
if c.header.length != 6 { if c.header.length != 6 {
return {}, E_PNG.BKGD_Invalid_Length return {}, .BKGD_Invalid_Length
} }
col := mem.slice_data_cast([]u16be, c.data[:]) col := mem.slice_data_cast([]u16be, c.data[:])
img.background = [3]u16{u16(col[0]), u16(col[1]), u16(col[2])} img.background = [3]u16{u16(col[0]), u16(col[1]), u16(col[2])}
} }
case .tRNS: case .tRNS:
c = read_chunk(ctx) or_return c = read_chunk(ctx) or_return
if .Alpha in info.header.color_type { if .Alpha in info.header.color_type {
return img, E_PNG.TRNS_Encountered_Unexpectedly return img, .TRNS_Encountered_Unexpectedly
} }
if .return_metadata in options { if .return_metadata in options {
append(&info.chunks, c) append_chunk(&info.chunks, c) or_return
} }
/* /*
@@ -622,20 +601,20 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
} }
} }
trns = c trns = c
case .iDOT, .CbGI: case .iDOT, .CbGI:
/* /*
iPhone PNG bastardization that doesn't adhere to spec with broken IDAT chunk. iPhone PNG bastardization that doesn't adhere to spec with broken IDAT chunk.
We're not going to add support for it. If you have the misfortunte of coming We're not going to add support for it. If you have the misfortunte of coming
across one of these files, use a utility to defry it.s across one of these files, use a utility to defry it.
*/ */
return img, E_PNG.PNG_Does_Not_Adhere_to_Spec return img, .Image_Does_Not_Adhere_to_Spec
case: case:
// Unhandled type // Unhandled type
c = read_chunk(ctx) or_return c = read_chunk(ctx) or_return
if .return_metadata in options { if .return_metadata in options {
// NOTE: Chunk cata is currently allocated on the temp allocator. append_chunk(&info.chunks, c) or_return
append(&info.chunks, c)
} }
first = false first = false
@@ -648,7 +627,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
} }
if !seen_idat { if !seen_idat {
return img, E_PNG.IDAT_Missing return img, .IDAT_Missing
} }
/* /*
@@ -685,7 +664,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
buf_len := len(buf.buf) buf_len := len(buf.buf)
if expected_size != buf_len { if expected_size != buf_len {
return {}, E_PNG.IDAT_Corrupt return {}, .IDAT_Corrupt
} }
/* /*
@@ -752,7 +731,9 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
// We need to create a new image buffer // We need to create a new image buffer
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8) dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
t := bytes.Buffer{} t := bytes.Buffer{}
resize(&t.buf, dest_raw_size) if !resize(&t.buf, dest_raw_size) {
return {}, mem.Allocator_Error.Out_Of_Memory
}
i := 0; j := 0 i := 0; j := 0
@@ -831,7 +812,9 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
// We need to create a new image buffer // We need to create a new image buffer
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16) dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16)
t := bytes.Buffer{} t := bytes.Buffer{}
resize(&t.buf, dest_raw_size) if !resize(&t.buf, dest_raw_size) {
return {}, mem.Allocator_Error.Out_Of_Memory
}
p16 := mem.slice_data_cast([]u16, temp.buf[:]) p16 := mem.slice_data_cast([]u16, temp.buf[:])
o16 := mem.slice_data_cast([]u16, t.buf[:]) o16 := mem.slice_data_cast([]u16, t.buf[:])
@@ -1028,7 +1011,9 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
// We need to create a new image buffer // We need to create a new image buffer
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8) dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
t := bytes.Buffer{} t := bytes.Buffer{}
resize(&t.buf, dest_raw_size) if !resize(&t.buf, dest_raw_size) {
return {}, mem.Allocator_Error.Out_Of_Memory
}
p := mem.slice_data_cast([]u8, temp.buf[:]) p := mem.slice_data_cast([]u8, temp.buf[:])
o := mem.slice_data_cast([]u8, t.buf[:]) o := mem.slice_data_cast([]u8, t.buf[:])
@@ -1524,7 +1509,7 @@ defilter_16 :: proc(params: ^Filter_Params) -> (ok: bool) {
return return
} }
defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, options: Options) -> (err: compress.Error) { defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^image.PNG_IHDR, options: Options) -> (err: Error) {
input := bytes.buffer_to_bytes(filter_bytes) input := bytes.buffer_to_bytes(filter_bytes)
width := int(header.width) width := int(header.width)
height := int(header.height) height := int(header.height)
@@ -1535,7 +1520,9 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
bytes_per_channel := depth == 16 ? 2 : 1 bytes_per_channel := depth == 16 ? 2 : 1
num_bytes := compute_buffer_size(width, height, channels, depth == 16 ? 16 : 8) num_bytes := compute_buffer_size(width, height, channels, depth == 16 ? 16 : 8)
resize(&img.pixels.buf, num_bytes) if !resize(&img.pixels.buf, num_bytes) {
return mem.Allocator_Error.Out_Of_Memory
}
filter_ok: bool filter_ok: bool
@@ -1560,7 +1547,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
} }
if !filter_ok { if !filter_ok {
// Caller will destroy buffer for us. // Caller will destroy buffer for us.
return E_PNG.Unknown_Filter_Method return .Unknown_Filter_Method
} }
} else { } else {
/* /*
@@ -1575,7 +1562,9 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
if x > 0 && y > 0 { if x > 0 && y > 0 {
temp: bytes.Buffer temp: bytes.Buffer
temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8) temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8)
resize(&temp.buf, temp_len) if !resize(&temp.buf, temp_len) {
return mem.Allocator_Error.Out_Of_Memory
}
params := Filter_Params{ params := Filter_Params{
src = input, src = input,
@@ -1598,7 +1587,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
if !filter_ok { if !filter_ok {
// Caller will destroy buffer for us. // Caller will destroy buffer for us.
return E_PNG.Unknown_Filter_Method return .Unknown_Filter_Method
} }
t := temp.buf[:] t := temp.buf[:]
+68 -1
View File
@@ -99,6 +99,57 @@ Rect :: struct {
bottom: i32, bottom: i32,
} }
Dev_Mode_A :: struct {
device_name: [32]u8,
spec_version: u16,
driver_version: u16,
size: u16,
driver_extra: u16,
fields: u32,
using _: struct #raw_union {
// Printer only fields.
using _: struct {
orientation: i16,
paper_size: i16,
paper_length: i16,
paper_width: i16,
scale: i16,
copies: i16,
default_source: i16,
print_quality: i16,
},
// Display only fields.
using _: struct {
position: Point,
display_orientation: u32,
display_fixed_output: u32,
},
},
color: i16,
duplex: i16,
y_resolution: i16,
tt_option: i16,
collate: i16,
form_name: [32]u8,
log_pixels: u16,
bits_per_pel: u32,
pels_width: u32,
pels_height: u32,
using _: struct #raw_union {
display_flags: u32,
nup: u32,
},
display_frequency: u32,
icm_method: u32,
icm_intent: u32,
media_type: u32,
dither_type: u32,
reserved_1: u32,
reserved_2: u32,
panning_width: u32,
panning_height: u32,
}
Filetime :: struct { Filetime :: struct {
lo, hi: u32, lo, hi: u32,
} }
@@ -364,6 +415,9 @@ MAPVK_VK_TO_CHAR :: 2
MAPVK_VSC_TO_VK_EX :: 3 MAPVK_VSC_TO_VK_EX :: 3
//WinUser.h //WinUser.h
ENUM_CURRENT_SETTINGS :: u32(4294967295) // (DWORD)-1
ENUM_REGISTRY_SETTINGS :: u32(4294967294) // (DWORD)-2
VK_LBUTTON :: 0x01 VK_LBUTTON :: 0x01
VK_RBUTTON :: 0x02 VK_RBUTTON :: 0x02
VK_CANCEL :: 0x03 VK_CANCEL :: 0x03
@@ -877,7 +931,20 @@ FILE_GENERIC_EXECUTE :: 0x20000000
FILE_GENERIC_WRITE :: 0x40000000 FILE_GENERIC_WRITE :: 0x40000000
FILE_GENERIC_READ :: 0x80000000 FILE_GENERIC_READ :: 0x80000000
FILE_APPEND_DATA :: 0x0004 FILE_READ_DATA :: 0x0001
FILE_LIST_DIRECTORY :: 0x0001
FILE_WRITE_DATA :: 0x0002
FILE_ADD_FILE :: 0x0002
FILE_APPEND_DATA :: 0x0004
FILE_ADD_SUBDIRECTORY :: 0x0004
FILE_CREATE_PIPE_INSTANCE :: 0x0004
FILE_READ_EA :: 0x0008
FILE_WRITE_EA :: 0x0010
FILE_EXECUTE :: 0x0020
FILE_TRAVERSE :: 0x0020
FILE_DELETE_CHILD :: 0x0040
FILE_READ_ATTRIBUTES :: 0x0080
FILE_WRITE_ATTRIBUTES :: 0x0100
STD_INPUT_HANDLE :: -10 STD_INPUT_HANDLE :: -10
STD_OUTPUT_HANDLE :: -11 STD_OUTPUT_HANDLE :: -11
+2
View File
@@ -106,6 +106,8 @@ foreign kernel32 {
bytes_returned: ^u32, overlapped: ^Overlapped, bytes_returned: ^u32, overlapped: ^Overlapped,
completion: rawptr) -> Bool --- completion: rawptr) -> Bool ---
@(link_name="GetOverlappedResult") get_overlapped_result :: proc(file: Handle, overlapped: ^Overlapped, number_of_bytes_transferred: ^u32, wait: Bool) -> Bool ---
@(link_name="WideCharToMultiByte") wide_char_to_multi_byte :: proc(code_page: u32, flags: u32, @(link_name="WideCharToMultiByte") wide_char_to_multi_byte :: proc(code_page: u32, flags: u32,
wchar_str: Wstring, wchar: i32, wchar_str: Wstring, wchar: i32,
multi_str: cstring, multi: i32, multi_str: cstring, multi: i32,
+2
View File
@@ -201,6 +201,8 @@ foreign user32 {
@(link_name="MapVirtualKeyExA") map_virtual_key_ex_a :: proc(code, map_type: u32, hkl: HKL) -> u32 --- @(link_name="MapVirtualKeyExA") map_virtual_key_ex_a :: proc(code, map_type: u32, hkl: HKL) -> u32 ---
@(link_name="EnumDisplayMonitors") enum_display_monitors :: proc(hdc: Hdc, rect: ^Rect, enum_proc: Monitor_Enum_Proc, lparam: Lparam) -> bool --- @(link_name="EnumDisplayMonitors") enum_display_monitors :: proc(hdc: Hdc, rect: ^Rect, enum_proc: Monitor_Enum_Proc, lparam: Lparam) -> bool ---
@(link_name="EnumDisplaySettingsA") enum_display_settings_a :: proc(device_name: cstring, mode_number: u32, mode: ^Dev_Mode_A) -> Bool ---
} }
@(default_calling_convention = "std") @(default_calling_convention = "std")
+2
View File
@@ -6,5 +6,7 @@ foreign import "system:winmm.lib"
@(default_calling_convention = "std") @(default_calling_convention = "std")
foreign winmm { foreign winmm {
@(link_name="timeBeginPeriod") time_begin_period :: proc(period: u32) -> u32 ---
@(link_name="timeGetTime") time_get_time :: proc() -> u32 --- @(link_name="timeGetTime") time_get_time :: proc() -> u32 ---
} }
+4
View File
@@ -0,0 +1,4 @@
@echo off
pushd ..
odin run image
popd
+24 -27
View File
@@ -1,5 +1,3 @@
package test_core_image
/* /*
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>. Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-3 license. Made available under Odin's BSD-3 license.
@@ -9,6 +7,7 @@ package test_core_image
A test suite for PNG. A test suite for PNG.
*/ */
package test_core_image
import "core:testing" import "core:testing"
@@ -64,7 +63,7 @@ PNG_Test :: struct {
file: string, file: string,
tests: []struct { tests: []struct {
options: image.Options, options: image.Options,
expected_error: compress.Error, expected_error: image.Error,
dims: PNG_Dims, dims: PNG_Dims,
hash: u32, hash: u32,
}, },
@@ -1198,37 +1197,37 @@ Corrupt_PNG_Tests := []PNG_Test{
{ {
"xs1n0g01", // signature byte 1 MSBit reset to zero "xs1n0g01", // signature byte 1 MSBit reset to zero
{ {
{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
}, },
}, },
{ {
"xs2n0g01", // signature byte 2 is a 'Q' "xs2n0g01", // signature byte 2 is a 'Q'
{ {
{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
}, },
}, },
{ {
"xs4n0g01", // signature byte 4 lowercase "xs4n0g01", // signature byte 4 lowercase
{ {
{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
}, },
}, },
{ {
"xs7n0g01", // 7th byte a space instead of control-Z "xs7n0g01", // 7th byte a space instead of control-Z
{ {
{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
}, },
}, },
{ {
"xcrn0g04", // added cr bytes "xcrn0g04", // added cr bytes
{ {
{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
}, },
}, },
{ {
"xlfn0g04", // added lf bytes "xlfn0g04", // added lf bytes
{ {
{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
}, },
}, },
{ {
@@ -1240,37 +1239,37 @@ Corrupt_PNG_Tests := []PNG_Test{
{ {
"xc1n0g08", // color type 1 "xc1n0g08", // color type 1
{ {
{Default, I_Error.Unknown_Color_Type, {}, 0x_0000_0000}, {Default, .Unknown_Color_Type, {}, 0x_0000_0000},
}, },
}, },
{ {
"xc9n2c08", // color type 9 "xc9n2c08", // color type 9
{ {
{Default, I_Error.Unknown_Color_Type, {}, 0x_0000_0000}, {Default, .Unknown_Color_Type, {}, 0x_0000_0000},
}, },
}, },
{ {
"xd0n2c08", // bit-depth 0 "xd0n2c08", // bit-depth 0
{ {
{Default, I_Error.Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, {Default, .Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000},
}, },
}, },
{ {
"xd3n2c08", // bit-depth 3 "xd3n2c08", // bit-depth 3
{ {
{Default, I_Error.Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, {Default, .Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000},
}, },
}, },
{ {
"xd9n2c08", // bit-depth 99 "xd9n2c08", // bit-depth 99
{ {
{Default, I_Error.Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, {Default, .Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000},
}, },
}, },
{ {
"xdtn0g01", // missing IDAT chunk "xdtn0g01", // missing IDAT chunk
{ {
{Default, I_Error.IDAT_Missing, {}, 0x_0000_0000}, {Default, .IDAT_Missing, {}, 0x_0000_0000},
}, },
}, },
{ {
@@ -1505,19 +1504,17 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) {
passed &= test.hash == hash passed &= test.hash == hash
if .return_metadata in test.options { if .return_metadata in test.options {
v: ^png.Info
if img.metadata_ptr != nil && img.metadata_type == png.Info { if v, ok := img.metadata.(^image.PNG_Info); ok {
v = (^png.Info)(img.metadata_ptr)
for c in v.chunks { for c in v.chunks {
#partial switch(c.header.type) { #partial switch(c.header.type) {
case .gAMA: case .gAMA:
switch(file.file) { switch(file.file) {
case "pp0n2c16", "pp0n6a08": case "pp0n2c16", "pp0n6a08":
gamma := png.gamma(c) gamma, gamma_ok := png.gamma(c)
expected_gamma := f32(1.0) expected_gamma := f32(1.0)
error = fmt.tprintf("%v test %v gAMA is %v, expected %v.", file.file, count, gamma, expected_gamma) error = fmt.tprintf("%v test %v gAMA is %v, expected %v.", file.file, count, gamma, expected_gamma)
expect(t, gamma == expected_gamma, error) expect(t, gamma == expected_gamma && gamma_ok, error)
} }
case .PLTE: case .PLTE:
switch(file.file) { switch(file.file) {
@@ -1557,25 +1554,25 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) {
expect(t, expected_chrm == chrm && chrm_ok, error) expect(t, expected_chrm == chrm && chrm_ok, error)
} }
case .pHYs: case .pHYs:
phys := png.phys(c) phys, phys_ok := png.phys(c)
phys_err := "%v test %v cHRM is %v, expected %v." phys_err := "%v test %v cHRM is %v, expected %v."
switch (file.file) { switch (file.file) {
case "cdfn2c08": case "cdfn2c08":
expected_phys := png.pHYs{ppu_x = 1, ppu_y = 4, unit = .Unknown} expected_phys := png.pHYs{ppu_x = 1, ppu_y = 4, unit = .Unknown}
error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
expect(t, expected_phys == phys, error) expect(t, expected_phys == phys && phys_ok, error)
case "cdhn2c08": case "cdhn2c08":
expected_phys := png.pHYs{ppu_x = 4, ppu_y = 1, unit = .Unknown} expected_phys := png.pHYs{ppu_x = 4, ppu_y = 1, unit = .Unknown}
error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
expect(t, expected_phys == phys, error) expect(t, expected_phys == phys && phys_ok, error)
case "cdsn2c08": case "cdsn2c08":
expected_phys := png.pHYs{ppu_x = 1, ppu_y = 1, unit = .Unknown} expected_phys := png.pHYs{ppu_x = 1, ppu_y = 1, unit = .Unknown}
error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
expect(t, expected_phys == phys, error) expect(t, expected_phys == phys && phys_ok, error)
case "cdun2c08": case "cdun2c08":
expected_phys := png.pHYs{ppu_x = 1000, ppu_y = 1000, unit = .Meter} expected_phys := png.pHYs{ppu_x = 1000, ppu_y = 1000, unit = .Meter}
error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
expect(t, expected_phys == phys, error) expect(t, expected_phys == phys && phys_ok, error)
} }
case .hIST: case .hIST:
hist, hist_ok := png.hist(c) hist, hist_ok := png.hist(c)
@@ -1589,7 +1586,7 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) {
expect(t, hist.used == 256 && hist_ok, error) expect(t, hist.used == 256 && hist_ok, error)
} }
case .tIME: case .tIME:
png_time := png.time(c) png_time, png_time_ok := png.time(c)
time_err := "%v test %v tIME was %v, expected %v." time_err := "%v test %v tIME was %v, expected %v."
expected_time: png.tIME expected_time: png.tIME
@@ -1610,7 +1607,7 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) {
} }
error = fmt.tprintf(time_err, file.file, count, png_time, expected_time) error = fmt.tprintf(time_err, file.file, count, png_time, expected_time)
expect(t, png_time == expected_time, error) expect(t, png_time == expected_time && png_time_ok, error)
error = fmt.tprintf(time_core_err, file.file, count, core_time, expected_core) error = fmt.tprintf(time_core_err, file.file, count, core_time, expected_core)
expect(t, core_time == expected_core && core_time_ok, error) expect(t, core_time == expected_core && core_time_ok, error)