A Faster Blurhash Alternative to flutter_blurhash and blurhash_dart

Introducing blurhash_ffi on the pub for Flutter

Dhikshith
7 min readAug 8, 2023
Blurry Image placeholder for a picture using blurhash_ffi on the pub for flutter.

In this blog I’ll take you through the journey of how i authored, open sourced and published my first package on pub for flutter.

The Basics :

What is an Image Placeholder?

When loading a picture asynchronously from the network or from the disk, to reduce the perceived time taken to load the image, applications use Image placeholders while the actual image loads in the background.

A famous example is Instagram, which presents a blurry version of the image while loading the actual image in the background (though I’m unsure if it’s using the same algorithm).

What is a Blurhash?

Originally started at the Wolt app (blurha.sh), a blurhash is a compact representation of a placeholder for an image.

It is a base83 encoded string to represent a blurry version of the image, like for example: “L5H2EC=PM+yV0g-mq.wG9c010J}I”, which then could be used to generate a blurry version of the same image.

Here is a link to the algorithm if you want to learn how it’s implemented.

Dart & Flutter :

There are already a few of those packages in pub.dev, the most popular ones are flutter_blurhash which only has a Decoder (from blurhash string to blurry Image), and blurhash_dart which has the Encoder (from original image to blurhash string).

pub.dev packages response for the “blurhash” query

The reason I authored blurhash_ffi was the annoyance I dealt with using those two packages, in my app, folksable, so I decided to implement one.

Things I didn’t like about the existing packages:

  1. I tried to use blurhash_dart for both encoding and decoding, but it was dependent upon the image package which had a version constraint on some other package in my app.
  2. It was written in pure dart, which was slow and blocking my UI thread, which causes frame drops.
  3. The flutter_blurhash was from the flutter community, I think this is particularly designed for a use case when images are loaded from the server along with the blurhash string (which had to be done on the server side) to represent the placeholder while the actual image is loading, and did not serve a general purpose for both encoding and decoding.
  4. flutter_blurhash was also slow at decoding since it was also a pure dart implementation and blocking the UI thread, but the package API was nice.
  5. I even tried using them inside a separate Isolate to solve UI freezes, but it brought new issues related to serialization and message passing between the Isolates.

So my priorities were clearly Performance and ease of use, and reducing the work on servers by doing it on the client.

As the package name (blurhash_ffi) suggests, I used dart:ffi bindings to call native C functions, using a foreign function interface was a new and thrilling experience for me, I always wondered how large projects have multiple languages that communicate with each other using native libraries and different protocols.

ffigen is a nice dart tooling published by the dart team, to automate the boring parts of ffi, i.e., the binding generation for each function.

So I wrote a single C header file both for the encoder and decoder, referencing the Wolt app’s C implementation, with all the necessary function definitions needed to do the work which I’m linking below.

the .src/ folder contains all the native code, one header file blurhash_ffi.h, one C file blurhash_ffi.c and a CMakeLists.txt

blurhash_ffi is fast, much faster experience (I opened a new good first issue to track performance testing against pure dart implementation one, make a benchmark comparison) than pure dart implementation and the reasons are obvious.

  1. written in C and ffi calls the native library.
  2. c library runs in a separate Isolate from the main UI thread.

The Challenges Faced Implementing FFI:

  1. The Android NDK, give me quite some trouble compiling using cmake, during the build process maybe because I was new to the NDK.
  2. Building for Windows is still having trouble because the MSVC Compiler does not support the C99 standard which the VLA’s (Variable Length Arrays) is a part of, which is being used in the C code for the blurhash encoder.
  3. The web is not supported at the moment, since i believe compilation for WASM as a target was not obvious from the ffi example.
  4. Passing Images between the flutter & native part was the biggest hurdle in building this package, the native part requires the image to be sent as a rgb param as - A pointer to the pixel data. This is supplied in RGB order, with 3 bytes per pixel, I missed the detail that it was expecting only 3 channels RGB, but Flutter often used 4 channels RGBA which required conversion between the number of channels, before sending it to the native part.

How to use blurhash_ffi in your projects:

To use this plugin, add blurhash_ffi as a dependency in your pubspec.yaml file

One Step (both Encoding & Decoding) Usage:

this can be helpful to demo and in small examples, very convenient to have both encoding and decoding in just one Neat API that takes Original Image’s ImageProvider and gives out ImageProvider for the blurry Image, but be mindful of widget rebuilds triggering encodes and decodes.

import 'package:blurhash_ffi/blurhash_ffi.dart';

/// Encoding and Decoding all in One Step
///
/// `ImageProvider` in -> Send your Image to be encoded.
/// `ImageProvider` out -> Get your blurry image version.

class BlurhashMyImage extends StatelessWidget {
final String imageUrl;
const BlurhashMyImage({required this.imageUrl, super.key});

@override
Widget build(BuildContext context) {
return Image(
image: BlurhashTheImage(
NetworkImage(imageUrl), // you can use any image provider of your choice.
decodingHeight: 1920, decodingWidth: 1080),
alignment: Alignment.center,
fit: BoxFit.cover
);
}
}

Or use it in two different steps (Highly recomended for getting an opportunity at pre-processing the blurhash and saving them locally)

Encoding

import 'package:blurhash_ffi/blurhash_ffi.dart';

/// Encoding a blurhash from an image provider
///
/// You can use any ImageProvider you want, including NetworkImage, FileImage, MemoryImage, AssetImage, etc.
final imageProvider = NetworkImage('https://picsum.photos/512');
final imageProvider2 = AssetImage('assets/image.jpg');

/// Signature
/// static Future<String> encode(
/// ImageProvider imageProvider, {
/// int componentX = 4,
/// int componentY = 3,
/// })
/// may throw `BlurhashFFIException` if encoding fails.
final String blurHash = await BlurhashFFI.encode(imageProvider);

Decoding

import 'package:blurhash_ffi/blurhash_ffi.dart';
import 'dart:ui' as ui;
/// You have 3 ways to decode a blurhash
///
/// 1. Using the `BlurhashFfi` widget
/// 2. Using the `BlurhashFfiImage` ImageProvider
/// 3. Using the `BlurhashFfi.decode` static method

/// 1. Using the `BlurhashFfi` widget (same constructor as flutter_blurhash's Blurhash widget)
class BlurHashApp extends StatelessWidget {
const BlurHashApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("BlurHash")),
body: const SizedBox.expand(
child: Center(
child: AspectRatio(
aspectRatio: 1.6,
child: BlurhashFfi(hash: "L5H2EC=PM+yV0g-mq.wG9c010J}I"),
),
),
),
),
);
}

/// 2. Using the `BlurhashFfiImage` ImageProvider
final imageProvider = BlurhashFfiImage("L5H2EC=PM+yV0g-mq.wG9c010J}I");
class BlurHashApp2 extends StatelessWidget {
const BlurHashApp2({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("BlurHash")),
body: const SizedBox.expand(
child: Center(
child: AspectRatio(
aspectRatio: 1.6,
child: Image(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
),
),
);
}

/// 3. Using the `BlurhashFfi.decode` static method which returns dart:ui.Image
/// Signature
/// static Future<ui.Image> decode(
/// String blurHash, {
/// int width = 32,
/// int height = 32,
/// int punch = 1,
/// })
/// may throw `BlurhashFFIException` if decoding fails.
final ui.Image image = await BlurhashFFI.decode("L5H2EC=PM+yV0g-mq.wG9c010J}I");

Release Isolate and its memory

do this only when you are done with encoding/decoding blur-hashes.

import 'package:blurhash_ffi/blurhash_ffi.dart';

BlurhashFFi.free();

What’s next for blurhash_ffi:

Addressing the Existing Issues, One fell swoop for (#1 & #2 ).

#1, Rust and flutter_rust_bridge have support for the web through WASM, probably there is a way to target current C code to compile for WASM. there is no official implementation for WASM in ffi or ffigen.

#2, this issue arose because MSVC does not support the c99 standard which includes the Variable Length Arrays(VLA’s), I’m not an expert not to try and fight the compiler to get this feature working, I tried Alloaca, but still no result.

I’m thinking Implementing the encoder and the decoder in Rust will solve the above two issues, so I think I’m gonna need some time to learn Rust ( heard good things, but did not work with Rust before).

I called for help from the Rust subreddit to migrate the native part to Rust and some people liked the project and wanted to help.

#7, Performance testing for encoding and decoding against pure dart implementation, this is a good first issue (Good for newcomers) to get involved in the development of this open-source project.

checkout the package on the pub.dev:

Thanks for reading : )

to know more about me and my work:

Follow on Twitter: I write about software, development, product development, and my project Folksable.

--

--

Dhikshith
Dhikshith

Written by Dhikshith

Indie Dev. Building Folksable, a Habit Accountability app. Interests in Businesses, Software, and Marketing.

No responses yet