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):

Sample image

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.

Color transform

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

Color space

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:

Screen shot

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.


SQLite database