Skip to content

Commit

Permalink
Merge pull request #154 from sdsykes/feature-add-jxl-support
Browse files Browse the repository at this point in the history
Add initial jxl support
  • Loading branch information
sdsykes committed Apr 1, 2024
2 parents 8d02ae8 + d1b1936 commit df84ad4
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 2 deletions.
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -13,7 +13,7 @@ But the image is not locally stored - it's on another asset server, or in the cl

You don't want to download the entire image to your app server - it could be many tens of kilobytes, or even megabytes just to get this information. For most common image types (GIF, PNG, BMP etc.), the size of the image is simply stored at the start of the file. For JPEG files it's a little bit more complex, but even so you do not need to fetch much of the image to find the size.

FastImage does this minimal fetch for image types GIF, JPEG, PNG, TIFF, BMP, ICO, CUR, PSD, SVG and WEBP. And it doesn't rely on installing external libraries such as RMagick (which relies on ImageMagick or GraphicsMagick) or ImageScience (which relies on FreeImage).
FastImage does this minimal fetch for image types GIF, JPEG, PNG, TIFF, BMP, ICO, CUR, PSD, SVG, WEBP and JXL. And it doesn't rely on installing external libraries such as RMagick (which relies on ImageMagick or GraphicsMagick) or ImageScience (which relies on FreeImage).

You only need supply the uri, and FastImage will do the rest.

Expand Down Expand Up @@ -196,6 +196,10 @@ ruby test/test.rb
- [Android by qstumn](https://github.com/qstumn/FastImageSize)
- [Flutter by ky1vstar](https://github.com/ky1vstar/fastimage.dart)

### Also of interest
- [C++ by xiaozhuai](https://github.com/xiaozhuai/imageinfo)
- [Rust by xiaozhuai](https://github.com/xiaozhuai/imageinfo-rs)

## Licence

MIT, see file "MIT-LICENSE"
84 changes: 83 additions & 1 deletion lib/fastimage.rb
Expand Up @@ -9,7 +9,7 @@
# No external libraries such as ImageMagick are used here, this is a very lightweight solution to
# finding image information.
#
# FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD, SVG and WEBP files.
# FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD, SVG, WEBP and JXL files.
#
# FastImage can also read files from the local filesystem by supplying the path instead of a uri.
# In this case FastImage reads the file in chunks of 256 bytes until
Expand Down Expand Up @@ -551,6 +551,8 @@ def parse_type
end
when '8B'
:psd
when "\xFF\x0A".b
:jxl
when "\0\0"
case @stream.peek(3).bytes.to_a.last
when 0
Expand All @@ -564,6 +566,10 @@ def parse_type
:heic
when "ftypmif1"
:heif
else
if @stream.peek(7)[4..-1] == 'JXL'
:jxl
end
end
# ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
when 1 then :ico
Expand Down Expand Up @@ -657,6 +663,8 @@ def read_boxes!(max_read_bytes = nil)
handle_ispe_box(box_size, index)
when "mdat"
@stream.skip(box_size)
when "jxlc"
handle_jxlc_box(box_size)
else
@stream.skip(box_size)
end
Expand Down Expand Up @@ -733,6 +741,11 @@ def handle_meta_box(box_size)
throw :finish
end

def handle_jxlc_box(box_size)
@final_size = JXL.new(@stream).read_size_header
throw :finish
end

def read_box_header!
size = read_uint32!
type = @stream.read(4)
Expand Down Expand Up @@ -772,6 +785,75 @@ def parse_size_for_heif
bmff.width_and_height
end

class JXL
LENGTHS = [9, 13, 18, 30]
MULTIPLIERS = [1, 1.2, Rational(4, 3), 1.5, Rational(16, 9), 1.25, 2]

def initialize(stream)
@stream = stream
@bit_counter = 0
end

def read_size_header
@words = @stream.read(6)[2..5].unpack('vv')

# small mode allows for values <= 256 that are divisible by 8
small = get_bits(1)
if small == 1
y = (get_bits(5) + 1) * 8
x = x_from_ratio(y)
if !x
x = (get_bits(5) + 1) * 8
end
return [x, y]
end

len = LENGTHS[get_bits(2)]
y = get_bits(len) + 1
x = x_from_ratio(y)
if !x
len = LENGTHS[get_bits(2)]
x = get_bits(len) + 1
end
[x, y]
end

def get_bits(size)
if @words.size < (@bit_counter + size) / 16 + 1
@words += @stream.read(4).unpack('vv')
end

dest_pos = 0
dest = 0
size.times do
word = @bit_counter / 16
source_pos = @bit_counter % 16
dest |= ((@words[word] & (1 << source_pos)) > 0 ? 1 : 0) << dest_pos
dest_pos += 1
@bit_counter += 1
end
dest
end

def x_from_ratio(y)
ratio = get_bits(3)
if ratio == 0
return nil
else
return (y * MULTIPLIERS[ratio - 1]).to_i
end
end
end

def parse_size_for_jxl
if @stream.peek(2) == "\xFF\x0A".b
JXL.new(@stream).read_size_header
else
bmff = IsoBmff.new(@stream)
bmff.width_and_height
end
end

class Gif # :nodoc:
def initialize(stream)
@stream = stream
Expand Down
Binary file added test/fixtures/isobmff.jxl
Binary file not shown.
Binary file added test/fixtures/naked.jxl
Binary file not shown.
2 changes: 2 additions & 0 deletions test/test.rb
Expand Up @@ -58,6 +58,8 @@
"avif/fox.avif" => [:avif, [1204, 799]],
"avif/kimono.avif" => [:avif, [722, 1024]],
"avif/red_green_flash.avif" => [:avif, [256, 256]],
"isobmff.jxl" => [:jxl, [1280,1600]],
"naked.jxl" => [:jxl, [1000,1000]],
}

BadFixtures = [
Expand Down

0 comments on commit df84ad4

Please sign in to comment.