| // Copyright 2020 The Wuffs Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| use "std/crc32" |
| use "std/zlib" |
| |
| pub status "#bad checksum" |
| pub status "#bad chunk" |
| pub status "#bad filter" |
| pub status "#bad header" |
| pub status "#missing palette" |
| pub status "#unsupported PNG file" |
| |
| pri status "#internal error: inconsistent workbuf length" |
| pri status "#internal error: zlib decoder did not exhaust its input" |
| |
| pub const DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE : base.u64 = 0 |
| |
| pub struct decoder? implements base.image_decoder( |
| // The 0x00FF_FFFF limit is arbitrary (the PNG spec says 0x7FFF_FFFF) but |
| // it means that (width * height * bytes_per_pixel) doesn't overflow. |
| width : base.u32[..= 0x00FF_FFFF], |
| height : base.u32[..= 0x00FF_FFFF], |
| |
| // bytes_per_row doesn't include the 1 byte for the per-row filter. |
| bytes_per_row : base.u64[..= 0x07FF_FFF8], |
| |
| workbuf_wi : base.u64, |
| workbuf_length : base.u64[..= 0x0007_FFFF_F100_0007], |
| |
| // Call sequence states: |
| // - 0x00: initial state. |
| // - 0x03: image config decoded. |
| // - 0x04: frame config decoded. |
| // - 0xFF: end-of-data, usually after (the non-animated) frame decoded. |
| // |
| // State transitions: |
| // |
| // - 0x00 -> 0x03: via DIC |
| // - 0x00 -> 0x04: via DFC with implicit DIC |
| // - 0x00 -> 0xFF: via DF with implicit DIC and DFC |
| // |
| // - 0x03 -> 0x04: via DFC |
| // - 0x03 -> 0xFF: via DF with implicit DFC |
| // |
| // - 0x04 -> 0xFF: via DFC |
| // - 0x04 -> 0xFF: via DF |
| // |
| // - ???? -> 0x03: via RF for ???? > 0x00 |
| // |
| // Where: |
| // - DF is decode_frame |
| // - DFC is decode_frame_config, implicit means nullptr args.dst |
| // - DIC is decode_image_config, implicit means nullptr args.dst |
| // - RF is restart_frame |
| call_sequence : base.u8, |
| |
| ignore_checksum : base.bool, |
| |
| depth : base.u8[..= 16], |
| color_type : base.u8, |
| filter_distance : base.u8[..= 8], |
| |
| seen_palette : base.bool, |
| |
| dst_pixfmt : base.u32, |
| src_pixfmt : base.u32, |
| |
| chunk_type : base.u32, |
| chunk_type_array : array[4] base.u8, |
| chunk_length : base.u64, |
| |
| frame_config_io_position : base.u64, |
| |
| swizzler : base.pixel_swizzler, |
| util : base.utility, |
| )( |
| crc32 : crc32.ieee_hasher, |
| zlib : zlib.decoder, |
| |
| dst_palette : array[4 * 256] base.u8, |
| src_palette : array[4 * 256] base.u8, |
| ) |
| |
| pub func decoder.set_ignore_checksum!(ic: base.bool) { |
| this.ignore_checksum = args.ic |
| this.zlib.set_ignore_checksum!(ic: args.ic) |
| } |
| |
| pub func decoder.set_quirk_enabled!(quirk: base.u32, enabled: base.bool) { |
| } |
| |
| pub func decoder.decode_image_config?(dst: nptr base.image_config, src: base.io_reader) { |
| var magic32 : base.u32 |
| var magic64 : base.u64 |
| var mark : base.u64 |
| var checksum_have : base.u32 |
| var checksum_want : base.u32 |
| var status : base.status |
| |
| if this.call_sequence <> 0 { |
| return base."#bad call sequence" |
| } |
| |
| magic64 = args.src.read_u64le?() |
| if magic64 <> '\x89PNG\x0D\x0A\x1A\x0A'le { |
| return "#bad header" |
| } |
| |
| magic32 = args.src.read_u32le?() |
| if magic32 <> '\x00\x00\x00\x0D'le { |
| return "#bad header" |
| } |
| |
| while true { |
| mark = args.src.mark() |
| status =? this.decode_ihdr?(src: args.src) |
| if not this.ignore_checksum { |
| checksum_have = this.crc32.update_u32!(x: args.src.since(mark: mark)) |
| } |
| if status.is_ok() { |
| break |
| } |
| yield? status |
| } endwhile |
| |
| // Verify CRC-32 checksum. |
| checksum_want = args.src.read_u32be?() |
| if (not this.ignore_checksum) and (checksum_have <> checksum_want) { |
| return "#bad checksum" |
| } |
| |
| // Read up until an IDAT chunk. |
| while true { |
| this.chunk_length = args.src.read_u32be_as_u64?() |
| this.chunk_type = args.src.read_u32le?() |
| if not this.ignore_checksum { |
| this.crc32.reset!() |
| this.chunk_type_array[0] = ((this.chunk_type >> 0) & 0xFF) as base.u8 |
| this.chunk_type_array[1] = ((this.chunk_type >> 8) & 0xFF) as base.u8 |
| this.chunk_type_array[2] = ((this.chunk_type >> 16) & 0xFF) as base.u8 |
| this.chunk_type_array[3] = ((this.chunk_type >> 24) & 0xFF) as base.u8 |
| this.crc32.update_u32!(x: this.chunk_type_array[..]) |
| } |
| |
| if this.chunk_type == 'IDAT'le { |
| break |
| } |
| |
| while true { |
| mark = args.src.mark() |
| status =? this.decode_other_chunk?(src: args.src) |
| if not this.ignore_checksum { |
| checksum_have = this.crc32.update_u32!(x: args.src.since(mark: mark)) |
| } |
| if status.is_ok() { |
| break |
| } |
| yield? status |
| } endwhile |
| checksum_want = args.src.read_u32be?() |
| if (not this.ignore_checksum) and (checksum_have <> checksum_want) { |
| // TODO: uncomment and fix test_mimic_png_decode_19k_8bpp. |
| // return "#bad checksum" |
| } |
| |
| } endwhile |
| |
| if (this.color_type == 3) and (not this.seen_palette) { |
| return "#missing palette" |
| } |
| |
| this.frame_config_io_position = args.src.position() |
| |
| if args.dst <> nullptr { |
| args.dst.set!( |
| pixfmt: this.dst_pixfmt, |
| pixsub: 0, |
| width: this.width, |
| height: this.height, |
| first_frame_io_position: this.frame_config_io_position, |
| first_frame_is_opaque: false) |
| } |
| |
| this.call_sequence = 3 |
| } |
| |
| pri func decoder.decode_ihdr?(src: base.io_reader) { |
| var magic32 : base.u32 |
| var a32 : base.u32 |
| var a8 : base.u8 |
| |
| magic32 = args.src.read_u32le?() |
| if magic32 <> 'IHDR'le { |
| return "#bad header" |
| } |
| |
| a32 = args.src.read_u32be?() |
| if a32 >= 0x8000_0000 { |
| return "#bad header" |
| } else if a32 >= 0x0100_0000 { |
| return "#unsupported PNG file" |
| } |
| this.width = a32 |
| |
| a32 = args.src.read_u32be?() |
| if a32 >= 0x8000_0000 { |
| return "#bad header" |
| } else if a32 >= 0x0100_0000 { |
| return "#unsupported PNG file" |
| } |
| this.height = a32 |
| |
| // Depth. |
| a8 = args.src.read_u8?() |
| if a8 > 16 { |
| return "#bad header" |
| } |
| this.depth = a8 |
| |
| // Color. |
| this.color_type = args.src.read_u8?() |
| if this.color_type == 0 { |
| this.dst_pixfmt = base.PIXEL_FORMAT__Y |
| this.src_pixfmt = base.PIXEL_FORMAT__Y |
| if this.depth == 8 { |
| this.filter_distance = 1 |
| this.bytes_per_row = (this.width as base.u64) * 1 |
| } else { |
| return "#unsupported PNG file" |
| } |
| } else if this.color_type == 2 { |
| this.dst_pixfmt = base.PIXEL_FORMAT__BGR |
| this.src_pixfmt = base.PIXEL_FORMAT__RGB |
| if this.depth == 8 { |
| this.filter_distance = 3 |
| this.bytes_per_row = (this.width as base.u64) * 3 |
| } else { |
| return "#unsupported PNG file" |
| } |
| } else if this.color_type == 3 { |
| // TODO: s/BINARY/NONPREMUL/ and decode the tRNS chunk. |
| this.dst_pixfmt = base.PIXEL_FORMAT__INDEXED__BGRA_BINARY |
| this.src_pixfmt = base.PIXEL_FORMAT__INDEXED__BGRA_BINARY |
| if this.depth == 8 { |
| this.filter_distance = 1 |
| this.bytes_per_row = (this.width as base.u64) * 1 |
| } else { |
| return "#unsupported PNG file" |
| } |
| } else if this.color_type == 6 { |
| this.dst_pixfmt = base.PIXEL_FORMAT__BGRA_NONPREMUL |
| this.src_pixfmt = base.PIXEL_FORMAT__RGBA_NONPREMUL |
| if this.depth == 8 { |
| this.filter_distance = 4 |
| this.bytes_per_row = (this.width as base.u64) * 4 |
| } else { |
| return "#unsupported PNG file" |
| } |
| } else { |
| return "#unsupported PNG file" |
| } |
| this.workbuf_length = (this.height as base.u64) * (1 + this.bytes_per_row) |
| this.choose_filter_implementations!() |
| |
| // Compression. |
| a8 = args.src.read_u8?() |
| if a8 <> 0 { |
| return "#bad header" |
| } |
| |
| // Filter. |
| a8 = args.src.read_u8?() |
| if a8 <> 0 { |
| return "#bad header" |
| } |
| |
| // Interlace. |
| a8 = args.src.read_u8?() |
| if a8 == 0 { |
| // No-op. |
| } else if a8 == 1 { |
| return "#unsupported PNG file" |
| } else { |
| return "#bad header" |
| } |
| } |
| |
| pri func decoder.choose_filter_implementations!() { |
| // Filter 0 is a no-op. Filter 2, the up filter, should already vectorize |
| // easily by a good optimizing C compiler. |
| if this.filter_distance == 3 { |
| choose filter_1 = [filter_1_distance_3_fallback] |
| choose filter_3 = [filter_3_distance_3_fallback] |
| choose filter_4 = [ |
| filter_4_distance_3_sse128, |
| filter_4_distance_3_fallback] |
| } else if this.filter_distance == 4 { |
| choose filter_1 = [ |
| filter_1_distance_4_sse128, |
| filter_1_distance_4_fallback] |
| choose filter_3 = [ |
| filter_3_distance_4_sse128, |
| filter_3_distance_4_fallback] |
| choose filter_4 = [ |
| filter_4_distance_4_sse128, |
| filter_4_distance_4_fallback] |
| } |
| } |
| |
| pri func decoder.decode_other_chunk?(src: base.io_reader) { |
| if this.chunk_type == 'PLTE'le { |
| if this.seen_palette { |
| return "#bad chunk" |
| } |
| this.decode_plte?(src: args.src) |
| this.seen_palette = true |
| } else if this.chunk_type == 'tRNS'le { |
| // TODO. |
| args.src.skip?(n: this.chunk_length) |
| } else { |
| args.src.skip?(n: this.chunk_length) |
| } |
| } |
| |
| pri func decoder.decode_plte?(src: base.io_reader) { |
| var num_palette_entries : base.u32[..= 256] |
| var i : base.u32 |
| var argb : base.u32 |
| |
| if (this.chunk_length > 768) or ((this.chunk_length % 3) <> 0) { |
| return "#bad header" |
| } |
| num_palette_entries = (this.chunk_length as base.u32) / 3 |
| |
| while i < num_palette_entries { |
| assert i < 256 via "a < b: a < c; c <= b"(c: num_palette_entries) |
| // Convert from RGB (in memory order) to ARGB (in native u32 order) |
| // to BGRA (in memory order). |
| argb = args.src.read_u24be_as_u32?() |
| argb |= 0xFF00_0000 |
| this.src_palette[(4 * i) + 0] = ((argb >> 0) & 0xFF) as base.u8 |
| this.src_palette[(4 * i) + 1] = ((argb >> 8) & 0xFF) as base.u8 |
| this.src_palette[(4 * i) + 2] = ((argb >> 16) & 0xFF) as base.u8 |
| this.src_palette[(4 * i) + 3] = ((argb >> 24) & 0xFF) as base.u8 |
| i += 1 |
| } endwhile |
| |
| // Set the remaining palette entries to opaque black. |
| while i < 256 { |
| this.src_palette[(4 * i) + 0] = 0x00 |
| this.src_palette[(4 * i) + 1] = 0x00 |
| this.src_palette[(4 * i) + 2] = 0x00 |
| this.src_palette[(4 * i) + 3] = 0xFF |
| i += 1 |
| } endwhile |
| } |
| |
| pub func decoder.decode_frame_config?(dst: nptr base.frame_config, src: base.io_reader) { |
| if this.call_sequence < 3 { |
| this.decode_image_config?(dst: nullptr, src: args.src) |
| } else if this.call_sequence == 3 { |
| if this.frame_config_io_position <> args.src.position() { |
| return base."#bad restart" |
| } |
| } else if this.call_sequence == 4 { |
| this.call_sequence = 0xFF |
| 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: this.frame_config_io_position, |
| disposal: 0, |
| opaque_within_bounds: false, |
| overwrite_instead_of_blend: false, |
| background_color: 0x0000_0000) |
| } |
| |
| this.call_sequence = 4 |
| } |
| |
| 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 w : base.io_writer |
| var w_mark : base.u64 |
| var r_mark : base.u64 |
| var swizzler_status : base.status |
| var zlib_status : base.status |
| var checksum_have : base.u32 |
| var checksum_want : base.u32 |
| |
| if this.call_sequence < 4 { |
| this.decode_frame_config?(dst: nullptr, src: args.src) |
| } else if this.call_sequence == 4 { |
| // No-op. |
| } else { |
| return base."@end of data" |
| } |
| |
| this.workbuf_wi = 0 |
| while true { |
| if (this.workbuf_wi > this.workbuf_length) or ( |
| this.workbuf_length > args.workbuf.length()) { |
| return base."#bad workbuf length" |
| } |
| io_bind (io: w, data: args.workbuf[this.workbuf_wi .. this.workbuf_length]) { |
| io_limit (io: args.src, limit: this.chunk_length) { |
| w_mark = w.mark() |
| r_mark = args.src.mark() |
| zlib_status =? this.zlib.transform_io?( |
| dst: w, src: args.src, workbuf: this.util.empty_slice_u8()) |
| if not this.ignore_checksum { |
| this.crc32.update_u32!(x: args.src.since(mark: r_mark)) |
| } |
| this.chunk_length ~sat-= args.src.count_since(mark: r_mark) |
| this.workbuf_wi ~sat+= w.count_since(mark: w_mark) |
| } |
| } |
| |
| if zlib_status.is_ok() { |
| break |
| } else if zlib_status == base."$short write" { |
| return base."#too much data" |
| } else if zlib_status <> base."$short read" { |
| return zlib_status |
| } else if this.chunk_length == 0 { |
| // Verify the non-final IDAT chunk's CRC-32 checksum. |
| checksum_want = args.src.read_u32be?() |
| if not this.ignore_checksum { |
| checksum_have = this.crc32.update_u32!(x: this.util.empty_slice_u8()) |
| if checksum_have <> checksum_want { |
| return "#bad checksum" |
| } |
| } |
| |
| // The next chunk should be another IDAT. |
| this.chunk_length = args.src.read_u32be_as_u64?() |
| this.chunk_type = args.src.read_u32le?() |
| if this.chunk_type <> 'IDAT'le { |
| return "#bad chunk" |
| } |
| |
| // The 'IDAT'be is part of the next CRC-32 checksum's input. |
| if not this.ignore_checksum { |
| this.crc32.reset!() |
| this.chunk_type_array[0] = 'I' |
| this.chunk_type_array[1] = 'D' |
| this.chunk_type_array[2] = 'A' |
| this.chunk_type_array[3] = 'T' |
| this.crc32.update_u32!(x: this.chunk_type_array[..]) |
| } |
| continue |
| } else if args.src.length() > 0 { |
| return "#internal error: zlib decoder did not exhaust its input" |
| } |
| yield? base."$short read" |
| } endwhile |
| |
| // Verify the final IDAT chunk's CRC-32 checksum. |
| if not this.ignore_checksum { |
| if this.chunk_length > 0 { |
| // TODO: should this be a fatal error? |
| return base."#too much data" |
| } |
| checksum_have = this.crc32.update_u32!(x: this.util.empty_slice_u8()) |
| checksum_want = args.src.read_u32be?() |
| if checksum_have <> checksum_want { |
| return "#bad checksum" |
| } |
| } |
| |
| if this.workbuf_wi <> this.workbuf_length { |
| return base."#not enough data" |
| } else if 0 < args.workbuf.length() { |
| // For the top row, the Paeth filter (4) is equivalent to the Sub |
| // filter (1), but the Paeth implementation is simpler if it can assume |
| // that there is a previous row. |
| if args.workbuf[0] == 4 { |
| args.workbuf[0] = 1 |
| } |
| } |
| |
| swizzler_status = this.swizzler.prepare!( |
| dst_pixfmt: args.dst.pixel_format(), |
| dst_palette: args.dst.palette_or_else(fallback: this.dst_palette[..]), |
| src_pixfmt: this.util.make_pixel_format(repr: this.src_pixfmt), |
| src_palette: this.src_palette[..], |
| blend: args.blend) |
| if not swizzler_status.is_ok() { |
| return swizzler_status |
| } |
| swizzler_status = this.filter_and_swizzle!(dst: args.dst, workbuf: args.workbuf) |
| if not swizzler_status.is_ok() { |
| return swizzler_status |
| } |
| |
| this.call_sequence = 0xFF |
| } |
| |
| pri func decoder.filter_and_swizzle!(dst: ptr base.pixel_buffer, workbuf: slice base.u8) base.status { |
| var dst_pixfmt : base.pixel_format |
| var dst_bits_per_pixel : base.u32[..= 256] |
| var dst_bytes_per_pixel : base.u64[..= 32] |
| var dst_bytes_per_row : base.u64 |
| var dst_palette : slice base.u8 |
| var tab : table base.u8 |
| |
| var y : base.u32 |
| var dst : slice base.u8 |
| var filter : base.u8 |
| var curr_row : slice base.u8 |
| var prev_row : slice base.u8 |
| |
| // 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) as base.u64 |
| dst_bytes_per_row = (this.width as base.u64) * dst_bytes_per_pixel |
| dst_palette = args.dst.palette_or_else(fallback: this.dst_palette[..]) |
| tab = args.dst.plane(p: 0) |
| |
| while y < this.height { |
| assert y < 0xFFFF_FFFF via "a < b: a < c; c <= b"(c: this.height) |
| dst = tab.row(y: y) |
| if dst_bytes_per_row < dst.length() { |
| dst = dst[.. dst_bytes_per_row] |
| } |
| |
| if 1 > args.workbuf.length() { |
| return "#internal error: inconsistent workbuf length" |
| } |
| filter = args.workbuf[0] |
| args.workbuf = args.workbuf[1 ..] |
| if this.bytes_per_row > args.workbuf.length() { |
| return "#internal error: inconsistent workbuf length" |
| } |
| curr_row = args.workbuf[.. this.bytes_per_row] |
| args.workbuf = args.workbuf[this.bytes_per_row ..] |
| |
| if filter == 0 { |
| // No-op. |
| } else if filter == 1 { |
| this.filter_1!(curr: curr_row) |
| } else if filter == 2 { |
| this.filter_2!(curr: curr_row, prev: prev_row) |
| } else if filter == 3 { |
| this.filter_3!(curr: curr_row, prev: prev_row) |
| } else if filter == 4 { |
| this.filter_4!(curr: curr_row, prev: prev_row) |
| } else { |
| return "#bad filter" |
| } |
| |
| this.swizzler.swizzle_interleaved_from_slice!( |
| dst: dst, |
| dst_palette: dst_palette, |
| src: curr_row) |
| |
| prev_row = curr_row |
| y += 1 |
| } endwhile |
| |
| 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 > 3 { |
| return 1 |
| } |
| return 0 |
| } |
| |
| pub func decoder.num_decoded_frames() base.u64 { |
| if this.call_sequence > 4 { |
| return 1 |
| } |
| return 0 |
| } |
| |
| pub func decoder.restart_frame!(index: base.u64, io_position: base.u64) base.status { |
| if this.call_sequence < 3 { |
| return base."#bad call sequence" |
| } |
| if args.index <> 0 { |
| return base."#bad argument" |
| } |
| this.call_sequence = 3 |
| this.frame_config_io_position = args.io_position |
| return ok |
| } |
| |
| pub func decoder.set_report_metadata!(fourcc: base.u32, report: base.bool) { |
| // TODO. |
| } |
| |
| 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: this.workbuf_length, |
| max_incl: this.workbuf_length) |
| } |