Image processing ᴾᴴᴾ - chung-leong/zigar GitHub Wiki
JavaScript | PHP
In this example we're going to build a server-side app that apply a filter on an image. It'll be a real server this time, one that accepts requests from the browser and sends different images based on parameters in the URL.
Creating the sample app
Per usual, we create the basic directory structure for our sample app:
mkdir image
cd image
mkdir src zig
In the sub-directory zig, create scale.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.
The function visits every pixels in the output image and sets its color to the color at the corresponding position in the input image, calculated based on bilinear interpolation.
Now on to the PHP side. In src create scale.php:
<?php
$m = zigar_use(__DIR__ . '/../zig/scale.zig');
header('Content-Type: image/png');
$width = $_GET['w'] ?? 400;
$height = $_GET['h'] ?? 300;
$im_out = imagecreatetruecolor($width, $height);
$im_in = imagecreatefrompng(__DIR__ . '/sample.png');
$m->scale($im_in, $im_out);
imagepng($im_out);
Next, save the following image to src as sample.png (or choose an image of your own):

Finally, launch the PHP development server:
php -S localhost:8080
Open a web browser tab and navigate to http://localhost:8080/src/scale.php
It'll take a moment for the module to compile. You should see a notification in the terminal while the compiler is working. Once it finishes, you should see a scaled version of the image in the browser.
Change the URL to http://localhost:8080/src/scale.php?w=800&h=600 and you'll see an enlarged image. Change it to http://localhost:8080/src/scale.php?w=800&h=100 and you'll see a squashed version.
Applying an image filter
In this section, we're going create a function that performs 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.

The picture below depicts the YIQ color space at Y = 0.5:

As you can see, 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 src, create sepia.php:
<?php
$m = zigar_use(__DIR__ . '/../zig/sepia.zig');
header('Content-Type: image/png');
$intensity = $_GET['i'] ?? 0.3;
$im_in = imagecreatefrompng(__DIR__ . '/sample.png');
$im_out = imagecreatetruecolor(imagesx($im_in), imagesy($im_in));
$m->apply($im_in, $im_out, $intensity);
imagepng($im_out);
Then navigate to http://localhost:8080/src/sepia.php. After a brief pause, the following should appear:

Source code
You can find the complete source code for this example here.
Conclusion
Finally, we have an actual server-side app. And it does something cool! A major advantage of using Zig for a task like image processing is that the same code can be deployed on the browser too.
In the next lesson, you're going to learn how to make use of third-party packages.