| // 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. |
| |
| pub status "#bad header" |
| pub status "#unsupported BMP file" |
| |
| pri status "#internal error: inconsistent swizzle count" |
| |
| pub const DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE : base.u64 = 0 |
| |
| pub struct decoder? implements base.image_decoder( |
| width : base.u32[..= 0x7FFF_FFFF], |
| height : base.u32[..= 0x7FFF_FFFF], |
| |
| call_sequence : base.u8, |
| |
| top_down : base.bool, |
| bits_per_pixel : base.u8[..= 32], |
| pad_per_row : base.u32[..= 3], |
| bytes_per_row : base.u64[..= 0x0000_0001_FFFF_FFFC], // 4 * 0x7FFF_FFFF |
| bytes_total : base.u64[..= 0xFFFF_FFFC_0000_0004], // 4 * 0x7FFF_FFFF * 0x7FFF_FFFF |
| pixfmt : base.pixel_format, |
| |
| padding : base.u32, |
| |
| mask_r : base.u32, |
| mask_g : base.u32, |
| mask_b : base.u32, |
| mask_a : base.u32, |
| |
| frame_config_io_position : base.u64, |
| |
| dst_x : base.u32, |
| dst_y : base.u32, |
| dst_y_end : base.u32, |
| dst_y_inc : base.u32, |
| |
| stash : array[4] base.u8, |
| num_stashed : base.u8[..= 4], |
| pending_pad : base.u8[..= 3], |
| |
| swizzler : base.pixel_swizzler, |
| util : base.utility, |
| ) |
| |
| 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 magic : base.u32 |
| var bitmap_info_len : base.u32 |
| var width : base.u32 |
| var height : base.u32 |
| var planes : base.u32 |
| var bits_per_pixel : base.u32 |
| var compression : base.u32 |
| |
| if this.call_sequence <> 0 { |
| return base."#bad call sequence" |
| } |
| |
| // Read the BITMAPFILEHEADER (14 bytes). |
| |
| magic = args.src.read_u16le_as_u32?() |
| if magic <> 'BM'le { |
| return "#bad header" |
| } |
| |
| args.src.skip32?(n: 8) |
| |
| this.padding = args.src.read_u32le?() |
| if this.padding < 14 { |
| return "#bad header" |
| } |
| this.padding -= 14 |
| |
| // Read the BITMAPINFOHEADER (version 3 / 4 / 5 is 40 / 108 / 124 bytes). |
| |
| bitmap_info_len = args.src.read_u32le?() |
| if (bitmap_info_len <> 40) and (bitmap_info_len <> 108) and (bitmap_info_len <> 124) { |
| return "#unsupported BMP file" |
| } |
| if this.padding < bitmap_info_len { |
| return "#bad header" |
| } |
| this.padding -= bitmap_info_len |
| |
| width = args.src.read_u32le?() |
| if width >= 0x8000_0000 { |
| return "#bad header" |
| } |
| this.width = width |
| |
| height = args.src.read_u32le?() |
| if height == 0x8000_0000 { |
| return "#bad header" |
| } else if height >= 0x8000_0000 { |
| // The &0x7FFF_FFFF is redundant, but proves to the compiler that the |
| // result is within this.height's refined bounds. |
| this.height = (0 ~mod- height) & 0x7FFF_FFFF |
| this.top_down = true |
| } else { |
| this.height = height |
| } |
| |
| planes = args.src.read_u16le_as_u32?() |
| if planes <> 1 { |
| return "#unsupported BMP file" |
| } |
| |
| bits_per_pixel = args.src.read_u16le_as_u32?() |
| if bits_per_pixel == 24 { |
| this.bits_per_pixel = 24 |
| // 3 bytes per pixel, but row lengths are rounded up to multiples of 4. |
| // The "((x + 3) >> 2) << 2" dance rounds x up. |
| this.bytes_per_row = ((((this.width as base.u64) * 3) + 3) >> 2) << 2 |
| this.pad_per_row = this.width & 3 |
| // TODO: a Wuffs (not just C) name for the |
| // WUFFS_BASE__PIXEL_FORMAT__BGR magic pixfmt constant. |
| this.pixfmt = this.util.make_pixel_format(repr: 0x4000_0888) |
| } else if bits_per_pixel == 32 { |
| this.bits_per_pixel = 32 |
| this.bytes_per_row = (this.width as base.u64) * 4 |
| this.pad_per_row = 0 |
| // TODO: a Wuffs (not just C) name for the |
| // WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL magic pixfmt constant. |
| this.pixfmt = this.util.make_pixel_format(repr: 0x4500_8888) |
| } else { |
| // TODO: support other bits_per_pixel's. |
| return "#unsupported BMP file" |
| } |
| this.bytes_total = this.bytes_per_row * (this.height as base.u64) |
| |
| compression = args.src.read_u32le?() |
| |
| // We've already read 20 bytes from the BITMAPINFOHEADER: size (4), width |
| // (4), height (4), planes (2), bpp (2), compression (4). Skip the rest of |
| // the version 3 BITMAPINFOHEADER (whose total size is 40). |
| args.src.skip32?(n: 40 - 20) |
| |
| if bitmap_info_len >= 108 { |
| this.mask_r = args.src.read_u32le?() |
| this.mask_g = args.src.read_u32le?() |
| this.mask_b = args.src.read_u32le?() |
| this.mask_a = args.src.read_u32le?() |
| |
| // If compression is 3 (BITFIELDS) but the explicit masks are what the |
| // implicit masks are for no compression, treat it as no compression. |
| if (compression == 3) and |
| (this.mask_r == 0x00FF_0000) and |
| (this.mask_g == 0x0000_FF00) and |
| (this.mask_b == 0x0000_00FF) and |
| (this.mask_a == 0xFF00_0000) { |
| compression = 0 |
| } |
| |
| // Skip the rest of the BITMAPINFOHEADER. We've already read (40 + (4 * |
| // 4)) bytes. |
| args.src.skip32?(n: bitmap_info_len - 56) |
| } |
| |
| if compression <> 0 { |
| // TODO: support compression. |
| return "#unsupported BMP file" |
| } |
| |
| this.frame_config_io_position = args.src.position() |
| |
| if args.dst <> nullptr { |
| // TODO: a Wuffs (not just C) name for the |
| // WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL magic pixfmt constant. |
| args.dst.set!( |
| pixfmt: 0x4500_8888, // TODO: this.pixfmt instead? |
| pixsub: 0, |
| width: this.width, |
| height: this.height, |
| first_frame_io_position: this.frame_config_io_position, |
| first_frame_is_opaque: true) |
| } |
| |
| this.call_sequence = 1 |
| } |
| |
| pub func decoder.decode_frame_config?(dst: nptr base.frame_config, src: base.io_reader) { |
| if this.call_sequence < 1 { |
| this.decode_image_config?(dst: nullptr, src: args.src) |
| } else if this.call_sequence == 1 { |
| if this.frame_config_io_position <> args.src.position() { |
| return base."#bad restart" |
| } |
| } else if this.call_sequence == 2 { |
| this.skip_frame?(src: args.src) |
| return base."@end of data" |
| } else { |
| return base."@end of data" |
| } |
| |
| if args.dst <> nullptr { |
| args.dst.update!(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: true, |
| overwrite_instead_of_blend: false, |
| background_color: 0xFF00_0000) |
| } |
| |
| this.call_sequence = 2 |
| } |
| |
| 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 |
| var bytes_remaining : base.u64 |
| var n : base.u64 |
| var src : slice base.u8 |
| |
| if this.call_sequence < 2 { |
| this.decode_frame_config?(dst: nullptr, src: args.src) |
| } else if this.call_sequence == 2 { |
| // No-op. |
| } else { |
| return base."@end of data" |
| } |
| |
| args.src.skip32?(n: this.padding) |
| |
| if (this.width > 0) and (this.height > 0) { |
| this.dst_x = 0 |
| if this.top_down { |
| this.dst_y = 0 |
| this.dst_y_end = this.height |
| this.dst_y_inc = 1 |
| } else { |
| this.dst_y = this.height ~mod- 1 |
| this.dst_y_end = 0xFFFF_FFFF // -1 as a base.u32. |
| this.dst_y_inc = 0xFFFF_FFFF // -1 as a base.u32. |
| } |
| |
| status = this.swizzler.prepare!( |
| dst_pixfmt: args.dst.pixel_format(), |
| dst_palette: args.dst.palette(), |
| src_pixfmt: this.pixfmt, |
| src_palette: this.util.empty_slice_u8(), |
| blend: args.blend) |
| if not status.is_ok() { |
| return status |
| } |
| |
| bytes_remaining = this.bytes_total |
| while true { |
| n = args.src.available() |
| if bytes_remaining >= n { |
| bytes_remaining -= n |
| } else { |
| n = bytes_remaining |
| bytes_remaining = 0 |
| } |
| src = args.src.take!(n: n) |
| status = this.swizzle!(dst: args.dst, src: src) |
| if status.is_ok() { |
| break |
| } else if status.is_suspension() { |
| yield? status |
| } else { |
| return status |
| } |
| } endwhile |
| } |
| |
| this.call_sequence = 3 |
| } |
| |
| pri func decoder.swizzle!(dst: ptr base.pixel_buffer, src: 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 src_bytes_per_pixel : base.u8[..= 4] |
| var tab : table base.u8 |
| var dst : slice base.u8 |
| var i : base.u64 |
| var n : 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) as base.u64 |
| dst_bytes_per_row = (this.width as base.u64) * dst_bytes_per_pixel |
| src_bytes_per_pixel = (this.bits_per_pixel / 8) as base.u8 |
| tab = args.dst.plane(p: 0) |
| |
| // Handle the case where the I/O suspension occurred in the middle of the |
| // end-of-row padding. |
| while this.pending_pad > 0 { |
| if args.src.length() <= 0 { |
| return base."$short read" |
| } |
| this.pending_pad -= 1 |
| args.src = args.src[1 ..] |
| } endwhile |
| |
| // Handle the case where the I/O suspension occurred in the middle of a |
| // source pixel. |
| if this.num_stashed <> 0 { |
| while this.num_stashed < src_bytes_per_pixel { |
| assert this.num_stashed < 4 via "a < b: a < c; c <= b"(c: src_bytes_per_pixel) |
| if args.src.length() <= 0 { |
| return base."$short read" |
| } |
| this.stash[this.num_stashed] = args.src[0] |
| this.num_stashed += 1 |
| args.src = args.src[1 ..] |
| } endwhile |
| |
| // Write the single pixel. |
| dst = tab.row(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 |
| if i < dst.length() { |
| this.swizzler.swizzle_interleaved!( |
| dst: dst[i ..], |
| dst_palette: this.util.empty_slice_u8(), |
| src: this.stash[.. this.num_stashed]) |
| this.dst_x ~sat+= 1 |
| } |
| |
| this.num_stashed = 0 |
| } |
| |
| while true { |
| if this.dst_x == this.width { |
| this.dst_x = 0 |
| this.dst_y ~mod+= this.dst_y_inc |
| |
| if this.pad_per_row <> 0 { |
| n = this.pad_per_row as base.u64 |
| if n <= args.src.length() { |
| args.src = args.src[n ..] |
| } else { |
| this.pending_pad = ((n - args.src.length()) & 3) as base.u8 |
| return base."$short read" |
| } |
| } |
| } |
| |
| if this.dst_y == this.dst_y_end { |
| break |
| } |
| |
| dst = tab.row(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 |
| if i < dst.length() { |
| n = this.swizzler.swizzle_interleaved!( |
| dst: dst[i ..], |
| dst_palette: this.util.empty_slice_u8(), |
| src: args.src) |
| this.dst_x ~sat+= (n & 0xFFFF_FFFF) as base.u32 |
| n = (n & 0xFFFF_FFFF) * (src_bytes_per_pixel as base.u64) |
| if n <= args.src.length() { |
| args.src = args.src[n ..] |
| } else { |
| return "#internal error: inconsistent swizzle count" |
| } |
| } |
| |
| // Suspend if we didn't complete the row, potentially in the middle of |
| // a source pixel. |
| if this.dst_x < this.width { |
| while (this.num_stashed < src_bytes_per_pixel) and (args.src.length() > 0) { |
| assert this.num_stashed < 4 via "a < b: a < c; c <= b"(c: src_bytes_per_pixel) |
| this.stash[this.num_stashed] = args.src[0] |
| this.num_stashed += 1 |
| args.src = args.src[1 ..] |
| } endwhile |
| return base."$short read" |
| } |
| } endwhile |
| |
| return ok |
| } |
| |
| pri func decoder.skip_frame?(src: base.io_reader) { |
| args.src.skip32?(n: this.padding) |
| args.src.skip?(n: this.bytes_total) |
| |
| this.call_sequence = 3 |
| } |
| |
| 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 > 1 { |
| return 1 |
| } |
| return 0 |
| } |
| |
| pub func decoder.num_decoded_frames() base.u64 { |
| if this.call_sequence > 2 { |
| return 1 |
| } |
| return 0 |
| } |
| |
| pub func decoder.restart_frame!(index: base.u64, io_position: base.u64) base.status { |
| if this.call_sequence == 0 { |
| return base."#bad call sequence" |
| } |
| if args.index <> 0 { |
| return base."#bad argument" |
| } |
| this.call_sequence = 1 |
| this.frame_config_io_position = args.io_position |
| return ok |
| } |
| |
| pub func decoder.set_report_metadata!(fourcc: base.u32, report: base.bool) { |
| // No-op. BMP 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) |
| } |