For our next trick, we'll tackle the most common headache in the C ecosystem: Dependency Hell. Usually, if you want to manipulate images, you need libpng, libjpeg, zlib, and headers for all of them installed on your system. If your user is on a different OS, your script fails. To avoid that, let's use Affix::Build to compile the famous stb single-header libraries.
The Recipe
Here we'll load a JPG/PNG, resize it to a thumbnail using high-quality linear scaling, and save it as a PNG.
In other words, we'll turn this:
...into this:
Simple enough.
This recipe demonstrates two advanced techniques:
- Output Parameters: Passing pointers to variables so C can populate them with data.
- Raw Buffers: Passing the memory address of Perl scalars to C for high-performance data manipulation.
use v5.40;
use Affix qw[:all];
use Affix::Build;
use HTTP::Tiny;
use Path::Tiny;
use Config;
$|++;
# 1. Setup Persistent Cache
# Like Inline::C, we keep artifacts in a local subdirectory.
my $build_dir = path('.')->child('_build');
$build_dir->mkpath;
# Calculate the expected library name (e.g., stb_lib.dll or libstb_lib.so)
my $lib_file = $build_dir->child( ( $^O eq 'MSWin32' ? '' : 'lib' ) . 'stb_lib.' . $Config{so} );
# 2. Check Cache
if ( $lib_file->exists ) {
say "Using cached library: $lib_file";
}
else {
say "Library not found. Building...";
build_library($build_dir);
}
# 3. Bindings
my $lib = Affix::load_library($lib_file);
# stbir_pixel_layout enum
use constant STBIR_RGBA => 4;
# "Buffer" passes the raw memory address of a Perl string to C.
affix $lib, 'stbi_load', [ String, Pointer [Int], Pointer [Int], Pointer [Int], Int ] => Buffer;
affix $lib, 'stbi_write_png', [ String, Int, Int, Int, Buffer, Int ] => Int;
affix $lib, 'stbir_resize_uint8_linear', [ Buffer, Int, Int, Int, Buffer, Int, Int, Int, Int ] => Buffer;
# 4. The Application
my $input_file = 'avatar.jpg';
my $output_file = 'avatar_small.png';
# Generate dummy input if needed
unless ( -e $input_file ) {
say 'Creating dummy input file...';
my $w = 100;
my $h = 100;
my $pixels = '';
# Gradient Pattern
for my $y ( 0 .. $h - 1 ) {
for my $x ( 0 .. $w - 1 ) {
$pixels .= pack( 'C4', int( $x * 2.5 ), int( $y * 2.5 ), 0, 255 );
}
}
stbi_write_png( $input_file, $w, $h, 4, $pixels, $w * 4 );
}
# Step A: Load
my ( $w, $h, $ch ) = ( 0, 0, 0 );
say "Loading $input_file...";
my $img_data = stbi_load( $input_file, \$w, \$h, \$ch, 4 );
die 'Failed to load image.' if is_null($img_data);
say "Original: ${w}x${h} ($ch channels)";
# Step B: Resize
my $new_w = int( $w / 2 );
my $new_h = int( $h / 2 );
my $in_stride = $w * 4;
my $out_stride = $new_w * 4;
# Allocate output memory (Buffer)
my $out_data = "\0" x ( $new_h * $out_stride );
say "Resizing to ${new_w}x${new_h}...";
my $res = stbir_resize_uint8_linear( $img_data, $w, $h, $in_stride, $out_data, $new_w, $new_h, $out_stride, STBIR_RGBA );
die 'Resize failed!' if is_null($res);
# Step C: Save
say "Saving to $output_file...";
stbi_write_png( $output_file, $new_w, $new_h, 4, $out_data, $out_stride );
say 'Done.';
# Subroutines
sub build_library($dir) {
my $http = HTTP::Tiny->new;
# Download headers if missing
for my $file (qw(stb_image.h stb_image_resize2.h stb_image_write.h)) {
my $path = $dir->child($file);
next if $path->exists;
say "Downloading $file...";
my $url = "https://raw.githubusercontent.com/nothings/stb/master/$file";
my $res = $http->get($url);
die "Failed to download $file" unless $res->{success};
$path->spew_raw( $res->{content} );
}
my $c = Affix::Build->new(
name => 'stb_lib',
build_dir => $dir,
flags => { cflags => "-I$dir -O3", ldflags => ( $^O eq 'MSWin32' ? '-Wl,--export-all-symbols' : '' ) }
);
$c->add( \<<~'C', lang => 'c' );
#if defined(_WIN32)
#define STBIDEF __declspec(dllexport)
#define STBIWDEF __declspec(dllexport)
#define STBIRDEF __declspec(dllexport)
#else
#define STBIDEF __attribute__((visibility("default")))
#define STBIWDEF __attribute__((visibility("default")))
#define STBIRDEF __attribute__((visibility("default")))
#endif
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image.h"
#include "stb_image_resize2.h"
#include "stb_image_write.h"
C
$c->compile_and_link;
}
How It Works
-
1. The
BufferType We definedBuffertype in Affix to handle passing an opaque pointer. Perl strings, when passed as aPointertype, usually copy the data. However, forPointer[Void], Affix optimizations kick in: it passes the raw address of the Perl scalar's memory. This allowsstbir_resizeto write directly into$out_datawithout any copy overhead. -
2. Output Parameters (
int *width) When callingstbi_load, we pass references (\$w,\$h). Affix detects this, allocates integer pointers, passes them to C, and then updates your Perl variables with the values C wrote (the image dimensions). -
3. The Pixel Layout The
stb_image_resize2library requires us to specify the memory layout of the pixels. We passSTBIR_RGBA(4) to tell it that every 4 bytes represent Red, Green, Blue, and Alpha. If we passed 0 (default), it would treat the image as Grayscale, resulting in a garbled output.