From cd3069b16bdc780a0317c81a4f4a441b9cfc6b02 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Tue, 9 Sep 2025 18:34:19 +0200 Subject: [PATCH] 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 | 1508 +++++++++++++++++++------------------ 2 files changed, 774 insertions(+), 762 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 - } - #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 - } + (first == 0xFF) or_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 + 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 - thumbnail: []image.RGB_Pixel + pixels := compress.read_slice(ctx, x_thumbnail * y_thumbnail * 3) or_return - 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([]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 { - 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.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 + 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 - 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 + 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 } + 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) + } - // - 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) + 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 + } - append(&info.exif, exif) or_return - img.metadata = info + 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 { - // - 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 + // If we can't determine the endianness then this Exif data is likely a continuation of the previous + // APP1 Exif data - 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 - } + // 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 - } - } - - 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.. 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 } - // 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 + if index < 0 || index > 3 { + return img, .Invalid_Quantization_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 - } - - 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.. .Cr { - return img, .Image_Does_Not_Adhere_to_Spec + 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..> 4 - vertical_sampling := h_v_factors & 0xF + // 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: 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 { + // 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 + } + + if img.metadata != nil { + info := img.metadata.(^image.JPEG_Info) + info.frame_type = marker + } + + 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.. .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 } - 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 - 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 - 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 + quantization_table_idx := compress.read_u8(ctx) or_return + + if quantization_table_idx < 0 || quantization_table_idx > 3 { + return img, .Invalid_Quantization_Table_Index } - // 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 + 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 + } + + // 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 } - for _ in 0.. .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 - // 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 + if (dc_table_idx < 0 || dc_table_idx > 3) || (ac_table_idx < 0 || ac_table_idx > 3) { + return img, .Invalid_Huffman_Table_Index } - // 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 + 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 - previous_dc: [Component]i16 + blocks = make([]Block, block_height * block_width) or_return - luma_v_sampling_factor := color_components[.Y].v_sampling_factor - luma_h_sampling_factor := color_components[.Y].h_sampling_factor + previous_dc: [Component]i16 - 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 + luma_v_sampling_factor := color_components[.Y].v_sampling_factor + luma_h_sampling_factor := color_components[.Y].h_sampling_factor - 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 { + 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 } - dc_coeff := cast(i16)read_bits_msb(ctx, length) + i += amnt_zeros - if length != 0 && dc_coeff < (1 << (length - 1)) { - dc_coeff -= (1 << length) - 1 + 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[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] - } + mcu[zigzag[i]] = ac_coeff * cast(i16)quantization_table[i] } } } + } - for c in 1..=img.channels { - c := cast(Component)c + 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)] + // 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 + 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) + 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 - } + y_blk[.Y][i] = r + y_blk[.Cb][i] = g + y_blk[.Cr][i] = b } } } } } - - orig_channels := img.channels - - // 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 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..