第15章 实战项目:开发图像滤镜
第15章 实战项目:开发图像滤镜
这是本书的最后一个实战项目,我们将挑战一个更有趣的任务:图像处理。
我们将编写一个程序,读取一张彩色的 PNG 图片,将其转换为灰度(黑白)图片,然后保存为新文件。
这个项目将串联起我们之前学到的核心知识点:
- C 互操作:使用
libspngC 库来解码/编码 PNG 图片。 - 内存管理:手动分配缓冲区来存储像素数据。
- 位操作与算法:处理 RGB 像素数据。
- 文件 I/O:读取和写入二进制文件。
1. 准备工作
依赖库:libspng
为了处理 PNG 图片,我们需要一个解码库。这里我们选择 libspng,因为它简单、现代且性能优异。
你需要确保系统中安装了 libspng。
- macOS:
brew install libspng - Linux (Ubuntu):
sudo apt install libspng-dev - Windows: 建议使用 vcpkg 或直接源码集成。
项目结构
image-filter/
├── build.zig
├── src/
│ └── main.zig
└── input.png (找一张测试图片)
2. 导入 C 库
首先,我们需要在 main.zig 中导入 libspng 和 C 标准库。
const std = @import("std");
const c = @cImport({
@cInclude("stdio.h");
@cInclude("spng.h");
});
在 build.zig 中,别忘了链接这个库:
// build.zig
exe.linkSystemLibrary("spng");
exe.linkLibC();
3. 读取并解码 PNG
打开文件
由于 libspng 是基于 C 的 FILE* 接口工作的,我们这里直接使用 C 的文件操作函数会更方便。
fn decodePng(allocator: std.mem.Allocator, filename: []const u8) !struct { []u8, u32, u32 } {
// 1. 打开文件
// filename 是 Zig 切片,我们需要转为以 0 结尾的 C 字符串
// 为了简单,假设 filename 是字面量,或者我们手动加个 0
const file = c.fopen(filename.ptr, "rb");
if (file == null) return error.FileNotFound;
defer _ = c.fclose(file); // 确保关闭文件
// 2. 创建解码上下文
const ctx = c.spng_ctx_new(0);
if (ctx == null) return error.OutOfMemory;
defer c.spng_ctx_free(ctx);
// 3. 关联文件
if (c.spng_set_png_file(ctx, file) != 0) return error.PngError;
// 4. 获取图片信息 (宽、高)
var ihdr: c.spng_ihdr = undefined;
if (c.spng_get_ihdr(ctx, &ihdr) != 0) return error.PngError;
const width = ihdr.width;
const height = ihdr.height;
// 5. 计算解码所需的缓冲区大小
var out_size: usize = 0;
// SPNG_FMT_RGBA8 表示我们希望解码为 RGBA 格式 (每个像素 4 字节)
if (c.spng_decoded_image_size(ctx, c.SPNG_FMT_RGBA8, &out_size) != 0) return error.PngError;
// 6. 分配内存
const buffer = try allocator.alloc(u8, out_size);
// 注意:这里不能 defer free,因为我们要把 buffer 返回给调用者
// 7. 解码
if (c.spng_decode_image(ctx, buffer.ptr, out_size, c.SPNG_FMT_RGBA8, 0) != 0) {
allocator.free(buffer); // 出错时释放内存
return error.PngDecodeFailed;
}
return .{ buffer, width, height };
}
这段代码展示了典型的 C 互操作流程:
- 资源管理:使用
defer关闭文件和释放 C 上下文。 - 错误处理:检查 C 函数返回值(通常 0 表示成功),并转换为 Zig 错误。
- 内存分配:使用 Zig 的
allocator分配内存,并传递指针给 C 函数。
4. 灰度滤镜算法
现在我们有了一个包含所有像素数据的 []u8 缓冲区。
由于我们请求的是 RGBA8 格式,每 4 个字节代表一个像素:
- Byte 0: Red
- Byte 1: Green
- Byte 2: Blue
- Byte 3: Alpha (透明度)
灰度公式:Gray = 0.2126 * R + 0.7152 * G + 0.0722 * B (这是常用的相对亮度公式)。
fn applyGrayscale(pixels: []u8) void {
var i: usize = 0;
// 每次跳过 4 个字节 (R, G, B, A)
while (i < pixels.len) : (i += 4) {
const r = pixels[i];
const g = pixels[i+1];
const b = pixels[i+2];
// alpha (pixels[i+3]) 保持不变
// 计算灰度值
const gray_f =
@as(f32, @floatFromInt(r)) * 0.2126 +
@as(f32, @floatFromInt(g)) * 0.7152 +
@as(f32, @floatFromInt(b)) * 0.0722;
const gray_u8: u8 = @intFromFloat(gray_f);
// 将 RGB 都设为同一个灰度值
pixels[i] = gray_u8;
pixels[i+1] = gray_u8;
pixels[i+2] = gray_u8;
}
}
这里我们使用了 @as 和 @floatFromInt 来进行安全的类型转换,这是 Zig 严格类型系统的体现。
5. 保存图片
处理完像素后,我们需要将其编码回 PNG 格式并保存。
fn savePng(filename: []const u8, pixels: []const u8, width: u32, height: u32) !void {
const file = c.fopen(filename.ptr, "wb");
if (file == null) return error.FileOpenFailed;
defer _ = c.fclose(file);
// 创建编码上下文
const ctx = c.spng_ctx_new(c.SPNG_CTX_ENCODER);
if (ctx == null) return error.OutOfMemory;
defer c.spng_ctx_free(ctx);
if (c.spng_set_png_file(ctx, file) != 0) return error.PngError;
// 设置图片头信息
var ihdr: c.spng_ihdr = undefined;
ihdr.width = width;
ihdr.height = height;
ihdr.color_type = c.SPNG_COLOR_TYPE_TRUECOLOR_ALPHA;
ihdr.bit_depth = 8;
if (c.spng_set_ihdr(ctx, &ihdr) != 0) return error.PngError;
// 编码并写入
if (c.spng_encode_image(ctx, pixels.ptr, pixels.len, c.SPNG_FMT_PNG, c.SPNG_ENCODE_FINALIZE) != 0) {
return error.PngEncodeFailed;
}
}
6. 整合 Main 函数
最后,将所有步骤在 main 中串联起来。
pub fn main() !void {
// 使用通用分配器
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const input_file = "input.png";
const output_file = "output.png";
std.debug.print("Processing {s}...\n", .{input_file});
// 1. 读取
const image = try decodePng(allocator, input_file);
const pixels = image[0];
const width = image[1];
const height = image[2];
defer allocator.free(pixels); // 记得释放像素内存!
std.debug.print("Image size: {d}x{d}\n", .{width, height});
// 2. 处理
applyGrayscale(pixels);
// 3. 保存
try savePng(output_file, pixels, width, height);
std.debug.print("Saved to {s}\n", .{output_file});
}
总结
通过这个项目,我们不仅复习了内存管理和文件操作,更重要的是亲身体验了 Zig 与 C 语言互操作的便捷性。
- 零开销互操作:我们像调用 Zig 函数一样调用了
spng_decode_image,没有任何封装成本。 - 手动内存管理:我们清晰地知道何时分配内存(在
decode中),何时释放(在main结束时)。 - 类型安全:在处理像素计算时,Zig 强制我们进行显式类型转换,避免了潜在的溢出错误。
这正是 Zig 作为一门现代系统编程语言的魅力所在:它赋予你对底层的完全控制,同时提供现代化的工具来保障安全。