| // Copyright 2024 The Wuffs Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
| // option. This file may not be copied, modified, or distributed |
| // except according to those terms. |
| // |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| pub status "#bad footer" |
| pub status "#bad header" |
| pub status "#truncated input" |
| |
| pub const DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE : base.u64 = 0 |
| |
| pub struct decoder? implements base.image_decoder( |
| pixfmt : base.u32, |
| width : base.u32[..= 0xFF_FFFF], |
| height : base.u32[..= 0xFF_FFFF], |
| |
| remaining_pixels_times_4 : base.u64, |
| |
| // The call sequence state machine is discussed in |
| // (/doc/std/image-decoders-call-sequence.md). |
| call_sequence : base.u8, |
| |
| buffer_index : base.u32[..= 8192], |
| |
| dst_x : base.u32, |
| dst_y : base.u32, |
| |
| swizzler : base.pixel_swizzler, |
| util : base.utility, |
| ) + ( |
| pixel : array[4] base.u8, |
| cache : array[256] base.u8, |
| buffer : array[8192 + 4] base.u8, |
| ) |
| |
| pub func decoder.get_quirk(key: base.u32) base.u64 { |
| return 0 |
| } |
| |
| pub func decoder.set_quirk!(key: base.u32, value: base.u64) base.status { |
| return base."#unsupported option" |
| } |
| |
| pub func decoder.decode_image_config?(dst: nptr base.image_config, src: base.io_reader) { |
| var status : base.status |
| |
| while true { |
| status =? this.do_decode_image_config?(dst: args.dst, src: args.src) |
| if (status == base."$short read") and args.src.is_closed() { |
| return "#truncated input" |
| } |
| yield? status |
| } |
| } |
| |
| pri func decoder.do_decode_image_config?(dst: nptr base.image_config, src: base.io_reader) { |
| var a : base.u32 |
| |
| if this.call_sequence <> 0x00 { |
| return base."#bad call sequence" |
| } |
| |
| a = args.src.read_u32le?() |
| if a <> 'qoif'le { |
| return "#bad header" |
| } |
| |
| a = args.src.read_u32be?() |
| if a > 0xFF_FFFF { |
| return base."#unsupported image dimension" |
| } |
| this.width = a |
| |
| a = args.src.read_u32be?() |
| if a > 0xFF_FFFF { |
| return base."#unsupported image dimension" |
| } |
| this.height = a |
| |
| a = args.src.read_u8_as_u32?() |
| if a == 3 { |
| this.pixfmt = base.PIXEL_FORMAT__BGRX |
| } else if a == 4 { |
| this.pixfmt = base.PIXEL_FORMAT__BGRA_NONPREMUL |
| } else { |
| return "#bad header" |
| } |
| |
| args.src.skip?(n: 1) |
| |
| if args.dst <> nullptr { |
| args.dst.set!( |
| pixfmt: this.pixfmt, |
| pixsub: 0, |
| width: this.width, |
| height: this.height, |
| first_frame_io_position: 14, |
| first_frame_is_opaque: this.pixfmt == base.PIXEL_FORMAT__BGRX) |
| } |
| |
| this.call_sequence = 0x20 |
| } |
| |
| pub func decoder.decode_frame_config?(dst: nptr base.frame_config, src: base.io_reader) { |
| var status : base.status |
| |
| while true { |
| status =? this.do_decode_frame_config?(dst: args.dst, src: args.src) |
| if (status == base."$short read") and args.src.is_closed() { |
| return "#truncated input" |
| } |
| yield? status |
| } |
| } |
| |
| pri func decoder.do_decode_frame_config?(dst: nptr base.frame_config, src: base.io_reader) { |
| if this.call_sequence == 0x20 { |
| // No-op. |
| } else if this.call_sequence < 0x20 { |
| this.do_decode_image_config?(dst: nullptr, src: args.src) |
| } else if this.call_sequence == 0x28 { |
| if 14 <> args.src.position() { |
| return base."#bad restart" |
| } |
| } else if this.call_sequence == 0x40 { |
| this.call_sequence = 0x60 |
| return base."@end of data" |
| } else { |
| return base."@end of data" |
| } |
| |
| if args.dst <> nullptr { |
| args.dst.set!(bounds: this.util.make_rect_ie_u32( |
| min_incl_x: 0, |
| min_incl_y: 0, |
| max_excl_x: this.width, |
| max_excl_y: this.height), |
| duration: 0, |
| index: 0, |
| io_position: 14, |
| disposal: 0, |
| opaque_within_bounds: this.pixfmt == base.PIXEL_FORMAT__BGRX, |
| overwrite_instead_of_blend: false, |
| background_color: 0x0000_0000) |
| } |
| |
| this.call_sequence = 0x40 |
| } |
| |
| pub func decoder.decode_frame?(dst: ptr base.pixel_buffer, src: base.io_reader, blend: base.pixel_blend, workbuf: slice base.u8, opts: nptr base.decode_frame_options) { |
| var status : base.status |
| |
| while true { |
| status =? this.do_decode_frame?(dst: args.dst, src: args.src, blend: args.blend, workbuf: args.workbuf, opts: args.opts) |
| if (status == base."$short read") and args.src.is_closed() { |
| return "#truncated input" |
| } |
| yield? status |
| } |
| } |
| |
| pri func decoder.do_decode_frame?(dst: ptr base.pixel_buffer, src: base.io_reader, blend: base.pixel_blend, workbuf: slice base.u8, opts: nptr base.decode_frame_options) { |
| var status : base.status |
| var c64 : base.u64 |
| |
| if this.call_sequence == 0x40 { |
| // No-op. |
| } else if this.call_sequence < 0x40 { |
| this.do_decode_frame_config?(dst: nullptr, src: args.src) |
| } else { |
| return base."@end of data" |
| } |
| |
| status = this.swizzler.prepare!( |
| dst_pixfmt: args.dst.pixel_format(), |
| dst_palette: args.dst.palette(), |
| src_pixfmt: this.util.make_pixel_format(repr: this.pixfmt), |
| src_palette: this.util.empty_slice_u8(), |
| blend: args.blend) |
| if not status.is_ok() { |
| return status |
| } |
| |
| this.dst_x = 0 |
| this.dst_y = 0 |
| this.pixel[0] = 0x00 |
| this.pixel[1] = 0x00 |
| this.pixel[2] = 0x00 |
| this.pixel[3] = 0xFF |
| this.cache[.. 256].bulk_memset!(byte_value: 0) |
| |
| this.remaining_pixels_times_4 = (this.width as base.u64) * (this.height as base.u64) * 4 |
| while this.remaining_pixels_times_4 > 0 { |
| this.from_src_to_buffer?(src: args.src) |
| if this.remaining_pixels_times_4 < (this.buffer_index as base.u64) { |
| return base."#too much data" |
| } |
| this.remaining_pixels_times_4 -= this.buffer_index as base.u64 |
| status = this.from_buffer_to_dst!(dst: args.dst) |
| if not status.is_ok() { |
| return status |
| } |
| } |
| |
| c64 = args.src.read_u64be?() |
| if c64 <> 1 { |
| return "#bad footer" |
| } |
| |
| this.call_sequence = 0x60 |
| } |
| |
| pri func decoder.from_src_to_buffer?(src: base.io_reader) { |
| var c8 : base.u8 |
| var dg : base.u8 |
| var bi : base.u32[..= 8192] |
| var bj : base.u32[..= 8188] |
| var bk : base.u32[..= 7936] |
| var ci : base.u32[..= 252] |
| var hash4 : base.u32[..= 252] |
| |
| bk = 7936 // 4 * (2048 - 64), so we always have room for a QOI_OP_RUN. |
| if this.remaining_pixels_times_4 < 7936 { |
| bk = this.remaining_pixels_times_4 as base.u32 |
| } |
| |
| while bi < bk { |
| assert bi < 7936 via "a < b: a < c; c <= b"(c: bk) |
| c8 = args.src.read_u8?() |
| if c8 == 0xFE { // QOI_OP_RGB. |
| this.pixel[2] = args.src.read_u8?() |
| this.pixel[1] = args.src.read_u8?() |
| this.pixel[0] = args.src.read_u8?() |
| |
| } else if c8 == 0xFF { // QOI_OP_RGBA. |
| this.pixel[2] = args.src.read_u8?() |
| this.pixel[1] = args.src.read_u8?() |
| this.pixel[0] = args.src.read_u8?() |
| this.pixel[3] = args.src.read_u8?() |
| |
| } else if (c8 >> 6) == 0 { // QOI_OP_INDEX. |
| ci = 4 * ((c8 & 0x3F) as base.u32) |
| this.pixel[0] = this.cache[ci + 0] |
| this.pixel[1] = this.cache[ci + 1] |
| this.pixel[2] = this.cache[ci + 2] |
| this.pixel[3] = this.cache[ci + 3] |
| this.buffer[bi + 0] = this.pixel[0] |
| this.buffer[bi + 1] = this.pixel[1] |
| this.buffer[bi + 2] = this.pixel[2] |
| this.buffer[bi + 3] = this.pixel[3] |
| bi += 4 |
| continue |
| |
| } else if (c8 >> 6) == 1 { // QOI_OP_DIFF. |
| this.pixel[2] ~mod+= ((c8 >> 4) & 0x03) ~mod+ 0xFE |
| this.pixel[1] ~mod+= ((c8 >> 2) & 0x03) ~mod+ 0xFE |
| this.pixel[0] ~mod+= ((c8 >> 0) & 0x03) ~mod+ 0xFE |
| |
| } else if (c8 >> 6) == 2 { // QOI_OP_LUMA. |
| dg = (c8 & 0x3F) ~mod+ 0xE0 |
| c8 = args.src.read_u8?() |
| this.pixel[2] ~mod+= (dg ~mod+ 0xF8) ~mod+ (0x0F & (c8 >> 4)) |
| this.pixel[1] ~mod+= dg |
| this.pixel[0] ~mod+= (dg ~mod+ 0xF8) ~mod+ (0x0F & (c8 >> 0)) |
| |
| } else { // QOI_OP_RUN. |
| bj = bi + (4 * (0x3F & (1 + (c8 as base.u32)))) |
| while bi < bj { |
| assert bi < 8188 via "a < b: a < c; c <= b"(c: bj) |
| this.buffer[bi + 0] = this.pixel[0] |
| this.buffer[bi + 1] = this.pixel[1] |
| this.buffer[bi + 2] = this.pixel[2] |
| this.buffer[bi + 3] = this.pixel[3] |
| bi += 4 |
| } |
| continue |
| } |
| |
| hash4 = 4 * (63 & ( |
| ((this.pixel[2] as base.u32) * 3) + |
| ((this.pixel[1] as base.u32) * 5) + |
| ((this.pixel[0] as base.u32) * 7) + |
| ((this.pixel[3] as base.u32) * 11))) |
| this.cache[hash4 + 0] = this.pixel[0] |
| this.cache[hash4 + 1] = this.pixel[1] |
| this.cache[hash4 + 2] = this.pixel[2] |
| this.cache[hash4 + 3] = this.pixel[3] |
| |
| this.buffer[bi + 0] = this.pixel[0] |
| this.buffer[bi + 1] = this.pixel[1] |
| this.buffer[bi + 2] = this.pixel[2] |
| this.buffer[bi + 3] = this.pixel[3] |
| bi += 4 |
| } |
| |
| this.buffer_index = bi |
| } |
| |
| pri func decoder.from_buffer_to_dst!(dst: ptr base.pixel_buffer) base.status { |
| var dst_pixfmt : base.pixel_format |
| var dst_bits_per_pixel : base.u32[..= 256] |
| var dst_bytes_per_pixel : base.u32[..= 32] |
| var dst_bytes_per_row : base.u64 |
| var tab : table base.u8 |
| var bi : base.u32 |
| var rem_x : base.u32[..= 0xFF_FFFF] |
| var dst : slice base.u8 |
| var src : slice base.u8 |
| var src_length : base.u32 |
| var i : base.u64 |
| |
| // TODO: the dst_pixfmt variable shouldn't be necessary. We should be able |
| // to chain the two calls: "args.dst.pixel_format().bits_per_pixel()". |
| dst_pixfmt = args.dst.pixel_format() |
| dst_bits_per_pixel = dst_pixfmt.bits_per_pixel() |
| if (dst_bits_per_pixel & 7) <> 0 { |
| return base."#unsupported option" |
| } |
| dst_bytes_per_pixel = dst_bits_per_pixel / 8 |
| dst_bytes_per_row = (this.width * dst_bytes_per_pixel) as base.u64 |
| tab = args.dst.plane(p: 0) |
| |
| while bi < this.buffer_index { |
| assert bi < 8192 via "a < b: a < c; c <= b"(c: this.buffer_index) |
| |
| if this.width <= this.dst_x { |
| this.dst_x = 0 |
| this.dst_y ~mod+= 1 |
| if this.dst_y >= this.height { |
| break |
| } |
| rem_x = this.width |
| } else { |
| rem_x = this.width - this.dst_x |
| } |
| |
| src = this.buffer[bi .. this.buffer_index] |
| if ((4 * rem_x) as base.u64) < src.length() { |
| src = src[.. (4 * rem_x) as base.u64] |
| } |
| src_length = (src.length() & 0xFFFF_FFFF) as base.u32 |
| bi ~mod+= src_length |
| |
| dst = tab.row_u32(y: this.dst_y) |
| if dst_bytes_per_row < dst.length() { |
| dst = dst[.. dst_bytes_per_row] |
| } |
| |
| i = (this.dst_x as base.u64) * (dst_bytes_per_pixel as base.u64) |
| this.dst_x ~mod+= src_length / 4 |
| if i < dst.length() { |
| this.swizzler.swizzle_interleaved_from_slice!( |
| dst: dst[i ..], |
| dst_palette: args.dst.palette(), |
| src: src) |
| } |
| } |
| |
| return ok |
| } |
| |
| pub func decoder.frame_dirty_rect() base.rect_ie_u32 { |
| return this.util.make_rect_ie_u32( |
| min_incl_x: 0, |
| min_incl_y: 0, |
| max_excl_x: this.width, |
| max_excl_y: this.height) |
| } |
| |
| pub func decoder.num_animation_loops() base.u32 { |
| return 0 |
| } |
| |
| pub func decoder.num_decoded_frame_configs() base.u64 { |
| if this.call_sequence > 0x20 { |
| return 1 |
| } |
| return 0 |
| } |
| |
| pub func decoder.num_decoded_frames() base.u64 { |
| if this.call_sequence > 0x40 { |
| return 1 |
| } |
| return 0 |
| } |
| |
| pub func decoder.restart_frame!(index: base.u64, io_position: base.u64) base.status { |
| if this.call_sequence < 0x20 { |
| return base."#bad call sequence" |
| } |
| if (args.index <> 0) or (args.io_position <> 14) { |
| return base."#bad argument" |
| } |
| this.call_sequence = 0x28 |
| return ok |
| } |
| |
| pub func decoder.set_report_metadata!(fourcc: base.u32, report: base.bool) { |
| // No-op. QOI doesn't support metadata. |
| } |
| |
| pub func decoder.tell_me_more?(dst: base.io_writer, minfo: nptr base.more_information, src: base.io_reader) { |
| return base."#no more information" |
| } |
| |
| pub func decoder.workbuf_len() base.range_ii_u64 { |
| return this.util.make_range_ii_u64(min_incl: 0, max_incl: 0) |
| } |