From cd3069b16bdc780a0317c81a4f4a441b9cfc6b02 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Tue, 9 Sep 2025 18:34:19 +0200 Subject: Small updates to JPEG loader - Remove some unnecessary nesting - Add frame type (SOF0, et al) to metadata if `.return_metadata` is used --- core/image/common.odin | 28 +- core/image/jpeg/jpeg.odin | 1456 +++++++++++++++++++++++---------------------- 2 files changed, 748 insertions(+), 736 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index 0e5668e50..4014e2ae6 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -642,22 +642,23 @@ JFXX_Extension_Code :: enum u8 { } JPEG_Marker :: enum u8 { - SOF0 = 0xC0, - SOF1 = 0xC1, - SOF2 = 0xC2, - SOF3 = 0xC3, + SOF0 = 0xC0, // Baseline sequential DCT + SOF1 = 0xC1, // Extended sequential DCT + SOF2 = 0xC2, // Progressive DCT + SOF3 = 0xC3, // Lossless (sequential) + SOF5 = 0xC5, // Differential sequential DCT + SOF6 = 0xC6, // Differential progressive DCT + SOF7 = 0xC7, // Differential lossless (sequential) + SOF9 = 0xC9, // Extended sequential DCT, Arithmetic coding + SOF10 = 0xCA, // Progressive DCT, Arithmetic coding + SOF11 = 0xCB, // Lossless (sequential), Arithmetic coding + SOF13 = 0xCD, // Differential sequential DCT, Arithmetic coding + SOF14 = 0xCE, // Differential progressive DCT, Arithmetic coding + SOF15 = 0xCF, // Differential lossless (sequential), Arithmetic coding + 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, @@ -713,6 +714,7 @@ JPEG_Info :: struct { jfxx_app0: Maybe(JFXX_APP0), comments: [dynamic]string, exif: [dynamic]Exif, + frame_type: JPEG_Marker, } // Function to help with image buffer calculations diff --git a/core/image/jpeg/jpeg.odin b/core/image/jpeg/jpeg.odin index a2ad7e61e..818bd13d5 100644 --- a/core/image/jpeg/jpeg.odin +++ b/core/image/jpeg/jpeg.odin @@ -219,841 +219,851 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a defer delete(blocks) loop: for { + // Loop until we find 0xFF. 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 + (first == 0xFF) or_continue + + 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 } - #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 } - 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) - 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 + 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 { - return img, .Invalid_Thumbnail_Size + info = img.metadata.(^image.JPEG_Info) } - 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 + 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 + } + } 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) } - 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.. 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 + info: ^image.JPEG_Info + if img.metadata == nil { + info = new(image.JPEG_Info) or_return } else { - compress.read_slice(ctx, length - len(ident) - 2) or_return - continue + 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 } - - // - 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: + return img, .Invalid_JFXX_Extension_Code } - case .DQT: - length := cast(int)(compress.read_data(ctx, u16be) or_return) - 2 + } 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) + } - for length > 0 { - precision_and_index := compress.read_u8(ctx) or_return - precision := precision_and_index >> 4 - index := precision_and_index & 0xF + 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 precision != 0 && precision != 1 { - return img, .Invalid_Quantization_Table_Precision + 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 } - - if index < 0 || index > 3 { - return img, .Invalid_Quantization_Table_Index + } 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 - // 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 - } + // 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 { - table := compress.read_slice(ctx, table_bytes) or_return - for v, i in table { - quantization[index][i] = cast(u16be)v - } + 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) - length -= table_bytes + 1 + 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 .DHT: - length := (compress.read_data(ctx, u16be) or_return) - 2 + } + case .DQT: + length := cast(int)(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 + for length > 0 { + precision_and_index := compress.read_u8(ctx) or_return + precision := precision_and_index >> 4 + index := precision_and_index & 0xF - if type != .DC && type != .AC { - return img, .Invalid_Huffman_Coefficient_Type - } + if precision != 0 && precision != 1 { + return img, .Invalid_Quantization_Table_Precision + } - if index < 0 || index > 3 { - return img, .Invalid_Huffman_Table_Index - } + if index < 0 || index > 3 { + return img, .Invalid_Quantization_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 + // 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 } - - if num_symbols > HUFFMAN_MAX_SYMBOLS { - return img, .Huffman_Symbols_Exceeds_Max + } else { + table := compress.read_slice(ctx, table_bytes) or_return + for v, i in table { + quantization[index][i] = cast(u16be)v } + } - symbols := compress.read_slice(ctx, cast(int)num_symbols) or_return - copy(huffman[type][index].symbols[:], symbols) + length -= table_bytes + 1 + } + case .DHT: + length := (compress.read_data(ctx, u16be) or_return) - 2 - length -= cast(u16be)(1 + HUFFMAN_MAX_BITS + num_symbols) + for length > 0 { + type_index := compress.read_u8(ctx) or_return + type := cast(Coefficient)((type_index >> 4) & 0xF) + index := type_index & 0xF - code: u32 = 0 - for i in 0.. 3 { + return img, .Invalid_Huffman_Table_Index } - // 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 + 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 u128(img.width) * u128(img.height) > image.MAX_DIMENSIONS { - return img, .Image_Dimensions_Too_Large + if num_symbols > HUFFMAN_MAX_SYMBOLS { + return img, .Huffman_Symbols_Exceeds_Max } - // 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 + symbols := compress.read_slice(ctx, cast(int)num_symbols) or_return + copy(huffman[type][index].symbols[:], symbols) - for _ in 0.. .Cr { - return img, .Image_Does_Not_Adhere_to_Spec - } + // 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 + } - h_v_factors := compress.read_u8(ctx) or_return - horizontal_sampling := h_v_factors >> 4 - vertical_sampling := h_v_factors & 0xF + if u128(img.width) * u128(img.height) > image.MAX_DIMENSIONS { + return img, .Image_Dimensions_Too_Large + } - // 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 - } + // 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 + } - 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 - } - } + if img.metadata != nil { + info := img.metadata.(^image.JPEG_Info) + info.frame_type = marker + } - quantization_table_idx := compress.read_u8(ctx) or_return + mcu_width = (img.width + 7) / BLOCK_SIZE + mcu_height = (img.height + 7) / BLOCK_SIZE + block_width = mcu_width + block_height = mcu_height - if quantization_table_idx < 0 || quantization_table_idx > 3 { - return img, .Invalid_Quantization_Table_Index - } + for _ in 0.. .Cr { + return img, .Image_Does_Not_Adhere_to_Spec } - // 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 + 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 } - for _ in 0.. .Cr { - return img, .Image_Does_Not_Adhere_to_Spec + if vertical_sampling == 2 && mcu_height % 2 == 1 { + block_height += 1 } + } else { + if horizontal_sampling != 1 && vertical_sampling != 1 { + return img, .Invalid_Sampling_Factor + } + } - // 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 + quantization_table_idx := compress.read_u8(ctx) or_return - if (dc_table_idx < 0 || dc_table_idx > 3) || (ac_table_idx < 0 || ac_table_idx > 3) { - return img, .Invalid_Huffman_Table_Index - } + 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 + fallthrough + 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 + if img.metadata != nil { + info := img.metadata.(^image.JPEG_Info) + info.frame_type = marker + } + 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 + } - color_components[component_id].dc_table_idx = dc_table_idx - color_components[component_id].ac_table_idx = ac_table_idx + // 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.. .Cr { + return img, .Image_Does_Not_Adhere_to_Spec } - // 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.. 11 { - return img, .Corrupt - } - dc_coeff := cast(i16)read_bits_msb(ctx, length) + // 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 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] + if (dc_table_idx < 0 || dc_table_idx > 3) || (ac_table_idx < 0 || ac_table_idx > 3) { + return img, .Invalid_Huffman_Table_Index + } - 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) + 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.. 11 { + return img, .Corrupt + } - // Special symbol used to indicate - // that the rest of the MCU is filled with 0s - if symbol == 0x00 { - continue h_loop - } + dc_coeff := cast(i16)read_bits_msb(ctx, length) - amnt_zeros := int(symbol >> 4) - ac_coeff_len := symbol & 0xF - ac_coeff: i16 = 0 + 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 + } - if i + amnt_zeros >= COEFFICIENT_COUNT || ac_coeff_len > 10 { - return img, .Corrupt - } + amnt_zeros := int(symbol >> 4) + ac_coeff_len := symbol & 0xF + ac_coeff: i16 = 0 - i += amnt_zeros + if i + amnt_zeros >= COEFFICIENT_COUNT || ac_coeff_len > 10 { + return img, .Corrupt + } - 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 - } + i += amnt_zeros - mcu[zigzag[i]] = ac_coeff * cast(i16)quantization_table[i] + 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..= 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)clamp(cast(f32)y_blk[.Y][i] + 1.402 * cast(f32)cbcr_blk[.Cr][cbcr_pixel] + 128, 0, 255) - g := cast(i16)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)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 - } + // 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)clamp(cast(f32)y_blk[.Y][i] + 1.402 * cast(f32)cbcr_blk[.Cr][cbcr_pixel] + 128, 0, 255) + g := cast(i16)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)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 } } } } } + } - orig_channels := img.channels + orig_channels := img.channels - // We automatically expand grayscale images to RGB - if img.channels == 1 { - img.channels += 2 - } + // We automatically expand grayscale images to RGB + if img.channels == 1 { + img.channels += 2 + } - if .alpha_add_if_missing in options { - img.channels += 1 - orig_channels += 1 - } + if .alpha_add_if_missing in options { + img.channels += 1 + orig_channels += 1 + } - if resize(&img.pixels.buf, img.width * img.height * img.channels) != nil { - return img, .Unable_To_Allocate_Or_Resize - } + if resize(&img.pixels.buf, img.width * img.height * img.channels) != nil { + return img, .Unable_To_Allocate_Or_Resize + } - switch orig_channels { - case 1: // Grayscale JPEG expanded to RGB - out := mem.slice_data_cast([]image.RGB_Pixel, img.pixels.buf[:]) - out_idx := 0 - for y in 0..