Skip to content

Instantly share code, notes, and snippets.

@schickling
Last active August 7, 2023 14:20
Show Gist options
  • Save schickling/db0aa873565a4132b1b02c6059a79c1f to your computer and use it in GitHub Desktop.
Save schickling/db0aa873565a4132b1b02c6059a79c1f to your computer and use it in GitHub Desktop.
Random Node Effect image utils
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/master";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShell = with pkgs; pkgs.mkShell {
buildInputs = [
nodejs_20
(yarn.override { nodejs = nodejs_20; })
# needed for `canvas` NPM package
pkg-config
pixman
cairo
pango
libjpeg
] ++ lib.optionals stdenv.isDarwin [
# needed for `canvas` NPM package
pkgs.darwin.apple_sdk.frameworks.CoreText
];
};
NIX_LDFLAGS = "-F${pkgs.darwin.apple_sdk.frameworks.CoreText}/Library/Frameworks -framework CoreText $NIX_LDFLAGS";
});
}
import * as blurhash from 'blurhash'
import { Effect, pipe } from '../effect/index.js'
import sharp from 'sharp'
import * as Canvas from 'canvas'
export * as blurhash from 'blurhash'
export type ImageDataUrlFromHash = {
hash: string
width: number
height: number
options?: {
size: number
quality: number
}
}
export const imageDataUrlFromHash = ({
hash,
width,
height,
options = { size: 16, quality: 40 },
}: ImageDataUrlFromHash) =>
Effect.gen(function* ($) {
const hashWidth = options?.size
const hashHeight = Math.round(hashWidth * (height / width))
const pixels = blurhash.decode(hash, hashWidth, hashHeight)
const resizedImageBuf = yield* $(
Effect.tryPromiseOrDie(
() =>
sharp(Buffer.from(pixels), {
raw: { channels: 4, width: hashWidth, height: hashHeight },
})
.jpeg({ overshootDeringing: true, quality: options.quality })
.toBuffer(), // Here also possible to do whatever with your image, e.g. save it or something else.)
),
)
const b64 = resizedImageBuf.toString('base64')
return `data:image/jpeg;base64,${b64}`
})
export const decodeAndHashFromImage = (encodedImage: Buffer) =>
Effect.gen(function* ($) {
const { data, height, width } = yield* $(decodeImageViaCanvas(encodedImage))
const hash = blurhash.encode(data, width, height, 4, 4)
return { hash, width, height }
})
export const blurhashEncode = (imageData: ArrayBuffer, width: number, height: number) =>
pipe(
Effect.try(() => blurhash.encode(new Uint8ClampedArray(imageData), width, height, 4, 4)),
Effect.orDie,
Effect.withSpan('blurhashEncode', { attributes: { width, height } }),
)
export const decodeAndImageDataUrlFromImage = (encodedImage: Buffer, options: Omit<ImageDataUrlFromHash, 'hash'>) =>
pipe(
decodeAndHashFromImage(encodedImage),
Effect.flatMap(({ hash, height, width }) =>
Effect.all({
dataUrl: imageDataUrlFromHash({ hash, ...options }),
width: Effect.succeed(width),
height: Effect.succeed(height),
}),
),
)
export const decodeImageViaCanvas = (imageBuffer: Buffer) =>
Effect.gen(function* ($) {
const img = yield* $(Effect.tryPromiseOrDie(() => Canvas.loadImage(imageBuffer)))
const canvas = Canvas.createCanvas(img.width, img.height)
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
const imageData = ctx.getImageData(0, 0, img.width, img.height)
return {
width: img.width,
height: img.height,
data: imageData.data,
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment