1

I am trying to read the WebP image header, according to the WebP Container Specification of Extended File Format.

fun get24bit(data: ByteArray, index: Int): Int {
    return ((data[0 + index].toInt()) or (data[1 + index].toInt() shl 8) or (data[2 + index].toInt() shl 16))
}

fun get32bit(data: ByteArray, index: Int): Int {
    return get24bit(data, index) or (data[3 + index].toInt() shl 24)
}

// data -> File(fileName).readBytes() for testing purpose
fun webpExtract(data: ByteArray) {
    println(String(data.copyOfRange(0, 4)))
    println("Size: ${get32bit(data, 4)}")
    println(String(data.copyOfRange(8, 12)))
    println(String(data.copyOfRange(12, 16)))
    // 16, 17, 18, 19 reserved

    val width = 1 + get24bit(data, 20)
    val height = 1 + get24bit(data, 23)

    println("Width: $width, Height: $height")
}

And the outputs are:

RIFF
Size: -52
WEBP
VP8X
Width: 17, Height: 32513

The String outputs are alright, but the Size is getting negative and Width and Heights are wrong i.e. They should be 128 and 128 respectively (for the test image I've used).

Is there something wrong in the code? I am not able to figure out what's the problem.

I've also verified the actual C++ implementation here in github. My code does the same bit shifting, but the results are not correct. As far as I know, left shifting does not has anything to do with unsigned and signed right?

Animesh Sahu
  • 7,445
  • 2
  • 21
  • 49
  • consider this: https://stackoverflow.com/questions/38651192/how-to-correctly-handle-byte-values-greater-than-127-in-kotlin – 9dan Oct 27 '20 at 04:59
  • @9dan thanks, the size comes out to be correct (+ve value and correct), but the Width and Heights are still incorrect :( – Animesh Sahu Oct 27 '20 at 05:09
  • @AdamMillerchip [Spec](https://developers.google.com/speed/webp/docs/riff_container#terminology_basics) says it is in little endian order :^) – Animesh Sahu Oct 27 '20 at 06:01
  • Yeah I deleted the comment after reading that. It doesn't make sense that your width and height are different with the same code, so there must be something strange going on. It would be helpful if you could log out your `data` values (in binary) before you do the shifts. – Adam Millerchip Oct 27 '20 at 06:03
  • @AdamMillerchip I've took byte values and get24bit values till 50 index, I don't see much of a pattern, maybe you can help me https://gist.github.com/Animeshz/affe8c7e54c2949e33444be904e9bffa (this time I used 400x400 image ) . – Animesh Sahu Oct 27 '20 at 06:10
  • @AdamMillerchip Ah, I actually got it, the values of height and widths were at index 24 and 27... Verified using 3 different resolution images. – Animesh Sahu Oct 27 '20 at 06:14
  • yes, looking at ParseVP8X function from webp_dec.c, width is 'VP8X' after 8 offset, that is 24. – 9dan Oct 27 '20 at 06:25

2 Answers2

2

Don't know the Spec was incomplete or something, I logged the byte values and found a pattern somehow. And found that the dimensions are at 24-26 and 27-29 indexes.

val width = 1 + (get24bit(data, 24))
val height = 1 + (get24bit(data, 27))

This does the trick! Hopefully it is helpful to note this point as long as documentation is not updated.

Animesh Sahu
  • 7,445
  • 2
  • 21
  • 49
1

The accepted answer only works for certain WebP files (Extertended format VP8X) but there are other two formats (lossy VP8 and lossless VP8L) that don't work with that answer.

The 3 formats have different ways to get the dimensions.

fun getWebPDimensions(imgFile: File) {
    val stream = FileInputStream(imgFile)
    val data = stream.readNBytes(30)
    // All formats consist of a file header (12 bytes) and a ChunkHeader (8 bytes)
    // The first four ChunkHeader bytes contain the 4 characters of the format (12 to 15):
    val imageFormat = String(Arrays.copyOfRange(data, 12, 16)) // exclusive range
    val width: Int
    val height: Int
    when(imageFormat) {
        "VP8 " -> { // last character is a space
            // Simple File Format (Lossy)
            // The data is in the VP8 specification and the decoding guide explains how to get the dimensions: https://datatracker.ietf.org/doc/html/rfc6386#section-19.1
            // The formats consists of the frame_tag (3 bytes), start code (3 bytes), horizontal_size_code (2 bytes) and vertical_size_code (2 bytes)
            // The size is 14 bits, use a mask to remove the last two digits
            width = get16bit(data, 26) and 0x3FFF
            height = get16bit(data, 28) and 0x3FFF
        }
        "VP8X" -> {
            // Extended File Format, size position specified here: https://developers.google.com/speed/webp/docs/riff_container#extended_file_format
            // The width starts 4 bytes after the ChunkHeader with a size of 3 bytes, the height comes after.
            width = 1 + (get24bit(data, 24))
            height = 1 + (get24bit(data, 27))
        }
        "VP8L" -> {
            // Simple File Format (Lossless), specification here: https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#3_riff_header
            // The format consists of a signature (1 byte), 14 bit width (2 bytes) and 14 bit height (2 bytes)
            // The width and height are in consecutive bits
            val firstBytes = get16bit(data, 21)
            width = 1 + (firstBytes and 0x3FFF)
            val lastTwoDigits =  (firstBytes and 0xC000) shr 14 // the last 2 bits correspond to the first 2 bits of the height
            // Extract the remaining 12 bits and shift them to add space for the two digits
            height = 1 + ((get16bit(data, 23) and 0xFFF shl 2) or lastTwoDigits)
        }
    }
}

private fun get16bit(data: ByteArray, index: Int): Int {
    // The mask (0xFF) converts the byte from signed (this is how java originally reads the byte) to unsigned
    return data[index].toInt() and 0xFF or (data[index + 1].toInt() and 0xFF shl 8)
}

private fun get24bit(data: ByteArray, index: Int): Int {
    return get16bit(data, index) or (data[index + 2].toInt() and 0xFF shl 16)
}