You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/lib/load_ico.rb

210 lines
6.7 KiB

# frozen_string_literal: true
module LoadIco
BI_RGB = 0
module_function
# rubocop:disable Metrics
def call(ico_bytes)
io = StringIO.new(ico_bytes)
_reserved, type, count = io.read(6)&.unpack('S<S<S<')
raise ArgumentError, 'Unable to load' unless type == 1 && count&.positive?
ico_entries_parsed = []
count.times do
entry_bytes = io.read(16)
raise ArgumentError, 'Unable to load' unless entry_bytes && entry_bytes.bytesize == 16
width_byte, height_byte, _num_colors_palette, _rsvd_entry, _planes_icon_entry, bpp_icon_entry,
img_data_size, img_data_offset = entry_bytes.unpack('CCCCS<S<L<L<')
width = width_byte.zero? ? 256 : width_byte
height = height_byte.zero? ? 256 : height_byte
sort_bpp = bpp_icon_entry.zero? ? 32 : bpp_icon_entry
ico_entries_parsed << {
width: width, height: height,
sort_bpp: sort_bpp,
size: img_data_size, offset: img_data_offset
}
end
best_entry = ico_entries_parsed.min_by { |e| [-e[:width] * e[:height], -e[:sort_bpp]] }
raise ArgumentError, 'Unable to load' unless best_entry
io.seek(best_entry[:offset])
image_data_bytes = io.read(best_entry[:size])
raise ArgumentError, 'Unable to load' unless image_data_bytes && image_data_bytes.bytesize == best_entry[:size]
image = load_image_entry(image_data_bytes, best_entry[:width], best_entry[:height])
raise ArgumentError, 'Unable to load' unless image
image
end
def load_image_entry(image_data_bytes, ico_entry_width, ico_entry_height)
dib_io = StringIO.new(image_data_bytes)
dib_header_size_arr = dib_io.read(4)&.unpack('L<')
return nil unless dib_header_size_arr
dib_header_size = dib_header_size_arr.first
return nil unless dib_header_size && dib_header_size >= 40
dib_params_bytes = dib_io.read(36)
return nil unless dib_params_bytes && dib_params_bytes.bytesize == 36
dib_width, dib_actual_height_field, dib_planes, dib_bpp,
dib_compression, _dib_image_size, _xpels, _ypels,
dib_clr_used, _dib_clr_important = dib_params_bytes.unpack('l<l<S<S<L<L<l<l<L<L<')
return nil unless dib_width && dib_actual_height_field && dib_planes && dib_bpp && dib_compression && dib_clr_used
return nil unless dib_width == ico_entry_width
image_pixel_height = ico_entry_height
expected_dib_height_no_mask = image_pixel_height
expected_dib_height_with_mask = image_pixel_height * 2
actual_dib_pixel_rows_abs = dib_actual_height_field.abs
unless actual_dib_pixel_rows_abs == expected_dib_height_no_mask ||
actual_dib_pixel_rows_abs == expected_dib_height_with_mask
return nil
end
return nil unless dib_planes == 1
return nil unless dib_compression == BI_RGB
return nil unless [1, 4, 8, 24, 32].include?(dib_bpp)
has_and_mask = (actual_dib_pixel_rows_abs == expected_dib_height_with_mask) && (dib_bpp < 32)
dib_io.seek(dib_header_size, IO::SEEK_SET)
palette = []
if dib_bpp <= 8
num_palette_entries = dib_clr_used.zero? ? (1 << dib_bpp) : dib_clr_used
num_palette_entries.times do
palette_color_bytes = dib_io.read(4)
return nil unless palette_color_bytes && palette_color_bytes.bytesize == 4
b, g, r, _a_reserved = palette_color_bytes.unpack('CCCC')
palette << [r, g, b, 255]
end
end
xor_mask_data_offset = dib_io.pos
xor_scanline_stride = (((dib_width * dib_bpp) + 31) / 32) * 4
and_mask_data_offset = 0
and_scanline_stride = 0
if has_and_mask
and_mask_data_offset = xor_mask_data_offset + (image_pixel_height * xor_scanline_stride)
and_scanline_stride = (((dib_width * 1) + 31) / 32) * 4
end
flat_rgba_pixels = []
(0...image_pixel_height).each do |y_row|
y_dib_row = image_pixel_height - 1 - y_row
dib_io.seek(xor_mask_data_offset + (y_dib_row * xor_scanline_stride))
xor_scanline_bytes = dib_io.read(xor_scanline_stride)
min_xor_bytes_needed = ((dib_width * dib_bpp) + 7) / 8
return nil unless xor_scanline_bytes && xor_scanline_bytes.bytesize >= min_xor_bytes_needed
and_mask_bits_for_row = []
if has_and_mask
dib_io.seek(and_mask_data_offset + (y_dib_row * and_scanline_stride))
and_mask_scanline_bytes = dib_io.read(and_scanline_stride)
min_and_bytes_needed = ((dib_width * 1) + 7) / 8
return nil unless and_mask_scanline_bytes && and_mask_scanline_bytes.bytesize >= min_and_bytes_needed
(0...dib_width).each do |x_pixel|
byte_index = x_pixel / 8
bit_index_in_byte = 7 - (x_pixel % 8)
byte_val = and_mask_scanline_bytes.getbyte(byte_index)
and_mask_bits_for_row << ((byte_val >> bit_index_in_byte) & 1)
end
end
(0...dib_width).each do |x_pixel|
r = 0
g = 0
b = 0
a = 255
case dib_bpp
when 32
offset = x_pixel * 4
blue = xor_scanline_bytes.getbyte(offset)
green = xor_scanline_bytes.getbyte(offset + 1)
red = xor_scanline_bytes.getbyte(offset + 2)
alpha_val = xor_scanline_bytes.getbyte(offset + 3)
r = red
g = green
b = blue
a = alpha_val
when 24
offset = x_pixel * 3
blue = xor_scanline_bytes.getbyte(offset)
green = xor_scanline_bytes.getbyte(offset + 1)
red = xor_scanline_bytes.getbyte(offset + 2)
r = red
g = green
b = blue
when 8
idx = xor_scanline_bytes.getbyte(x_pixel)
r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
r = r_p
g = g_p
b = b_p
a = a_p
when 4
byte_val = xor_scanline_bytes.getbyte(x_pixel / 2)
idx = (x_pixel.even? ? (byte_val >> 4) : (byte_val & 0x0F))
r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
r = r_p
g = g_p
b = b_p
a = a_p
when 1
byte_val = xor_scanline_bytes.getbyte(x_pixel / 8)
idx = (byte_val >> (7 - (x_pixel % 8))) & 1
r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
r = r_p
g = g_p
b = b_p
a = a_p
end
if has_and_mask && !and_mask_bits_for_row.empty?
a = and_mask_bits_for_row[x_pixel] == 1 ? 0 : 255
end
flat_rgba_pixels.push(r, g, b, a)
end
end
pixel_data_string = flat_rgba_pixels.pack('C*')
expected_bytes = dib_width * image_pixel_height * 4
return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive?
Vips::Image.new_from_memory(
pixel_data_string,
dib_width,
image_pixel_height,
4,
:uchar
)
end
# rubocop:enable Metrics
end