mirror of https://github.com/docusealco/docuseal
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.
210 lines
6.7 KiB
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
|