mirror of https://github.com/docusealco/docuseal
parent
8a10852fe2
commit
bfa244c8fa
@ -0,0 +1,170 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module LoadBmp
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(bmp_bytes)
|
||||
bmp_bytes = bmp_bytes.b
|
||||
|
||||
header_data = parse_bmp_headers(bmp_bytes)
|
||||
|
||||
raw_pixel_data_from_file = extract_raw_pixel_data_blob(
|
||||
bmp_bytes,
|
||||
header_data[:pixel_data_offset],
|
||||
header_data[:bmp_stride],
|
||||
header_data[:height]
|
||||
)
|
||||
|
||||
final_pixel_data = prepare_unpadded_pixel_data_string(
|
||||
raw_pixel_data_from_file,
|
||||
header_data[:bpp],
|
||||
header_data[:width],
|
||||
header_data[:height],
|
||||
header_data[:bmp_stride]
|
||||
)
|
||||
|
||||
bands = header_data[:bpp] / 8
|
||||
|
||||
unless header_data[:bpp] == 24 || header_data[:bpp] == 32
|
||||
raise ArgumentError, "Conversion for #{header_data[:bpp]}-bpp BMP not implemented."
|
||||
end
|
||||
|
||||
image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
|
||||
|
||||
image = image.flip(:vertical) if header_data[:orientation] == -1
|
||||
|
||||
image_rgb =
|
||||
if bands == 3
|
||||
Vips::Image.bandjoin([image[2], image[1], image[0]])
|
||||
elsif bands == 4
|
||||
Vips::Image.bandjoin([image[2], image[1], image[0], image[3]])
|
||||
else
|
||||
image
|
||||
end
|
||||
|
||||
if image_rgb.interpretation == :multiband || image_rgb.interpretation == :'b-w'
|
||||
image_rgb = image_rgb.copy(interpretation: :srgb)
|
||||
end
|
||||
|
||||
image_rgb
|
||||
end
|
||||
|
||||
def parse_bmp_headers(bmp_bytes)
|
||||
raise ArgumentError, 'BMP data too short for file header (14 bytes).' if bmp_bytes.bytesize < 14
|
||||
|
||||
signature, pixel_data_offset = bmp_bytes.unpack('a2@10L<')
|
||||
|
||||
raise ArgumentError, "Not a valid BMP file (invalid signature 'BM')." if signature != 'BM'
|
||||
|
||||
raise ArgumentError, 'BMP data too short for info header size field (4 bytes).' if bmp_bytes.bytesize < (14 + 4)
|
||||
|
||||
info_header_size = bmp_bytes.unpack1('@14L<')
|
||||
|
||||
min_expected_info_header_size = 40
|
||||
|
||||
if info_header_size < min_expected_info_header_size
|
||||
raise ArgumentError,
|
||||
"Unsupported BMP info header size: #{info_header_size}. Expected at least #{min_expected_info_header_size}."
|
||||
end
|
||||
|
||||
header_and_info_header_min_bytes = 14 + min_expected_info_header_size
|
||||
|
||||
if bmp_bytes.bytesize < header_and_info_header_min_bytes
|
||||
raise ArgumentError,
|
||||
'BMP data too short for essential BITMAPINFOHEADER fields ' \
|
||||
"(requires #{header_and_info_header_min_bytes} bytes total)."
|
||||
end
|
||||
|
||||
_header_size_check, width, raw_height_from_header, planes, bpp, compression =
|
||||
bmp_bytes.unpack('@14L<l<l<S<S<L<')
|
||||
|
||||
height = 0
|
||||
orientation = -1
|
||||
|
||||
if raw_height_from_header.negative?
|
||||
height = -raw_height_from_header
|
||||
orientation = 1
|
||||
else
|
||||
height = raw_height_from_header
|
||||
end
|
||||
|
||||
raise ArgumentError, 'BMP width must be positive.' if width <= 0
|
||||
raise ArgumentError, 'BMP height must be positive.' if height <= 0
|
||||
|
||||
if compression != 0
|
||||
raise ArgumentError,
|
||||
"Unsupported BMP compression type: #{compression}. Only uncompressed (0) is supported."
|
||||
end
|
||||
|
||||
unless [24, 32].include?(bpp)
|
||||
raise ArgumentError, "Unsupported BMP bits per pixel: #{bpp}. Only 24-bit and 32-bit are supported."
|
||||
end
|
||||
|
||||
raise ArgumentError, "Unsupported BMP planes: #{planes}. Expected 1." if planes != 1
|
||||
|
||||
bytes_per_pixel = bpp / 8
|
||||
row_size_unpadded = width * bytes_per_pixel
|
||||
bmp_stride = (row_size_unpadded + 3) & ~3
|
||||
|
||||
{
|
||||
width:,
|
||||
height:,
|
||||
bpp:,
|
||||
pixel_data_offset:,
|
||||
bmp_stride:,
|
||||
orientation:
|
||||
}
|
||||
end
|
||||
|
||||
def extract_raw_pixel_data_blob(bmp_bytes, pixel_data_offset, bmp_stride, height)
|
||||
expected_pixel_data_size = bmp_stride * height
|
||||
|
||||
if pixel_data_offset + expected_pixel_data_size > bmp_bytes.bytesize
|
||||
actual_available = bmp_bytes.bytesize - pixel_data_offset
|
||||
actual_available = 0 if actual_available.negative?
|
||||
raise ArgumentError,
|
||||
"Pixel data segment (offset #{pixel_data_offset}, expected size #{expected_pixel_data_size}) " \
|
||||
"exceeds BMP file size (#{bmp_bytes.bytesize}). " \
|
||||
"Only #{actual_available} bytes available after offset."
|
||||
end
|
||||
|
||||
raw_pixel_data_from_file = bmp_bytes.byteslice(pixel_data_offset, expected_pixel_data_size)
|
||||
|
||||
if raw_pixel_data_from_file.nil? || raw_pixel_data_from_file.bytesize < expected_pixel_data_size
|
||||
raise ArgumentError,
|
||||
"Extracted pixel data is smaller (#{raw_pixel_data_from_file&.bytesize || 0} bytes) " \
|
||||
"than expected (#{expected_pixel_data_size} bytes based on stride and height)."
|
||||
end
|
||||
|
||||
raw_pixel_data_from_file
|
||||
end
|
||||
|
||||
def prepare_unpadded_pixel_data_string(raw_pixel_data_from_file, bpp, width, height, bmp_stride)
|
||||
bytes_per_pixel = bpp / 8
|
||||
actual_row_width_bytes = width * bytes_per_pixel
|
||||
|
||||
unpadded_rows = Array.new(height)
|
||||
current_offset_in_blob = 0
|
||||
|
||||
height.times do |i|
|
||||
if current_offset_in_blob + actual_row_width_bytes > raw_pixel_data_from_file.bytesize
|
||||
raise ArgumentError,
|
||||
"Not enough data in pixel blob for row #{i}. Offset #{current_offset_in_blob}, " \
|
||||
"row width #{actual_row_width_bytes}, blob size #{raw_pixel_data_from_file.bytesize}"
|
||||
end
|
||||
|
||||
unpadded_row_slice = raw_pixel_data_from_file.byteslice(current_offset_in_blob, actual_row_width_bytes)
|
||||
|
||||
if unpadded_row_slice.nil? || unpadded_row_slice.bytesize < actual_row_width_bytes
|
||||
raise ArgumentError, "Failed to slice a full unpadded row from pixel data blob for row #{i}."
|
||||
end
|
||||
|
||||
unpadded_rows[i] = unpadded_row_slice
|
||||
current_offset_in_blob += bmp_stride
|
||||
end
|
||||
|
||||
unpadded_rows.join
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
@ -0,0 +1,209 @@
|
||||
# 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
|
||||
Loading…
Reference in new issue