0

I am using webp encoder from this. The authur abandoned the project so no help from there. It had some issue like reading FourcC ICCP and ALPH. I added codes for them and now It can encode bitmaps to animated webp.

But the problem is some of animated webp files are throwing error Not supported FourC while encoding.

Here How I use the encoder

      WebpBitmapEncoder encoder = new WebpBitmapEncoder(destination);  
      encoder.setLoops(0); // 0 = infinity.  
      for(Bitmap bitmap:bitmap_array)  {
          Bitmap bitmap2 = scalePreserveRatio(bitmap,512,512);
          // Bitmap comp_bitmap = compress(new_bitmap);
          encoder.setDuration(80);  
          encoder.writeFrame(bitmap2, 80); 
      }
      encoder.close();

decoding:

            val drawable = GlideApp.with(context).load(source_file).skipMemoryCache(true)
                    .diskCacheStrategy(DiskCacheStrategy.NONE)
                    .submit().get() as WebpDrawable
            drawable.constantState
            val state = drawable.constantState as Drawable.ConstantState

            val frameLoader: Field = state::class.java.getDeclaredField("frameLoader")
            frameLoader.isAccessible = true

            @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
            
            val webpFrameLoader = frameLoader.get(state) as WebpFrameLoader
            val webpDecoder: Field = webpFrameLoader.javaClass.getDeclaredField("webpDecoder")
            webpDecoder.isAccessible = true
            val standardGifDecoder = webpDecoder.get(webpFrameLoader) as GifDecoder
            Log.d("ReactNative", "got ${standardGifDecoder.frameCount} frames:")

            for (i in 0 until standardGifDecoder.frameCount) {
                val delay = standardGifDecoder.nextDelay
                val bitmap = standardGifDecoder.nextFrame as Bitmap
                  // bitmap
                standardGifDecoder.advance()
            }

Resizing..

 public static Bitmap scalePreserveRatio(Bitmap imageToScale, int destinationWidth,
        int destinationHeight) {
            // NOTE 
        if (destinationHeight > 0 && destinationWidth > 0 && imageToScale != null) {
            int width = imageToScale.getWidth();
            int height = imageToScale.getHeight();

            //Calculate the max changing amount and decide which dimension to use
            float widthRatio = (float) destinationWidth / (float) width;
            float heightRatio = (float) destinationHeight / (float) height;

            //Use the ratio that will fit the image into the desired sizes
            int finalWidth = (int)Math.floor(width * widthRatio);
            int finalHeight = (int)Math.floor(height * widthRatio);
            if (finalWidth > destinationWidth || finalHeight > destinationHeight) {
                finalWidth = (int)Math.floor(width * heightRatio);
                finalHeight = (int)Math.floor(height * heightRatio);
            }

            //Scale given bitmap to fit into the desired area
            imageToScale = Bitmap.createScaledBitmap(imageToScale, finalWidth, finalHeight, true);

            //Created a bitmap with desired sizes
            Bitmap scaledImage = Bitmap.createBitmap(destinationWidth, destinationHeight, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(scaledImage);

            //Draw background color
            Paint paint = new Paint();
            paint.setColor(Color.TRANSPARENT);
            paint.setStyle(Paint.Style.FILL);
            canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);

            //Calculate the ratios and decide which part will have empty areas (width or height)
            float ratioBitmap = (float)finalWidth / (float)finalHeight;
            float destinationRatio = (float) destinationWidth / (float) destinationHeight;
            float left = ratioBitmap >= destinationRatio ? 0 : (float)(destinationWidth - finalWidth) / 2;
            float top = ratioBitmap < destinationRatio ? 0: (float)(destinationHeight - finalHeight) / 2;
            canvas.drawBitmap(imageToScale, left, top, null);

            return scaledImage;
        } else {
            return imageToScale;
        }
    }

And the location of the error in encoder is read() from where errro is being thrown :

package com.n4no.webpencoder.webp.muxer;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.BitSet;

/**
 * @author Bartlomiej Tadych, b4rtaz
 */
public class WebpContainerReader {

    private final InputStream _inputStream;
    private final boolean _debug;
    private int _fileSize;
    private int _offset;

    public WebpContainerReader(InputStream inputStream, boolean debug) {
        _inputStream = inputStream;
        _debug = debug;
    }

    public void close() throws IOException {
    }

    public void readHeader() throws IOException {
        byte[] fcc = new byte[4];

        read(fcc, 4);
        if (!isFourCc(fcc, 'R', 'I', 'F', 'F'))
            throw new IOException("Expected RIFF file.");

        _fileSize = readUInt32() + 8 - 1;

        read(fcc, 4);
        if (!isFourCc(fcc, 'W', 'E', 'B', 'P'))
            throw new IOException("Expected Webp file.");
    }

    public WebpChunk read() throws IOException {
        byte[] fcc = new byte[4];

        if (read(fcc, 4) > 0) {
            if (isFourCc(fcc, 'V', 'P', '8', ' '))
                return readVp8();
            if (isFourCc(fcc, 'V', 'P', '8', 'L'))
                return readVp8l();
            if (isFourCc(fcc, 'V', 'P', '8', 'X'))
                return readVp8x();
            if (isFourCc(fcc, 'A', 'N', 'I', 'M'))
                return readAnim();
            if (isFourCc(fcc, 'A', 'N', 'M', 'F'))
                return readAnmf();
            if (isFourCc(fcc, 'I', 'C', 'C', 'P'))
                return readIccp();
            if (isFourCc(fcc, 'A', 'L', 'P', 'H'))
                return readAlph();
            
                
            throw new IOException(String.format("Not supported FourCC: %c.%c.%c.%c.",
                    fcc[0], fcc[1], fcc[2], fcc[3]));
        }

        if (_fileSize != _offset)
            throw new IOException(String.format("Header has wrong file size: %d, expected: %d", 
                    _fileSize, _offset));
        return null;
    }

    private WebpChunk readVp8x() throws IOException {
        int chunkSize = readUInt32();
        if (chunkSize != 10)
            throw new IOException("Expected 10 bytes for VP8X.");

        WebpChunk chunk = new WebpChunk(WebpChunkType.VP8X);

        byte[] flags = new byte[4];
        read(flags, 4);
        BitSet bs = BitSet.valueOf(flags);

        chunk.hasIccp = bs.get(0);
        chunk.hasAnim = bs.get(1);
        chunk.hasExif = bs.get(2);
        chunk.hasXmp = bs.get(3);
        chunk.hasAlpha = bs.get(4);

        chunk.width = readUInt24();
        chunk.height = readUInt24();

        debug(String.format("VP8X: size = %dx%d", chunk.width, chunk.height));
        return chunk;
    }

    private byte[] readPayload(int bytes) throws IOException {
        byte[] payload = new byte[bytes];
        if (read(payload, bytes) != bytes)
            throw new IOException("Can not read all bytes.");
        return payload;
    }

    private WebpChunk readVp8() throws IOException {
        int chunkSize = readUInt32();

        WebpChunk chunk = new WebpChunk(WebpChunkType.VP8);
        chunk.isLossless = false;
        chunk.payload = readPayload(chunkSize);

        debug(String.format("VP8: bytes = %d", chunkSize));
        return chunk;
    }

    private WebpChunk readVp8l() throws IOException {
        int chunkSize = readUInt32();

        WebpChunk chunk = new WebpChunk(WebpChunkType.VP8L);
        chunk.isLossless = true;
        chunk.payload = readPayload(chunkSize);

        debug(String.format("VP8L: bytes = %d", chunkSize));
        return chunk;
    }

    private WebpChunk readAnim() throws IOException {
        int chunkSize = readUInt32();
        if (chunkSize != 6)
            throw new IOException("Expected 6 bytes for ANIM.");

        WebpChunk chunk = new WebpChunk(WebpChunkType.ANIM);
        chunk.background = readUInt32();
        chunk.loops = readUInt16();

        debug(String.format("ANIM: loops = %d", chunk.loops));
        return chunk;
    }

    private WebpChunk readAnmf() throws IOException {
        int chunkSize = readUInt32();

        WebpChunk chunk = new WebpChunk(WebpChunkType.ANMF);
        chunk.x = readUInt24();
        chunk.y = readUInt24();
        chunk.width = readUInt24();
        chunk.height = readUInt24();
        chunk.duration = readUInt24();

        byte[] flags = new byte[1];
        read(flags, 1);
        BitSet bs = BitSet.valueOf(flags);
        chunk.useAlphaBlending = bs.get(1);
        chunk.disposeToBackgroundColor = bs.get(0);

        byte[] cch = new byte[4];
        read(cch, 4);
        if (isFourCc(cch, 'V', 'P', '8', 'L'))
            chunk.isLossless = true;
        else if (isFourCc(cch, 'V', 'P', '8', ' '))
            chunk.isLossless = false;
        else
            throw new IOException("Not supported ANMF payload.");

        readUInt32(); // Payload size.
        int payloadSize = chunkSize - 24;
        chunk.payload = readPayload(payloadSize);

        debug(String.format("ANMF: size = %dx%d, offset = %dx%d, duration = %d, bytes = %d",
                chunk.width, chunk.height, chunk.x, chunk.y, chunk.duration, payloadSize));
        return chunk;
    }

    private WebpChunk readIccp() throws IOException {
        int chunkSize = readUInt32();
        WebpChunk chunk = new WebpChunk(WebpChunkType.VP8);
        chunk.isLossless = false;
        readPayload(chunkSize);
        chunk.payload = null;
        debug(String.format("iccp: bytes = %d", chunkSize));
        return chunk;
    }

    private WebpChunk readAlph() throws IOException {
        int chunkSize = readUInt32();
        WebpChunk chunk = new WebpChunk(WebpChunkType.VP8);
        chunk.isLossless = false;
        readPayload(chunkSize);
        chunk.payload = null;
        debug(String.format("alph: bytes = %d", chunkSize));
        return chunk;
    }
    //

    private final int read(byte[] buffer, int bytes) throws IOException {
        int count = _inputStream.read(buffer, 0, bytes);
        _offset += count;
        return count;
    }

    private final int readUint(int bytes) throws IOException {
        byte[] b = new byte[] { 0, 0, 0, 0 };
        read(b, bytes);
        return ByteBuffer.wrap(b, 0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
    }

    private final int readUInt32() throws IOException {
        return readUint(4);
    }

    private final int readUInt24() throws IOException {
        return readUint(3);
    }

    private final int readUInt16() throws IOException {
        return readUint(2);
    }

    private boolean isFourCc(byte[] h, char a, char b, char c, char d) {
        return h[0] == a && h[1] == b && h[2] == c && h[3] == d;
    }

    private void debug(String message) {
        if (_debug)
            System.out.println(message);
    }
}

rest of related files are here on sandbox. https://codesandbox.io/s/polished-water-hl32ud?file=/app/muxer/WebpMuxer.java

without using scalePreserveRatio() All webp files gets encoded without fail. The scaling function is the culprit here.

I want to resize and encode these animated webp files. How would I fix this exception.

UHS
  • 584
  • 4
  • 8

2 Answers2

0

Ok I found that resizing the bitmap causing the issue. I was able to encode frames if I skipped the resizing

public static Bitmap scalePreserveRatio(Bitmap imageToScale, int destinationWidth, int destinationHeight) {

So this answer can solve the issue for them who do need resizing.

PS: Found the reason behind the issue and solution to mitigate the error.

Solution 1:

setting Paint color as solid color.

Color.TRANSPARENT to Color.BLACK In ```paint.setColor(Color.TRANSPARENT);````

Solution 2 and cause of the problem: Write code for reading ALPH chunks in WebP Encoder.

Author of the encoder says alpha channel is not supported. So you need to write code for ALPH. Currently code for ALPH is same as ICCP. So reading the alpha chunks will allow us to use Color.TRANSPARENT in paint.

UHS
  • 584
  • 4
  • 8
0

I have read the codes in webp encoder and also read the google's specification of webp.

The encoder is missing the alpha chunk of VP8X. Need to read verify if ALPH chunk exist on giveb position and get ALPH data. Then use those data to write to the image while encoding. And the reading writing must be done according to google's instruction about those bits.

https://developers.google.com/speed/webp/docs/riff_container

CrackerKSR
  • 1,380
  • 1
  • 11
  • 29