Quellcode durchsuchen

Merge branch 'odin-lang:master' into patch-2

FourteenBrush vor 2 Tagen
Ursprung
Commit
eb7218eff0

+ 142 - 2
core/image/common.odin

@@ -64,8 +64,16 @@ Image_Metadata :: union #shared_nil {
 	^QOI_Info,
 	^TGA_Info,
 	^BMP_Info,
+	^JPEG_Info,
 }
 
+Exif :: struct {
+	byte_order: enum {
+		little_endian,
+		big_endian,
+	},
+	data: []u8 `fmt:"-"`,
+}
 
 
 /*
@@ -112,8 +120,7 @@ Image_Option:
 
 	`.alpha_drop_if_present`
 		If the image has an alpha channel, drop it.
-		You may want to use `.alpha_
-		tiply` in this case.
+		You may want to use `.alpha_premultiply` in this case.
 
 		NOTE: For PNG, this also skips handling of the tRNS chunk, if present,
 		unless you select `alpha_premultiply`.
@@ -163,6 +170,7 @@ Error :: union #shared_nil {
 	PNG_Error,
 	QOI_Error,
 	BMP_Error,
+	JPEG_Error,
 
 	compress.Error,
 	compress.General_Error,
@@ -575,6 +583,138 @@ TGA_Info :: struct {
 	extension: Maybe(TGA_Extension),
 }
 
+
+/*
+	JPEG-specific
+*/
+JFIF_Magic := [?]byte{0x4A, 0x46, 0x49, 0x46} // "JFIF"
+JFXX_Magic := [?]byte{0x4A, 0x46, 0x58, 0x58} // "JFXX"
+Exif_Magic := [?]byte{0x45, 0x78, 0x69, 0x66} // "Exif"
+
+JPEG_Error :: enum {
+	None = 0,
+	Duplicate_SOI_Marker,
+	Invalid_JFXX_Extension_Code,
+	Encountered_SOS_Before_SOF,
+	Invalid_Quantization_Table_Precision,
+	Invalid_Quantization_Table_Index,
+	Invalid_Huffman_Coefficient_Type,
+	Invalid_Huffman_Table_Index,
+	Unsupported_Frame_Type,
+	Invalid_Frame_Bit_Depth_Combo,
+	Invalid_Sampling_Factor,
+	Unsupported_12_Bit_Depth,
+	Multiple_SOS_Markers,
+	Encountered_RST_Marker_Outside_ECS,
+	Extra_Data_After_SOS, // Image seemed to have decoded okay, but there's more data after SOS
+	Invalid_Thumbnail_Size,
+	Huffman_Symbols_Exceeds_Max,
+}
+
+JFIF_Unit :: enum byte {
+	None = 0,
+	Dots_Per_Inch = 1,
+	Dots_Per_Centimeter = 2,
+}
+
+JFIF_APP0 :: struct {
+	version: u16be,
+	x_density: u16be,
+	y_density: u16be,
+	units: JFIF_Unit,
+	x_thumbnail: u8,
+	y_thumbnail: u8,
+	greyscale_thumbnail: bool,
+	thumbnail: []RGB_Pixel `fmt:"-"`,
+}
+
+JFXX_APP0 :: struct {
+	extension_code: JFXX_Extension_Code,
+	x_thumbnail: u8,
+	y_thumbnail: u8,
+	thumbnail: []byte `fmt:"-"`,
+}
+
+JFXX_Extension_Code :: enum u8 {
+	Thumbnail_JPEG = 0x10,
+	Thumbnail_1_Byte_Palette = 0x11,
+	Thumbnail_3_Byte_RGB = 0x13,
+}
+
+JPEG_Marker :: enum u8 {
+	SOF0  = 0xC0,
+	SOF1  = 0xC1,
+	SOF2  = 0xC2,
+	SOF3  = 0xC3,
+	DHT   = 0xC4,
+	SOF5  = 0xC5,
+	SOF6  = 0xC6,
+	SOF7  = 0xC7,
+	JPG   = 0xC8,
+	SOF9  = 0xC9,
+	SOF10 = 0xCA,
+	SOF11 = 0xCB,
+	DAC   = 0xCC,
+	SOF13 = 0xCD,
+	SOF14 = 0xCE,
+	SOF15 = 0xCF,
+	RST0  = 0xD0,
+	RST1  = 0xD1,
+	RST2  = 0xD2,
+	RST3  = 0xD3,
+	RST4  = 0xD4,
+	RST5  = 0xD5,
+	RST6  = 0xD6,
+	RST7  = 0xD7,
+	SOI   = 0xD8,
+	EOI   = 0xD9,
+	SOS   = 0xDA,
+	DQT   = 0xDB,
+	DNL   = 0xDC,
+	DRI   = 0xDD,
+	DHP   = 0xDE,
+	EXP   = 0xDF,
+	APP0  = 0xE0,
+	APP1  = 0xE1,
+	APP2  = 0xE2,
+	APP3  = 0xE3,
+	APP4  = 0xE4,
+	APP5  = 0xE5,
+	APP6  = 0xE6,
+	APP7  = 0xE7,
+	APP8  = 0xE8,
+	APP9  = 0xE9,
+	APP10 = 0xEA,
+	APP11 = 0xEB,
+	APP12 = 0xEC,
+	APP13 = 0xED,
+	APP14 = 0xEE,
+	APP15 = 0xEF,
+	JPG0  = 0xF0,
+	JPG1  = 0xF1,
+	JPG2  = 0xF2,
+	JPG3  = 0xF3,
+	JPG4  = 0xF4,
+	JPG5  = 0xF5,
+	JPG6  = 0xF6,
+	JPG7  = 0xF7,
+	JPG8  = 0xF8,
+	JPG9  = 0xF9,
+	JPG10 = 0xFA,
+	JPG11 = 0xFB,
+	JPG12 = 0xFC,
+	JPG13 = 0xFD,
+	COM   = 0xFE,
+	TEM   = 0x01,
+}
+
+JPEG_Info :: struct {
+	jfif_app0: Maybe(JFIF_APP0),
+	jfxx_app0: Maybe(JFXX_APP0),
+	comments: [dynamic]string,
+	exif: [dynamic]Exif,
+}
+
 // Function to help with image buffer calculations
 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

+ 1 - 1
core/image/general.odin

@@ -147,7 +147,7 @@ which_bytes :: proc(data: []byte) -> Which_File_Type {
 		return .JPEG
 	case s[:3] == "\xff\xd8\xff":
 		switch s[3] {
-		case 0xdb, 0xee, 0xe1, 0xe0:
+		case 0xdb, 0xee, 0xe1, 0xe0, 0xfe, 0xed:
 			return .JPEG
 		}
 		switch {

+ 1016 - 0
core/image/jpeg/jpeg.odin

@@ -0,0 +1,1016 @@
+package jpeg
+
+import "core:bytes"
+import "core:compress"
+import "core:math"
+import "core:mem"
+import "core:image"
+import "core:slice"
+import "core:strings"
+
+Image :: image.Image
+Error :: image.Error
+Options :: image.Options
+
+HUFFMAN_MAX_SYMBOLS :: 176
+HUFFMAN_MAX_BITS  :: 16
+// 768 bytes of 24-bit RGB values.
+THUMBNAIL_PALETTE_SIZE :: 768
+BLOCK_SIZE :: 8
+COEFFICIENT_COUNT :: BLOCK_SIZE * BLOCK_SIZE
+SEGMENT_MAX_SIZE :: 65533
+
+Coefficient :: enum u8 {
+	DC,
+	AC,
+}
+
+Component :: enum u8 {
+	Y = 1,
+	Cb = 2,
+	Cr = 3,
+}
+
+Huffman_Table :: struct {
+	symbols: [HUFFMAN_MAX_SYMBOLS]byte,
+	codes: [HUFFMAN_MAX_SYMBOLS]u32,
+	offsets: [HUFFMAN_MAX_BITS + 1]byte,
+}
+
+Quantization_Table :: [COEFFICIENT_COUNT]u16be
+
+Color_Component :: struct {
+	dc_table_idx: u8,
+	ac_table_idx: u8,
+	quantization_table_idx: u8,
+	v_sampling_factor: int,
+	h_sampling_factor: int,
+}
+
+// 8x8 block of pixels
+Block :: [Component][COEFFICIENT_COUNT]i16
+
+@(private="file")
+zigzag := [?]byte{
+    0,   1,  8, 16,  9,  2,  3, 10,
+    17, 24, 32, 25, 18, 11,  4,  5,
+    12, 19, 26, 33, 40, 48, 41, 34,
+    27, 20, 13,  6,  7, 14, 21, 28,
+    35, 42, 49, 56, 57, 50, 43, 36,
+    29, 22, 15, 23, 30, 37, 44, 51,
+    58, 59, 52, 45, 38, 31, 39, 46,
+    53, 60, 61, 54, 47, 55, 62, 63,
+}
+
+@(optimization_mode="favor_size", private="file")
+refill_msb :: #force_inline proc(z: ^compress.Context_Memory_Input, width := i8(48)) {
+	refill := u64(width)
+	b      := u64(0)
+
+	if z.num_bits > refill {
+		return
+	}
+
+	for {
+		if len(z.input_data) != 0 {
+			b = u64(z.input_data[0])
+
+			if len(z.input_data) > 1 && b == 0xFF {
+				next := u64(z.input_data[1])
+
+				if next == 0x00 {
+					// 0x00 is used as a stuffing to indicate that the 0xFF is part of the data and not
+					// the beginning of a marker
+					z.input_data = z.input_data[2:]
+				} else if next >= cast(u64)image.JPEG_Marker.RST0 && next <= cast(u64)image.JPEG_Marker.RST7 {
+					// Skip any RSTn markers if we encounter them
+					if len(z.input_data) > 2 {
+						b = u64(z.input_data[2])
+						z.input_data = z.input_data[3:]
+					} else {
+						b = 0
+					}
+				}
+			} else {
+				z.input_data = z.input_data[1:]
+			}
+		} else {
+			b = 0
+		}
+
+		z.code_buffer |= ((b << 56) >> u8(z.num_bits))
+		z.num_bits += 8
+		if z.num_bits > refill {
+			break
+		}
+	}
+}
+
+@(optimization_mode="favor_size", private="file")
+consume_bits_msb :: #force_inline proc(z: ^compress.Context_Memory_Input, width: u8) {
+	z.code_buffer <<= width
+	z.num_bits -= u64(width)
+}
+
+@(private="file")
+byte_align :: #force_inline proc(z: ^compress.Context_Memory_Input) {
+	skip := z.num_bits % 8
+	consume_bits_msb(z, cast(u8)skip)
+}
+
+@(optimization_mode="favor_size", private="file")
+peek_bits_msb :: #force_inline proc(z: ^compress.Context_Memory_Input, width: u8) -> u32 {
+	if z.num_bits < u64(width) {
+		refill_msb(z)
+	}
+	return u32((z.code_buffer &~ (max(u64) >> width)) >> (64 - width))
+}
+
+@(optimization_mode="favor_size", private="file")
+read_bits_msb :: #force_inline proc(z: ^compress.Context_Memory_Input, width: u8) -> u32 {
+	k := #force_inline peek_bits_msb(z, width)
+	#force_inline consume_bits_msb(z, width)
+	return k
+}
+
+load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
+	ctx := &compress.Context_Memory_Input{
+		input_data = data,
+	}
+
+	img, err = load_from_context(ctx, options, allocator)
+	return img, err
+}
+
+@(private="file")
+get_symbol :: proc(ctx: ^$C, huffman_table: Huffman_Table) -> byte {
+	possible_code: u32 = 0
+
+	for i in 0..<HUFFMAN_MAX_BITS {
+		bit := read_bits_msb(ctx, 1)
+		possible_code = (possible_code << 1) | bit
+
+		for j := huffman_table.offsets[i]; j < huffman_table.offsets[i + 1]; j += 1 {
+			if possible_code == huffman_table.codes[j] {
+				return huffman_table.symbols[j]
+			}
+		}
+	}
+
+	return 0
+}
+
+load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
+	context.allocator = allocator
+	options := options
+
+	// Precalculate IDCT scaling factors
+	m0 := 2.0 * math.cos_f32(1.0 / 16.0 * 2.0 * math.PI)
+	m1 := 2.0 * math.cos_f32(2.0 / 16.0 * 2.0 * math.PI)
+	m3 := 2.0 * math.cos_f32(2.0 / 16.0 * 2.0 * math.PI)
+	m5 := 2.0 * math.cos_f32(3.0 / 16.0 * 2.0 * math.PI)
+	m2 := m0 - m5
+	m4 := m0 + m5
+
+	s0 := math.cos_f32(0.0 / 16.0 * math.PI) / math.sqrt_f32(8.0)
+	s1 := math.cos_f32(1.0 / 16.0 * math.PI) / 2.0
+	s2 := math.cos_f32(2.0 / 16.0 * math.PI) / 2.0
+	s3 := math.cos_f32(3.0 / 16.0 * math.PI) / 2.0
+	s4 := math.cos_f32(4.0 / 16.0 * math.PI) / 2.0
+	s5 := math.cos_f32(5.0 / 16.0 * math.PI) / 2.0
+	s6 := math.cos_f32(6.0 / 16.0 * math.PI) / 2.0
+	s7 := math.cos_f32(7.0 / 16.0 * math.PI) / 2.0
+
+	if .info in options {
+		options += {.return_metadata, .do_not_decompress_image}
+		options -= {.info}
+	}
+
+	if .return_header in options && .return_metadata in options {
+		options -= {.return_header}
+	}
+
+	first := compress.read_u8(ctx) or_return
+	soi := cast(image.JPEG_Marker)compress.read_u8(ctx) or_return
+	if first != 0xFF && soi != .SOI {
+		return img, .Invalid_Signature
+	}
+
+	img = new(Image) or_return
+	img.which = .JPEG
+
+	expect_EOI := false
+	zero_based_components := false
+	huffman: [Coefficient][4]Huffman_Table
+	quantization: [4]Quantization_Table
+	color_components: [Component]Color_Component
+	restart_interval: int
+	// Image width and height in MCUs
+	mcu_width: int
+	mcu_height: int
+	// Image width and height in blocks
+	block_width: int
+	block_height: int
+	blocks: []Block
+	defer delete(blocks)
+
+	loop: for {
+		first = compress.read_u8(ctx) or_return
+		if first == 0xFF {
+			marker := cast(image.JPEG_Marker)compress.read_u8(ctx) or_return
+			if expect_EOI && marker != .EOI {
+				return img, .Extra_Data_After_SOS
+			}
+			#partial switch marker {
+			case cast(image.JPEG_Marker)0xFF:
+				// If we encounter multiple FF bytes then just skip them
+				continue
+			case .SOI:
+				return img, .Duplicate_SOI_Marker
+			case .APP0:
+				ident := make([dynamic]byte, 0, 16, context.temp_allocator) or_return
+				length := cast(int)((compress.read_data(ctx, u16be) or_return) - 2)
+				for {
+					b := compress.read_u8(ctx) or_return
+					if b == 0x00 {
+						break
+					}
+					append(&ident, b) or_return
+				}
+				if slice.equal(ident[:], image.JFIF_Magic[:]) {
+					if length != 14 {
+						// Malformed APP0. Skip it
+						compress.read_slice(ctx, length - len(ident) - 1) or_return
+						continue
+					}
+
+					version := compress.read_data(ctx, u16be) or_return
+					units := cast(image.JFIF_Unit)(compress.read_u8(ctx) or_return)
+					x_density := compress.read_data(ctx, u16be) or_return
+					y_density := compress.read_data(ctx, u16be) or_return
+					x_thumbnail := cast(int)compress.read_u8(ctx) or_return
+					y_thumbnail := cast(int)compress.read_u8(ctx) or_return
+					thumbnail: []image.RGB_Pixel
+
+					if x_thumbnail * y_thumbnail != 0 {
+						greyscale_thumbnail := false
+						thumbnail_size := x_thumbnail * y_thumbnail * 3
+						// According to the JFIF spec, the thumbnail should always be made of RGB pixels.
+						// But some jpegs encode single-channel thumbnails.
+						if thumbnail_size != length - 14 && thumbnail_size / 3 == length - 14 {
+							thumbnail_size = x_thumbnail * y_thumbnail
+							greyscale_thumbnail = true
+						} else {
+							return img, .Invalid_Thumbnail_Size
+						}
+						thumb_pixels := slice.reinterpret([]image.RGB_Pixel, compress.read_slice_from_memory(ctx, x_thumbnail * y_thumbnail) or_return)
+
+						if .return_metadata in options {
+							thumbnail = make([]image.RGB_Pixel, x_thumbnail * y_thumbnail) or_return
+							copy(thumbnail, thumb_pixels)
+
+							info: ^image.JPEG_Info
+							if img.metadata == nil {
+								info = new(image.JPEG_Info) or_return
+							} else {
+								info = img.metadata.(^image.JPEG_Info)
+							}
+							info.jfif_app0 = image.JFIF_APP0{
+								version,
+								x_density,
+								y_density,
+								units,
+								cast(u8)x_thumbnail,
+								cast(u8)y_thumbnail,
+								greyscale_thumbnail,
+								thumbnail,
+							}
+							img.metadata = info
+						}
+					}
+				} else if slice.equal(ident[:], image.JFXX_Magic[:]) {
+					extension_code := cast(image.JFXX_Extension_Code)compress.read_u8(ctx) or_return
+					thumbnail: []byte
+
+					switch extension_code {
+					// We return the JPEG-compressed bytes for this type of thumbnail.
+					// It's up to the user if they want to decode it by checking the extension code
+					// and calling image.load() on the thumbnail.
+					// Not sure where to document that though, maybe it's better if the thumbnail is always raw pixel data.
+					case .Thumbnail_JPEG:
+						// +1 for the NUL byte
+						thumbnail_len := length - (size_of(image.JFXX_Magic) + 1 + size_of(image.JFXX_Extension_Code))
+						thumbnail_jpeg := compress.read_slice(ctx, thumbnail_len) or_return
+
+						if .return_metadata in options {
+							thumbnail = make([]byte, thumbnail_len) or_return
+							copy(thumbnail, thumbnail_jpeg)
+
+							info: ^image.JPEG_Info
+							if img.metadata == nil {
+								info = new(image.JPEG_Info) or_return
+							} else {
+								info = img.metadata.(^image.JPEG_Info)
+							}
+							info.jfxx_app0 = image.JFXX_APP0{
+								extension_code,
+								0,
+								0,
+								thumbnail,
+							}
+							img.metadata = info
+						}
+					case .Thumbnail_3_Byte_RGB:
+						x_thumbnail := cast(int)compress.read_u8(ctx) or_return
+						y_thumbnail := cast(int)compress.read_u8(ctx) or_return
+						pixels := compress.read_slice(ctx, x_thumbnail * y_thumbnail * 3) or_return
+
+						if .return_metadata in options {
+							thumbnail = make([]byte, x_thumbnail * y_thumbnail * 3) or_return
+							copy(thumbnail, pixels)
+
+							info: ^image.JPEG_Info
+							if img.metadata == nil {
+								info = new(image.JPEG_Info) or_return
+							} else {
+								info = img.metadata.(^image.JPEG_Info)
+							}
+							info.jfxx_app0 = image.JFXX_APP0{
+								extension_code,
+								cast(u8)x_thumbnail,
+								cast(u8)y_thumbnail,
+								thumbnail,
+							}
+							img.metadata = info
+						}
+					case .Thumbnail_1_Byte_Palette: // NOTE(illusionman1212): NOT TESTED. Couldn't find a jpeg to test this with.
+						x_thumbnail := cast(int)compress.read_u8(ctx) or_return
+						y_thumbnail := cast(int)compress.read_u8(ctx) or_return
+						palette := slice.reinterpret([]image.RGB_Pixel, compress.read_slice(ctx, THUMBNAIL_PALETTE_SIZE / 3) or_return)
+						old_pixels := compress.read_slice(ctx, x_thumbnail * y_thumbnail) or_return
+
+						if .return_metadata in options {
+							pixels := make([]byte, x_thumbnail * y_thumbnail * 3) or_return
+							for i in 0..<x_thumbnail*y_thumbnail {
+								pixel := palette[old_pixels[i]]
+								pixels[i] = pixel.r
+								pixels[i + 1] = pixel.g
+								pixels[i + 2] = pixel.b
+							}
+
+							info: ^image.JPEG_Info
+							if img.metadata == nil {
+								info = new(image.JPEG_Info) or_return
+							} else {
+								info = img.metadata.(^image.JPEG_Info)
+							}
+							info.jfxx_app0 = image.JFXX_APP0{
+								extension_code,
+								cast(u8)x_thumbnail,
+								cast(u8)y_thumbnail,
+								pixels,
+							}
+							img.metadata = info
+						}
+					case:
+						return img, .Invalid_JFXX_Extension_Code
+					}
+				} else {
+					// - 1 for the NUL byte
+					compress.read_slice(ctx, length - len(ident) - 1) or_return
+					continue
+				}
+			case .APP1: // Metadata
+				length := cast(int)((compress.read_data(ctx, u16be) or_return) - 2)
+				if .return_metadata not_in options {
+					compress.read_slice(ctx, length) or_return
+					continue
+				}
+				info: ^image.JPEG_Info
+				if img.metadata == nil {
+					info = new(image.JPEG_Info) or_return
+				} else {
+					info = img.metadata.(^image.JPEG_Info)
+				}
+
+				ident := make([dynamic]byte, 0, 16, context.temp_allocator) or_return
+				for {
+					b := compress.read_u8(ctx) or_return
+					if b == 0x00 {
+						break
+					}
+					append(&ident, b) or_return
+				}
+
+				if slice.equal(ident[:], image.Exif_Magic[:]) {
+					// Padding byte according to section 4.7.2.2 in Exif spec 3.0
+					compress.read_u8(ctx) or_return
+
+					exif: image.Exif
+					peek := compress.peek_data(ctx, [4]byte) or_return
+					if peek[0] == 'M' && peek[1] == 'M' {
+						exif.byte_order = .big_endian
+						if peek[2] != 0 || peek[3] != 42 {
+							// - 2 for the NUL byte and padding byte
+							compress.read_slice(ctx, length - len(ident) - 2) or_return
+							continue
+						}
+					} else if peek[0] == 'I' && peek[1] == 'I' {
+						exif.byte_order = .little_endian
+						if peek[2] != 42 || peek[3] != 0 {
+							compress.read_slice(ctx, length - len(ident) - 2) or_return
+							continue
+						}
+					} else {
+						// If we can't determine the endianness then this Exif data is likely a continuation of the previous
+						// APP1 Exif data
+
+						// We only treat it as such if a previous Exif entry exists and its data length is the max
+						if len(info.exif) > 0 && len(info.exif[len(info.exif) - 1].data) == SEGMENT_MAX_SIZE - len(ident) - 2 {
+							exif.byte_order = info.exif[len(info.exif) - 1].byte_order
+						} else {
+							compress.read_slice(ctx, length - len(ident) - 2) or_return
+							continue
+						}
+					}
+
+					// - 2 for the NUL byte and padding byte
+					data := compress.read_slice(ctx, length - len(ident) - 2) or_return
+					exif.data = make([]byte, len(data)) or_return
+					copy(exif.data, data)
+
+					append(&info.exif, exif) or_return
+					img.metadata = info
+				} else {
+					// - 1 for the NUL byte
+					compress.read_slice(ctx, length - len(ident) - 1) or_return
+					continue
+				}
+			case .COM:
+				length := (compress.read_data(ctx, u16be) or_return) - 2
+				comment := string(compress.read_slice(ctx, cast(int)length) or_return)
+				if .return_metadata in options {
+					if info, ok := img.metadata.(^image.JPEG_Info); ok {
+						append(&info.comments, strings.clone(comment)) or_return
+					}
+				}
+			case .DQT:
+				length := cast(int)(compress.read_data(ctx, u16be) or_return) - 2
+
+				for length > 0 {
+					precision_and_index := compress.read_u8(ctx) or_return
+					precision := precision_and_index >> 4
+					index := precision_and_index & 0xF
+
+					if precision != 0 && precision != 1 {
+						return img, .Invalid_Quantization_Table_Precision
+					}
+
+					if index < 0 || index > 3 {
+						return img, .Invalid_Quantization_Table_Index
+					}
+
+					// When precision is 0, we read 64 u8s.
+					// when it's 1, we read 64 u16s.
+					table_bytes := 64
+					if precision == 1 {
+						table_bytes = 128
+						table := compress.read_slice(ctx, table_bytes) or_return
+						for v, i in slice.reinterpret([]u16be, table) {
+							quantization[index][i] = v
+						}
+					} else {
+						table := compress.read_slice(ctx, table_bytes) or_return
+						for v, i in table {
+							quantization[index][i] = cast(u16be)v
+						}
+					}
+
+					length -= table_bytes + 1
+				}
+			case .DHT:
+				length := (compress.read_data(ctx, u16be) or_return) - 2
+
+				for length > 0 {
+					type_index := compress.read_u8(ctx) or_return
+					type := cast(Coefficient)((type_index >> 4) & 0xF)
+					index := type_index & 0xF
+
+					if type != .DC && type != .AC {
+						return img, .Invalid_Huffman_Coefficient_Type
+					}
+
+					if index < 0 || index > 3 {
+						return img, .Invalid_Huffman_Table_Index
+					}
+
+					lengths := compress.read_slice(ctx, HUFFMAN_MAX_BITS) or_return
+					num_symbols: u8 = 0
+					for length, i in lengths {
+						num_symbols += length
+						huffman[type][index].offsets[i + 1] = num_symbols
+					}
+
+					if num_symbols > HUFFMAN_MAX_SYMBOLS {
+						return img, .Huffman_Symbols_Exceeds_Max
+					}
+
+					symbols := compress.read_slice(ctx, cast(int)num_symbols) or_return
+					copy(huffman[type][index].symbols[:], symbols)
+
+					length -= cast(u16be)(1 + HUFFMAN_MAX_BITS + num_symbols)
+
+					code: u32 = 0
+					for i in 0..<HUFFMAN_MAX_BITS {
+						for j := huffman[type][index].offsets[i]; j < huffman[type][index].offsets[i + 1]; j += 1 {
+							huffman[type][index].codes[j] = code
+							code += 1
+						}
+						code <<= 1
+					}
+				}
+			case .EOI:
+				break loop
+			case .DRI:
+				// Length
+				compress.read_data(ctx, u16be) or_return
+				restart_interval = cast(int)compress.read_data(ctx, u16be) or_return
+			case .RST0..=.RST7: // Handled by the bit reader. These shouldn't appear outside the entropy coded stream.
+				return img, .Encountered_RST_Marker_Outside_ECS
+			case .SOF0, .SOF1: // Baseline sequential DCT, and extended sequential DCT
+				if img.channels != 0 {
+					return img, .Multiple_SOS_Markers
+				}
+
+				// Length
+				compress.read_data(ctx, u16be) or_return
+				precision := compress.read_u8(ctx) or_return
+				height := compress.read_data(ctx, u16be) or_return
+				width := compress.read_data(ctx, u16be) or_return
+				components := compress.read_u8(ctx) or_return
+				img.width = cast(int)width
+				img.height = cast(int)height
+				img.depth = cast(int)precision
+				img.channels = cast(int)components
+
+				// TODO: 12-bit precision is valid too but we don't support it.
+				if precision == 12 {
+					return img, .Unsupported_12_Bit_Depth
+				}
+				if precision != 8 {
+					return img, .Invalid_Frame_Bit_Depth_Combo
+				}
+
+				// TODO: spec allows for the height to be 0 on the condition that a DNL marker MUST exist to define
+				// how many lines in the frame we have.
+				// ISO/IEC 10918-1: 1993.
+				// Section B.2.5
+				if img.width == 0 || img.height == 0 {
+					return img, .Invalid_Image_Dimensions
+				}
+
+				if u128(img.width) * u128(img.height) > image.MAX_DIMENSIONS {
+					return img, .Image_Dimensions_Too_Large
+				}
+
+				// TODO: Some JPEGs use CMYK as the color model which means there will be 4 components
+				if components != 1 && components != 3 {
+					return img, .Invalid_Number_Of_Channels
+				}
+
+				mcu_width = (img.width + 7) / BLOCK_SIZE
+				mcu_height = (img.height + 7) / BLOCK_SIZE
+				block_width = mcu_width
+				block_height = mcu_height
+
+				for _ in 0..<components {
+					id := cast(Component)compress.read_u8(ctx) or_return
+
+					if id == Component(0) {
+						zero_based_components = true
+					}
+
+					if zero_based_components {
+						id += Component(1)
+					}
+
+					// TODO: while others that use CMYK have these IDs 67, 77, 89, 75 which are CMYK in ASCII
+					// TODO: even more weird ids. 82, 71, 66 which is RGB in ASCII
+					if id < .Y || id > .Cr {
+						return img, .Image_Does_Not_Adhere_to_Spec
+					}
+
+					h_v_factors := compress.read_u8(ctx) or_return
+					horizontal_sampling := h_v_factors >> 4
+					vertical_sampling := h_v_factors & 0xF
+
+					// TODO: spec says the range for the sampling factors is 1-4
+					// We only support 1,2 for now.
+					if horizontal_sampling < 1 || horizontal_sampling > 2 {
+						return img, .Invalid_Sampling_Factor
+					}
+					if vertical_sampling < 1 || vertical_sampling > 2 {
+						return img, .Invalid_Sampling_Factor
+					}
+
+					if id == .Y {
+						if horizontal_sampling == 2 && mcu_width % 2 == 1 {
+							block_width += 1
+						}
+						if vertical_sampling == 2 && mcu_height % 2 == 1 {
+							block_height += 1
+						}
+					} else {
+						if horizontal_sampling != 1 && vertical_sampling != 1 {
+							return img, .Invalid_Sampling_Factor
+						}
+					}
+
+					quantization_table_idx := compress.read_u8(ctx) or_return
+
+					if quantization_table_idx < 0 || quantization_table_idx > 3 {
+						return img, .Invalid_Quantization_Table_Index
+					}
+
+					color_components[id].quantization_table_idx = quantization_table_idx
+					color_components[id].v_sampling_factor = cast(int)vertical_sampling
+					color_components[id].h_sampling_factor = cast(int)horizontal_sampling
+				}
+			case .SOF2: // Progressive DCT
+				unimplemented("SOF2")
+			case .SOF3: // Lossless (sequential)
+				fallthrough
+			case .SOF5: // Differential sequential DCT
+				fallthrough
+			case .SOF6: // Differential progressive DCT
+				fallthrough
+			case .SOF7: // Differential lossless (sequential)
+				fallthrough
+			case .SOF9: // Extended sequential DCT, Arithmetic coding
+				fallthrough
+			case .SOF10: // Progressive DCT, Arithmetic coding
+				fallthrough
+			case .SOF11: // Lossless (sequential), Arithmetic coding
+				fallthrough
+			case .SOF13: // Differential sequential DCT, Arithmetic coding
+				fallthrough
+			case .SOF14: // Differential progressive DCT, Arithmetic coding
+				fallthrough
+			case .SOF15: // Differential lossless (sequential), Arithmetic coding
+				return img, .Unsupported_Frame_Type
+			case .SOS:
+				if img.channels == 0 && img.depth == 0 && img.width == 0 && img.height == 0 {
+					return img, .Encountered_SOS_Before_SOF
+				}
+
+				if .do_not_decompress_image in options {
+					return img, nil
+				}
+
+				// Length
+				compress.read_data(ctx, u16be) or_return
+				num_components := compress.read_u8(ctx) or_return
+				if num_components != 1 && num_components != 3 {
+					return img, .Invalid_Number_Of_Channels
+				}
+
+				for _ in 0..<num_components {
+					component_id := cast(Component)compress.read_u8(ctx) or_return
+					if zero_based_components {
+						component_id += Component(1)
+					}
+					if component_id < .Y || component_id > .Cr {
+						return img, .Image_Does_Not_Adhere_to_Spec
+					}
+
+					// high 4 is DC, low 4 is AC
+					coefficient_indices := compress.read_u8(ctx) or_return
+					dc_table_idx := coefficient_indices >> 4
+					ac_table_idx := coefficient_indices & 0xF
+
+					if (dc_table_idx < 0 || dc_table_idx > 3) || (ac_table_idx < 0 || ac_table_idx > 3) {
+						return img, .Invalid_Huffman_Table_Index
+					}
+
+					color_components[component_id].dc_table_idx = dc_table_idx
+					color_components[component_id].ac_table_idx = ac_table_idx
+				}
+				// TODO: These aren't used for sequential DCT, only progressive and lossless.
+				Ss := compress.read_u8(ctx) or_return
+				_ = Ss
+				Se := compress.read_u8(ctx) or_return
+				_ = Se
+				Ah_Al := compress.read_u8(ctx) or_return
+				_ = Ah_Al
+
+				blocks = make([]Block, block_height * block_width) or_return
+
+				previous_dc: [Component]i16
+
+				luma_v_sampling_factor := color_components[.Y].v_sampling_factor
+				luma_h_sampling_factor := color_components[.Y].h_sampling_factor
+
+				restart_interval *= luma_v_sampling_factor * luma_h_sampling_factor
+				#no_bounds_check for y := 0; y < mcu_height; y += luma_v_sampling_factor {
+					for x := 0; x < mcu_width; x += luma_h_sampling_factor {
+						blk := y * block_width + x
+
+						if restart_interval != 0 && blk % restart_interval == 0 {
+							previous_dc[.Y] = 0
+							previous_dc[.Cb] = 0
+							previous_dc[.Cr] = 0
+							byte_align(ctx)
+						}
+						for c in 1..=img.channels {
+							c := cast(Component)c
+							for v in 0..<color_components[c].v_sampling_factor {
+							h_loop:
+								for h in 0..<color_components[c].h_sampling_factor {
+									mcu := &blocks[(y + v) * block_width + (h + x)][c]
+									dc_table := huffman[.DC][color_components[c].dc_table_idx]
+									ac_table := huffman[.AC][color_components[c].ac_table_idx]
+									quantization_table := quantization[color_components[c].quantization_table_idx]
+
+									length := get_symbol(ctx, dc_table)
+
+									if length > 11 {
+										return img, .Corrupt
+									}
+
+									dc_coeff := cast(i16)read_bits_msb(ctx, length)
+
+									if length != 0 && dc_coeff < (1 << (length - 1)) {
+										dc_coeff -= (1 << length) - 1
+									}
+									mcu[0] = (dc_coeff + previous_dc[c]) * cast(i16)quantization_table[0]
+									previous_dc[c] = dc_coeff + previous_dc[c]
+
+									for i := 1; i < COEFFICIENT_COUNT; i += 1 {
+										// High nibble is amount of 0s to skip.
+										// Low nibble is length of coeff.
+										symbol := get_symbol(ctx, ac_table)
+
+										// Special symbol used to indicate
+										// that the rest of the MCU is filled with 0s
+										if symbol == 0x00 {
+											continue h_loop
+										}
+
+										amnt_zeros := int(symbol >> 4)
+										ac_coeff_len := symbol & 0xF
+										ac_coeff: i16 = 0
+
+										if i + amnt_zeros >= COEFFICIENT_COUNT || ac_coeff_len > 10 {
+											return img, .Corrupt
+										}
+
+										i += amnt_zeros
+
+										ac_coeff = cast(i16)read_bits_msb(ctx, ac_coeff_len)
+										if ac_coeff < (1 << (ac_coeff_len - 1)) {
+											ac_coeff -= (1 << ac_coeff_len) - 1
+										}
+
+										mcu[zigzag[i]] = ac_coeff * cast(i16)quantization_table[i]
+									}
+								}
+							}
+						}
+
+						for c in 1..=img.channels {
+							c := cast(Component)c
+
+							for v in 0..<color_components[c].v_sampling_factor {
+								for h in 0..< color_components[c].h_sampling_factor {
+									mcu := &blocks[(y + v) * block_width + (x + h)][c]
+									for i in 0..<BLOCK_SIZE {
+										g0 := cast(f32)mcu[0 * BLOCK_SIZE + i] * s0
+										g1 := cast(f32)mcu[4 * BLOCK_SIZE + i] * s4
+										g2 := cast(f32)mcu[2 * BLOCK_SIZE + i] * s2
+										g3 := cast(f32)mcu[6 * BLOCK_SIZE + i] * s6
+										g4 := cast(f32)mcu[5 * BLOCK_SIZE + i] * s5
+										g5 := cast(f32)mcu[1 * BLOCK_SIZE + i] * s1
+										g6 := cast(f32)mcu[7 * BLOCK_SIZE + i] * s7
+										g7 := cast(f32)mcu[3 * BLOCK_SIZE + i] * s3
+
+										f4 := g4 - g7
+										f5 := g5 + g6
+										f6 := g5 - g6
+										f7 := g4 + g7
+
+										e0 := g0
+										e1 := g1
+										e2 := g2 - g3
+										e3 := g2 + g3
+										e4 := f4
+										e5 := f5 - f7
+										e6 := f6
+										e7 := f5 + f7
+										e8 := f4 + f6
+
+										d0 := e0
+										d1 := e1
+										d2 := e2 * m1
+										d3 := e3
+										d4 := e4 * m2
+										d5 := e5 * m3
+										d6 := e6 * m4
+										d7 := e7
+										d8 := e8 * m5
+
+										c0 := d0 + d1
+										c1 := d0 - d1
+										c2 := d2 - d3
+										c3 := d3
+										c4 := d4 + d8
+										c5 := d5 + d7
+										c6 := d6 - d8
+										c7 := d7
+										c8 := c5 - c6
+
+										b0 := c0 + c3
+										b1 := c1 + c2
+										b2 := c1 - c2
+										b3 := c0 - c3
+										b4 := c4 - c8
+										b5 := c8
+										b6 := c6 - c7
+										b7 := c7
+
+										mcu[0 * BLOCK_SIZE + i] = cast(i16)(b0 + b7)
+										mcu[1 * BLOCK_SIZE + i] = cast(i16)(b1 + b6)
+										mcu[2 * BLOCK_SIZE + i] = cast(i16)(b2 + b5)
+										mcu[3 * BLOCK_SIZE + i] = cast(i16)(b3 + b4)
+										mcu[4 * BLOCK_SIZE + i] = cast(i16)(b3 - b4)
+										mcu[5 * BLOCK_SIZE + i] = cast(i16)(b2 - b5)
+										mcu[6 * BLOCK_SIZE + i] = cast(i16)(b1 - b6)
+										mcu[7 * BLOCK_SIZE + i] = cast(i16)(b0 - b7)
+									}
+
+									for i in 0..<BLOCK_SIZE {
+										g0 := cast(f32)mcu[i * BLOCK_SIZE + 0] * s0
+										g1 := cast(f32)mcu[i * BLOCK_SIZE + 4] * s4
+										g2 := cast(f32)mcu[i * BLOCK_SIZE + 2] * s2
+										g3 := cast(f32)mcu[i * BLOCK_SIZE + 6] * s6
+										g4 := cast(f32)mcu[i * BLOCK_SIZE + 5] * s5
+										g5 := cast(f32)mcu[i * BLOCK_SIZE + 1] * s1
+										g6 := cast(f32)mcu[i * BLOCK_SIZE + 7] * s7
+										g7 := cast(f32)mcu[i * BLOCK_SIZE + 3] * s3
+
+										f4 := g4 - g7
+										f5 := g5 + g6
+										f6 := g5 - g6
+										f7 := g4 + g7
+
+										e0 := g0
+										e1 := g1
+										e2 := g2 - g3
+										e3 := g2 + g3
+										e4 := f4
+										e5 := f5 - f7
+										e6 := f6
+										e7 := f5 + f7
+										e8 := f4 + f6
+
+										d0 := e0
+										d1 := e1
+										d2 := e2 * m1
+										d3 := e3
+										d4 := e4 * m2
+										d5 := e5 * m3
+										d6 := e6 * m4
+										d7 := e7
+										d8 := e8 * m5
+
+										c0 := d0 + d1
+										c1 := d0 - d1
+										c2 := d2 - d3
+										c3 := d3
+										c4 := d4 + d8
+										c5 := d5 + d7
+										c6 := d6 - d8
+										c7 := d7
+										c8 := c5 - c6
+
+										b0 := c0 + c3
+										b1 := c1 + c2
+										b2 := c1 - c2
+										b3 := c0 - c3
+										b4 := c4 - c8
+										b5 := c8
+										b6 := c6 - c7
+										b7 := c7
+
+										mcu[i * BLOCK_SIZE + 0] = cast(i16)(b0 + b7)
+										mcu[i * BLOCK_SIZE + 1] = cast(i16)(b1 + b6)
+										mcu[i * BLOCK_SIZE + 2] = cast(i16)(b2 + b5)
+										mcu[i * BLOCK_SIZE + 3] = cast(i16)(b3 + b4)
+										mcu[i * BLOCK_SIZE + 4] = cast(i16)(b3 - b4)
+										mcu[i * BLOCK_SIZE + 5] = cast(i16)(b2 - b5)
+										mcu[i * BLOCK_SIZE + 6] = cast(i16)(b1 - b6)
+										mcu[i * BLOCK_SIZE + 7] = cast(i16)(b0 - b7)
+									}
+								}
+							}
+						}
+
+						// Convert the YCbCr pixel data to RGB
+						cbcr_blk := &blocks[y * block_width + x]
+						for v := luma_v_sampling_factor - 1; v >= 0; v -= 1 {
+							for h := luma_h_sampling_factor - 1; h >= 0; h -= 1 {
+								y_blk := &blocks[(y + v) * block_width + (x + h)]
+
+								for j := BLOCK_SIZE - 1; j >= 0; j -= 1 {
+									for k := BLOCK_SIZE - 1; k >= 0; k -= 1 {
+										i := j * BLOCK_SIZE + k
+										cbcr_pixel_row    := j / luma_v_sampling_factor + 4 * v
+										cbcr_pixel_column := k / luma_h_sampling_factor + 4 * h
+										cbcr_pixel := cbcr_pixel_row * BLOCK_SIZE + cbcr_pixel_column
+
+										r := cast(i16)math.clamp(cast(f32)y_blk[.Y][i] + 1.402 * cast(f32)cbcr_blk[.Cr][cbcr_pixel] + 128, 0, 255)
+										g := cast(i16)math.clamp(cast(f32)y_blk[.Y][i] - 0.344 * cast(f32)cbcr_blk[.Cb][cbcr_pixel] - 0.714 * cast(f32)cbcr_blk[.Cr][cbcr_pixel] + 128, 0, 255)
+										b := cast(i16)math.clamp(cast(f32)y_blk[.Y][i] + 1.772 * cast(f32)cbcr_blk[.Cb][cbcr_pixel] + 128, 0, 255)
+
+										y_blk[.Y][i]  = r
+										y_blk[.Cb][i] = g
+										y_blk[.Cr][i] = b
+									}
+								}
+							}
+						}
+					}
+				}
+
+				if resize(&img.pixels.buf, img.width * img.height * img.channels) != nil {
+					return img, .Unable_To_Allocate_Or_Resize
+				}
+
+				out := mem.slice_data_cast([]image.RGB_Pixel, img.pixels.buf[:])
+				for y in 0..<img.height {
+					mcu_row := y / BLOCK_SIZE
+					pixel_row := y % BLOCK_SIZE
+					for x in 0..<img.width {
+						mcu_col := x / BLOCK_SIZE
+						pixel_col := x % BLOCK_SIZE
+						mcu_idx := mcu_row * block_width + mcu_col
+						pixel_idx := pixel_row * BLOCK_SIZE + pixel_col
+
+						if img.channels == 3 {
+							out[y * img.width + x] = {
+								cast(byte)blocks[mcu_idx][.Y][pixel_idx],
+								cast(byte)blocks[mcu_idx][.Cb][pixel_idx],
+								cast(byte)blocks[mcu_idx][.Cr][pixel_idx],
+							}
+						} else {
+							img.pixels.buf[y * img.width + x] = cast(byte)blocks[mcu_idx][.Y][pixel_idx]
+						}
+					}
+				}
+
+				expect_EOI = true
+			case .TEM:
+				// TEM doesn't have a length, continue to next marker
+			case:
+				length := (compress.read_data(ctx, u16be) or_return) - 2
+				compress.read_slice_from_memory(ctx, cast(int)length) or_return
+			}
+		}
+	}
+
+	return
+}
+
+destroy :: proc(img: ^Image) {
+	if img == nil {
+		return
+	}
+
+	bytes.buffer_destroy(&img.pixels)
+
+	if v, ok := img.metadata.(^image.JPEG_Info); ok {
+		if jfxx, jfxx_ok := v.jfxx_app0.?; jfxx_ok {
+			delete(jfxx.thumbnail)
+		}
+		if jfif, jfif_ok := v.jfif_app0.?; jfif_ok {
+			delete(jfif.thumbnail)
+		}
+
+		for comment in v.comments {
+			delete(comment)
+		}
+		delete(v.comments)
+
+		for exif in v.exif {
+			delete(exif.data)
+		}
+		delete(v.exif)
+
+		free(v)
+	}
+	free(img)
+}
+
+@(init, private)
+_register :: proc "contextless" () {
+	image.register(.JPEG, load_from_bytes, destroy)
+}

+ 3 - 0
core/image/jpeg/jpeg_js.odin

@@ -0,0 +1,3 @@
+package jpeg
+
+load :: proc{load_from_bytes, load_from_context}

+ 18 - 0
core/image/jpeg/jpeg_os.odin

@@ -0,0 +1,18 @@
+package jpeg
+
+import "core:os"
+
+load :: proc{load_from_file, load_from_bytes, load_from_context}
+
+load_from_file :: proc(filename: string, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
+	context.allocator = allocator
+
+	data, ok := os.read_entire_file(filename)
+	defer delete(data)
+
+	if ok {
+		return load_from_bytes(data, options)
+	} else {
+		return nil, .Unable_To_Read_File
+	}
+}

+ 2 - 2
core/image/png/helpers.odin

@@ -366,7 +366,7 @@ chrm :: proc(c: image.PNG_Chunk) -> (res: cHRM, ok: bool) {
 	return
 }
 
-exif :: proc(c: image.PNG_Chunk) -> (res: Exif, ok: bool) {
+exif :: proc(c: image.PNG_Chunk) -> (res: image.Exif, ok: bool) {
 
 	ok = true
 
@@ -396,4 +396,4 @@ exif :: proc(c: image.PNG_Chunk) -> (res: Exif, ok: bool) {
 	General helper functions
 */
 
-compute_buffer_size :: image.compute_buffer_size
+compute_buffer_size :: image.compute_buffer_size

+ 5 - 9
core/image/png/png.odin

@@ -138,14 +138,6 @@ Text :: struct {
 	text:              string,
 }
 
-Exif :: struct {
-	byte_order: enum {
-		little_endian,
-		big_endian,
-	},
-	data: []u8,
-}
-
 iCCP :: struct {
 	name:    string,
 	profile: []u8,
@@ -250,10 +242,14 @@ read_header :: proc(ctx: ^$C) -> (image.PNG_IHDR, Error) {
 	header := (^image.PNG_IHDR)(raw_data(c.data))^
 	// Validate IHDR
 	using header
-	if width == 0 || height == 0 || u128(width) * u128(height) > image.MAX_DIMENSIONS {
+	if width == 0 || height == 0 {
 		return {}, .Invalid_Image_Dimensions
 	}
 
+	if u128(width) * u128(height) > image.MAX_DIMENSIONS {
+		return {}, .Image_Dimensions_Too_Large
+	}
+
 	if compression_method != 0 {
 		return {}, compress.General_Error.Unknown_Compression_Method
 	}

+ 1 - 0
core/sys/posix/spawn.odin

@@ -1,3 +1,4 @@
+#+build linux, darwin, openbsd, freebsd, netbsd, haiku
 package posix
 
 when ODIN_OS == .Darwin {

+ 1 - 1
core/time/time_js.odin

@@ -24,7 +24,7 @@ _sleep :: proc "contextless" (d: Duration) {
 
 _tick_now :: proc "contextless" () -> Tick {
 	foreign odin_env {
-		tick_now :: proc "contextless" () -> f32 ---
+		tick_now :: proc "contextless" () -> f64 ---
 	}
 	return Tick{i64(tick_now()*1e6)}
 }

+ 1 - 0
examples/all/all_main.odin

@@ -82,6 +82,7 @@ package all
 @(require) import "core:image/png"
 @(require) import "core:image/qoi"
 @(require) import "core:image/tga"
+@(require) import "core:image/jpeg"
 
 @(require) import "core:io"
 @(require) import "core:log"

+ 10 - 2
src/check_expr.cpp

@@ -6473,7 +6473,14 @@ gb_internal CallArgumentError check_call_arguments_internal(CheckerContext *c, A
 		}
 
 		if (e && e->kind == Entity_Constant && is_type_proc(e->type)) {
-			if (o->mode != Addressing_Constant) {
+			bool ok = false;
+			if (o->mode == Addressing_Constant) {
+				ok = true;
+			} else if (o->value.kind == ExactValue_Procedure) {
+				ok = true;
+			}
+
+			if (!ok) {
 				if (show_error) {
 					error(o->expr, "Expected a constant procedure value for the argument '%.*s'", LIT(e->token.string));
 				}
@@ -7947,7 +7954,7 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O
 				s = gb_string_append_fmt(s, "$%.*s", LIT(name));
 
 				if (v->kind == Entity_TypeName) {
-					if (v->type->kind != Type_Generic) {
+					if (v->type != nullptr && v->type->kind != Type_Generic) {
 						s = gb_string_append_fmt(s, "=");
 						s = write_type_to_string(s, v->type, false);
 					}
@@ -11423,6 +11430,7 @@ gb_internal ExprKind check_expr_base_internal(CheckerContext *c, Operand *o, Ast
 
 		o->mode = Addressing_Value;
 		o->type = type;
+		o->value = exact_value_procedure(node);
 	case_end;
 
 	case_ast_node(te, TernaryIfExpr, node);

+ 3 - 0
src/checker.cpp

@@ -1797,6 +1797,9 @@ gb_internal void add_type_and_value(CheckerContext *ctx, Ast *expr, AddressingMo
 		}
 
 		expr = unparen_expr(expr);
+		if (expr == nullptr) {
+			break;
+		};
 	}
 	mutex_unlock(mutex);
 }

+ 1 - 0
tests/core/.gitignore

@@ -1,4 +1,5 @@
 *.bmp
 *.zip
 *.png
+*.jpg
 math_big_test_library.*

+ 4 - 2
tests/core/download_assets.py

@@ -7,7 +7,7 @@ import zipfile
 import hashlib
 import hmac
 
-TEST_SUITES        = ['PNG', 'XML', 'BMP']
+TEST_SUITES        = ['PNG', 'XML', 'BMP', 'JPG']
 DOWNLOAD_BASE_PATH = sys.argv[1] + "/{}"
 ASSETS_BASE_URL    = "https://raw.githubusercontent.com/odin-lang/test-assets/master/{}/{}"
 HMAC_KEY           = "https://odin-lang.org"
@@ -280,7 +280,9 @@ HMAC_DIGESTS = {
 	'rletopdown.bmp':      "37500893aad0b40656aa80fd5c7c5f9b35d033018b8070d8b1d7baeb34c90f90462288b13295204b90aa3e5c9be797d22a328e3714ab259334e879a09a3de175",
 	'shortfile.bmp':       "be3ffade7999304f00f9b7d152b5b27811ad1166d0fd43004392467a28f44b6a4ec02a23c0296bacd4f02f8041cd824b9ca6c9fc31fed27e36e572113bb47d73",
 
-	'unicode.xml':  "e0cdc94f07fdbb15eea811ed2ae6dcf494a83d197dafe6580c740270feb0d8f5f7146d4a7d4c2d2ea25f8bd9678bc986123484b39399819a6b7262687959d1ae",
+	'emblem-1024.jpg':     "d7b7e3ffaa5cda04c667e3742752091d78e02aa2d3c7a63406af679ce810a0a86666b10fcab12cc7ead2fadf2f6c2e1237bc94f892a62a4c218e18a20f96dbe4",
+
+	'unicode.xml':         "e0cdc94f07fdbb15eea811ed2ae6dcf494a83d197dafe6580c740270feb0d8f5f7146d4a7d4c2d2ea25f8bd9678bc986123484b39399819a6b7262687959d1ae",
 }
 
 def try_download_file(url, out_file):

+ 48 - 0
tests/core/image/test_core_image.odin

@@ -19,6 +19,7 @@ import pbm "core:image/netpbm"
 import "core:image/png"
 import "core:image/qoi"
 import "core:image/tga"
+import "core:image/jpeg"
 
 import "core:bytes"
 import "core:hash"
@@ -28,6 +29,7 @@ import "core:time"
 
 TEST_SUITE_PATH_PNG :: ODIN_ROOT + "tests/core/assets/PNG"
 TEST_SUITE_PATH_BMP :: ODIN_ROOT + "tests/core/assets/BMP"
+TEST_SUITE_PATH_JPG :: ODIN_ROOT + "tests/core/assets/JPG"
 
 I_Error :: image.Error
 
@@ -2360,6 +2362,52 @@ run_bmp_suite :: proc(t: ^testing.T, suite: []Test) {
 	return
 }
 
+// JPG test image
+Basic_JPG_Tests := []Test{
+	{
+		"emblem-1024", {
+			{Default, nil, {1024, 1024, 3, 8}, 0x_46a29e0f},
+		},
+	},
+}
+
+@test
+jpeg_test_basic :: proc(t: ^testing.T) {
+	run_jpg_suite(t, Basic_JPG_Tests)
+}
+
+run_jpg_suite :: proc(t: ^testing.T, suite: []Test) {
+	for file in suite {
+		test_file := strings.concatenate({TEST_SUITE_PATH_JPG, "/", file.file, ".jpg"}, context.allocator)
+		defer delete(test_file)
+
+		for test in file.tests {
+			img, err := jpeg.load(test_file, test.options)
+
+			passed := (test.expected_error == nil && err == nil) || (test.expected_error == err)
+			testing.expectf(t, passed, "%q failed to load with error %v.", file.file, err)
+
+			if err == nil { // No point in running the other tests if it didn't load.
+				pixels := bytes.buffer_to_bytes(&img.pixels)
+
+				dims   := Dims{img.width, img.height, img.channels, img.depth}
+				testing.expectf(t, test.dims == dims, "%v has %v, expected: %v.", file.file, dims, test.dims)
+
+				img_hash := hash.crc32(pixels)
+				testing.expectf(t, test.hash == img_hash, "%v test #1's hash is %08x, expected %08x with %v.", file.file, img_hash, test.hash, test.options)
+
+				// Save to BMP file to check load
+				test_bmp := strings.concatenate({TEST_SUITE_PATH_JPG, "/", file.file, ".bmp"}, context.temp_allocator)
+
+				save_err := bmp.save(test_bmp, img)
+				testing.expectf(t, save_err == nil, "expected saving to BMP in memory not to raise error, got %v", save_err)
+			}
+			bmp.destroy(img)
+		}
+	}
+	return
+}
+
 @test
 will_it_blend :: proc(t: ^testing.T) {
 	Pixel    :: image.RGB_Pixel

+ 1 - 1
vendor/box2d/box2d.odin

@@ -1370,7 +1370,7 @@ foreign lib {
 
 	// Create a motor joint
 	//	@see b2MotorJointDef for details
-	CreateMotorJoint               :: proc(worldId: WorldId, def: MotorJointDef) -> JointId ---
+	CreateMotorJoint               :: proc(worldId: WorldId, #by_ptr def: MotorJointDef) -> JointId ---
 
 	// Set the motor joint linear offset target
 	MotorJoint_SetLinearOffset     :: proc(jointId: JointId, linearOffset: Vec2) ---