Reputation: 8106
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?
Upvotes: 2
Views: 1057
Reputation: 833
The accepted answer only works for certain WebP files (Extended 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 format 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)
}
Upvotes: 2
Reputation: 8106
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.
Upvotes: 2