sdsykes / fastimage

FastImage finds the size or type of an image given its uri by fetching as little as needed
http://github.com/sdsykes/fastimage/tree/master
MIT License
1.37k stars 115 forks source link

JPEG XL not supported #149

Closed dkam closed 5 months ago

dkam commented 11 months ago

FastImage doesn't support the JPEG XL format.

FastImage.new('https://jpegxl.info/logo.jxl').size
nil
FastImage.new('https://jpegxl.info/logo.jxl').type
nil

Thanks very much for writing the gem!

dkam commented 11 months ago

Notes

There are two formats for JXL files, a naked stream and an ISO Bmff contained format.

The logo for the https://jpegxl.info site is a naked stream, and there is a ISOBMFF formatted image here.

Type

With the naked stream, in the parse_type method we can:

when "\xFF\x0A".b
      :jxl

We can detect the ISOBMFF format where the \0\0 is matched - probably don't need to match both JXL and ftypjxl.

when "\0\0"
      case @stream.peek(3).bytes.to_a.last
      when 0
        # http://www.ftyps.com/what.html
        case @stream.peek(12)[4..-1]
        when "ftypavif"
          :avif
        when "ftypavis"
          :avif
        when "ftypheic"
          :heic
        when "ftypmif1"
          :heif
        else
          if @stream.peek(7)[4..-1] == 'JXL'  || @stream.peek(32)[16..22] == 'ftypjxl'
            :jxl
          end
        end
      # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
      when 1 then :ico
      when 2 then :cur
      end

Size

A user in the JXL Discord pointed me to their Python parser.

Another user mentioned, that for the naked stream, the image dimensions come directly after the signature of ff 0a.

The reference implementation in C++ code for extracting the dimensions is here. There was also this comment you basically have to find the jxlc box or the first jxlp box to find the actual codestream (say if you want to get the image dimensions)

sdsykes commented 5 months ago

The type detection works fine.

The size detection is rather complex, but at least the size appears to be present at the start of the jxlc stream. By looking at the reference decoder, I see we need to read 16 bit values and read them in bit groups starting from the least significant bits.

The bit groups that need to be read look like:

-- this is only used if xsize and ysize <= 256 and divisible by 8 -- values are stored divided by 8 with 1 subtracted, so fit in 5 bits [1] [Y bits(5)] [ratio bits(3)] [X bits(5) if nil ratio]

or

[0] [Y selector bits(2)] [Y bits(9/13/18/30)] [ratio bits(3)] [X selector bits(2) if nil ratio] [X bits(9/13/18/30) if nil ratio]

Size values can't be zero, so are stored with 1 subtracted from them.

Ratio can be 0 (x size must be given) or: x-y 1) 1-1 2) 12-10 3) 4-3 4) 3-2 5) 16-9 6) 5-4 7) 2-1

Now to implement it all!

dkam commented 1 month ago

Thanks! Any chance you could release a version with this update?