Image processing (Rollup) - chung-leong/zigar GitHub Wiki
JavaScript | PHP
In this example we're going to export some image processing functions.
We first initialize the Node project and install the necessary modules:
mkdir image
cd image
npm init -y
npm install http-server
mkdir src zigAfter creating the basic skeleton, create scale.zig in the sub-directory zig:
const std = @import("std");
const zigar = @import("zigar");
pub fn scale(image_in: zigar.image.Any(.ro), image_out: zigar.image.Any(.rw)) void {
const Pixel = @Vector(4, f32);
inline for (zigar.image.formats) |tag| {
if (image_in == tag and image_out == tag) {
const in = image_in.getField(tag);
const out = image_out.getField(tag);
const x_adv: f32 = in.getWidthAsFloat() / out.getWidthAsFloat();
const y_adv: f32 = in.getHeightAsFloat() / out.getHeightAsFloat();
var coord: @Vector(2, f32) = undefined;
coord[1] = 0.5;
for (0..out.getHeight()) |y| {
coord[0] = 0.5;
for (0..out.getWidth()) |x| {
const pixel = in.sampleLinear(Pixel, coord);
out.setPixel(Pixel, x, y, pixel);
coord[0] += x_adv;
}
coord[1] += y_adv;
}
}
}
}The code above is a function that enlarges or shrinks an image. zigar.image.Any is a parametric union type that can accommodate either a PHP GD image or a JavaScript ImageData object. An inline loop is used here to generate different binaries for different formats from the same source code.
When the compilation target is WebAssembly, GD support is absent. zigar.image.formats would only
have a single entry.
Next, we're going to create rollup.config.js as we've done in the
hello world example:
import nodeResolve from '@rollup/plugin-node-resolve';
import zigar from 'rollup-plugin-zigar';
export default [
{
input: './zig/scale.zig',
plugins: [
zigar({
optimize: 'ReleaseSmall',
topLevelAwait: false,
embedWASM: true,
}),
nodeResolve(),
],
output: {
file: './src/scale.js',
format: 'umd',
exports: 'named',
name: 'Scale',
},
},
];And some make adjustments in package.json:
"type": "module",
"scripts": {
"build": "rollup -c rollup.config.js"
},We can then initiate the conversion:
npm run buildIn src, paste the following into scale.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Image processing</title>
</head>
<body>
<p>
<h3>Original</h3>
<img id="srcImage" src="./sample.png">
</p>
<p>
<h3>Result</h3>
<canvas id="dstCanvas"></canvas>
</p>
<p>
Width: <input id="widthInput" type="text" value="400">
</p>
<p>
Height: <input id="heightInput" type="text" value="300">
</p>
</body>
<script src="./scale.js"></script>
<script>
const srcImage = document.getElementById('srcImage');
const dstCanvas = document.getElementById('dstCanvas');
const widthInput = document.getElementById('widthInput');
const heightInput = document.getElementById('heightInput');
if (srcImage.complete) {
apply();
} else {
srcImage.onload = apply;
}
widthInput.oninput = apply;
heightInput.oninput = apply;
async function apply() {
// copy source image into a canvas
const srcCanvas = document.createElement('CANVAS');
srcCanvas.width = srcImage.naturalWidth;
srcCanvas.height = srcImage.naturalHeight;
const srcCTX = srcCanvas.getContext('2d');
srcCTX.drawImage(srcImage, 0, 0);
// obtain the bitmap
const srcImageData = srcCTX.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
// create destination image data object
const newWidth = parseInt(widthInput.value);
const newHeight = parseInt(heightInput.value);
const dstImageData = new ImageData(newWidth, newHeight);
// call the Zig function
await Scale.scale(srcImageData, dstImageData);
// paint resulting image onto the destination canvas
dstCanvas.width = newWidth;
dstCanvas.height = newHeight;
const dstCTX = dstCanvas.getContext('2d');
dstCTX.putImageData(dstImageData, 0, 0);
}
</script>
</html>We can't just open the file in the browser since we'd run into security restrictions (the contents of an image in the file system are considered "tainted"). We have to serve the file over HTTP.
Open package.json and add a "preview" command to scripts:
"scripts": {
"build": "rollup -c rollup.config.js",
"preview": "http-server ./src"
},Finally, download the following image into src (or choose an image of your own):

Then run the preview command:
npm run previewAnd head to http://localhost:8080/scale.html. You should see the following:

In this section, we're going create a function that apply a sepia effect on an image. In the
sub-directory zig, create sepia.zig:
const std = @import("std");
const zigar = @import("zigar");
pub fn apply(image_in: zigar.image.Any(.ro), image_out: zigar.image.Any(.rw), intensity: f32) void {
const Pixel = @Vector(4, f32);
inline for (zigar.image.formats) |tag| {
if (image_in == tag and image_out == tag) {
const in = image_in.getField(tag);
const out = image_out.getField(tag);
var coord: @Vector(2, f32) = undefined;
coord[1] = 0.5;
for (0..out.getHeight()) |y| {
coord[0] = 0.5;
for (0..out.getWidth()) |x| {
const yiq_matrix: [4]@Vector(4, f32) = .{
.{ 0.299, 0.596, 0.212, 0.0 },
.{ 0.587, -0.275, -0.523, 0.0 },
.{ 0.114, -0.321, 0.311, 0.0 },
.{ 0.0, 0.0, 0.0, 1.0 },
};
const inverse_yiq: [4]@Vector(4, f32) = .{
.{ 1.0, 1.0, 1.0, 0.0 },
.{ 0.956, -0.272, -1.1, 0.0 },
.{ 0.621, -0.647, 1.7, 0.0 },
.{ 0.0, 0.0, 0.0, 1.0 },
};
const rgba_color = in.sampleNearest(@Vector(4, f32), coord);
var yiqa_color = @"M * V"(yiq_matrix, rgba_color);
yiqa_color[1] = intensity;
yiqa_color[2] = 0.0;
const pixel = @"M * V"(inverse_yiq, yiqa_color);
out.setPixel(Pixel, x, y, pixel);
coord[0] += 1;
}
coord[1] += 1;
}
}
}
}
fn @"M * V"(m1: anytype, v2: anytype) @TypeOf(v2) {
const ar = @typeInfo(@TypeOf(m1)).array;
var t1: @TypeOf(m1) = undefined;
inline for (m1, 0..) |column, c| {
inline for (0..ar.len) |r| {
t1[r][c] = column[r];
}
}
var result: @TypeOf(v2) = undefined;
inline for (t1, 0..) |column, c| {
result[c] = @reduce(.Add, column * v2);
}
return result;
}The working principle of the filter is quite simple. We transform each pixel of the image from RGB color space into YIQ, the color space used for NTSC broadcast. Acting like an old television set, we toss away the chromatic components (I and Q). Then we add a yellowish tint by assigning a specific value to I.

As you can see in the picture above depicting the YIQ color space at Y = 0.5, the I channel represents the reddish orange color:
The @"M * V" function might look odd to you. Zig allows identifiers to contain whitespaces and
special characters using the @"..." escape sequence. We're taking advantage of that here to
clearly indicate what the function does, namely multipling a matrix with a vector.
Loops are unrolled using the inline keyword to enhance performance.
In rollup.config.js add an entry for the new file:
import nodeResolve from '@rollup/plugin-node-resolve';
import zigar from 'rollup-plugin-zigar';
export default [
{
input: './zig/scale.zig',
plugins: [
zigar({
optimize: 'ReleaseSmall',
topLevelAwait: false,
embedWASM: true,
}),
nodeResolve(),
],
output: {
file: './src/scale.js',
format: 'umd',
exports: 'named',
name: 'Scale',
},
},
{
input: './zig/sepia.zig',
plugins: [
zigar({
optimize: 'ReleaseSmall',
topLevelAwait: false,
embedWASM: true,
}),
nodeResolve(),
],
output: {
file: './src/sepia.js',
format: 'umd',
exports: 'named',
name: 'Sepia',
},
},
];Run the build command again then create sepia.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Image processing</title>
</head>
<body>
<p>
<h3>Original</h3>
<img id="srcImage" src="./sample.png">
</p>
<p>
<h3>Result</h3>
<canvas id="dstCanvas"></canvas>
</p>
<p>
Intensity: <input id="intensityInput" type="range" min="0" max="1" step="0.0001" value="0.3">
</p>
</body>
<script src="./sepia.js"></script>
<script>
const srcImage = document.getElementById('srcImage');
const dstCanvas = document.getElementById('dstCanvas');
const intensityInput = document.getElementById('intensityInput');
if (srcImage.complete) {
apply();
} else {
srcImage.onload = apply;
}
intensityInput.oninput = apply;
async function apply() {
const srcCanvas = document.createElement('CANVAS');
srcCanvas.width = srcImage.naturalWidth;
srcCanvas.height = srcImage.naturalHeight;
const srcCTX = srcCanvas.getContext('2d');
srcCTX.drawImage(srcImage, 0, 0);
const srcImageData = srcCTX.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
const dstImageData = new ImageData(srcCanvas.width, srcCanvas.height);
const intensity = parseFloat(intensityInput.value);
await Sepia.apply(srcImageData, dstImageData, intensity);
dstCanvas.width = dstImageData.width;
dstCanvas.height = dstImageData.height;
const dstCTX = dstCanvas.getContext('2d');
dstCTX.putImageData(dstImageData, 0, 0);
}
</script>
</html>Start the preview server and go to http://localhost:8080/sepia.html. You should see the following:

You can find the complete source code for this example here.
A major advantage of using Zig for a task like image processing is that the same code can be deployed both on the browser and on the server. After a user has made some changes to an image on the frontend, the backend can apply the exact same effect using the same code. Consult the Node version of this example to learn how to do it.
The image filter employed for this example is very rudimentary. Check out pb2zig's project page to see more advanced code.
That's it for now. I hope this tutorial is enough to get you started with using Zigar.