Phase 3 - Images: - JPEG parser with direct DCT passthrough (no re-encoding) - PNG metadata extraction (full embedding pending) - Page.image() for drawing images at position - Page.imageFit() for auto-scaling with aspect ratio - Pdf.addJpegImage() / addJpegImageFromFile() - XObject generation in OutputProducer New modules: - src/images/mod.zig - Image module exports - src/images/image_info.zig - ImageInfo struct - src/images/jpeg.zig - JPEG parser - src/images/png.zig - PNG metadata parser New example: - examples/image_demo.zig - Image embedding demo Stats: - 66 unit tests passing - 4 working examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
196 lines
7.4 KiB
Zig
196 lines
7.4 KiB
Zig
//! Image Demo - Demonstrates JPEG image embedding
|
|
//!
|
|
//! Usage: ./image_demo [path_to_jpeg]
|
|
//! If no path is provided, creates a simple PDF with text explaining the feature.
|
|
|
|
const std = @import("std");
|
|
const pdf = @import("zpdf");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
std.debug.print("zpdf - Image Demo\n", .{});
|
|
|
|
// Get command line args
|
|
const args = try std.process.argsAlloc(allocator);
|
|
defer std.process.argsFree(allocator, args);
|
|
|
|
var doc = pdf.Pdf.init(allocator, .{});
|
|
defer doc.deinit();
|
|
|
|
doc.setTitle("Image Demo");
|
|
doc.setAuthor("zpdf");
|
|
|
|
var page = try doc.addPage(.{});
|
|
page.setMargins(50, 50, 50);
|
|
page.setXY(50, 800);
|
|
|
|
// Title
|
|
try page.setFont(.helvetica_bold, 24);
|
|
page.setFillColor(pdf.Color.rgb(41, 98, 255));
|
|
try page.cell(0, 30, "Image Demo - JPEG Support", pdf.Border.none, .center, false);
|
|
page.ln(40);
|
|
|
|
// Check if an image path was provided
|
|
if (args.len > 1) {
|
|
const image_path = args[1];
|
|
std.debug.print("Loading image: {s}\n", .{image_path});
|
|
|
|
// Try to load the image
|
|
const image_index = doc.addJpegImageFromFile(image_path) catch |err| {
|
|
std.debug.print("Error loading image: {any}\n", .{err});
|
|
|
|
try page.setFont(.helvetica, 12);
|
|
page.setFillColor(pdf.Color.red);
|
|
try page.cell(0, 20, "Error: Could not load image file", pdf.Border.none, .left, false);
|
|
page.ln(25);
|
|
|
|
try page.setFont(.helvetica, 10);
|
|
page.setFillColor(pdf.Color.black);
|
|
const long_text =
|
|
\\Make sure the file exists and is a valid JPEG image.
|
|
\\
|
|
\\Supported features:
|
|
\\- JPEG/JPG images (RGB and Grayscale)
|
|
\\- Direct embedding (no re-encoding)
|
|
\\- Automatic dimension detection
|
|
;
|
|
try page.multiCell(450, null, long_text, pdf.Border.none, .left, false);
|
|
|
|
const filename = "image_demo.pdf";
|
|
try doc.save(filename);
|
|
std.debug.print("Created: {s}\n", .{filename});
|
|
return;
|
|
};
|
|
|
|
// Get image info
|
|
const img_info = doc.getImage(image_index).?;
|
|
|
|
// Show image info
|
|
try page.setFont(.helvetica_bold, 14);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 20, "Image Information:", pdf.Border.none, .left, false);
|
|
page.ln(25);
|
|
|
|
try page.setFont(.helvetica, 11);
|
|
|
|
// Display image properties
|
|
var buf: [256]u8 = undefined;
|
|
const size_text = std.fmt.bufPrint(&buf, "Dimensions: {d} x {d} pixels", .{ img_info.width, img_info.height }) catch "Error";
|
|
try page.cell(0, 16, size_text, pdf.Border.none, .left, false);
|
|
page.ln(18);
|
|
|
|
const color_text = std.fmt.bufPrint(&buf, "Color Space: {s}", .{img_info.color_space.pdfName()}) catch "Error";
|
|
try page.cell(0, 16, color_text, pdf.Border.none, .left, false);
|
|
page.ln(18);
|
|
|
|
const bpc_text = std.fmt.bufPrint(&buf, "Bits per Component: {d}", .{img_info.bits_per_component}) catch "Error";
|
|
try page.cell(0, 16, bpc_text, pdf.Border.none, .left, false);
|
|
page.ln(30);
|
|
|
|
// Draw the image
|
|
try page.setFont(.helvetica_bold, 14);
|
|
try page.cell(0, 20, "Image Preview:", pdf.Border.none, .left, false);
|
|
page.ln(25);
|
|
|
|
// Calculate size to fit in available space (max 400x400)
|
|
const max_w: f32 = 400;
|
|
const max_h: f32 = 400;
|
|
const img_w: f32 = @floatFromInt(img_info.width);
|
|
const img_h: f32 = @floatFromInt(img_info.height);
|
|
const scale = @min(max_w / img_w, max_h / img_h, 1.0);
|
|
const display_w = img_w * scale;
|
|
const display_h = img_h * scale;
|
|
|
|
// Draw border around image area
|
|
const img_x = page.getX();
|
|
const img_y = page.getY() - display_h;
|
|
|
|
page.setStrokeColor(pdf.Color.light_gray);
|
|
try page.drawRect(img_x - 2, img_y - 2, display_w + 4, display_h + 4);
|
|
|
|
// Draw the image
|
|
try page.image(image_index, img_info, img_x, img_y, display_w, display_h);
|
|
|
|
std.debug.print("Image embedded: {d}x{d} pixels, displayed at {d:.0}x{d:.0} points\n", .{ img_info.width, img_info.height, display_w, display_h });
|
|
} else {
|
|
// No image provided - show instructions
|
|
try page.setFont(.helvetica_bold, 14);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 20, "How to Use Images:", pdf.Border.none, .left, false);
|
|
page.ln(25);
|
|
|
|
try page.setFont(.helvetica, 11);
|
|
const instructions =
|
|
\\To include a JPEG image in your PDF:
|
|
\\
|
|
\\1. Load the image file:
|
|
\\ const img_idx = try doc.addJpegImageFromFile("photo.jpg");
|
|
\\
|
|
\\2. Get the image info:
|
|
\\ const info = doc.getImage(img_idx).?;
|
|
\\
|
|
\\3. Draw on a page:
|
|
\\ try page.image(img_idx, info, x, y, width, height);
|
|
\\
|
|
\\Or use imageFit to auto-scale:
|
|
\\ try page.imageFit(img_idx, info, x, y, max_w, max_h);
|
|
\\
|
|
\\Run this example with a JPEG file path:
|
|
\\ ./image_demo photo.jpg
|
|
;
|
|
try page.multiCell(450, null, instructions, pdf.Border.all, .left, false);
|
|
|
|
page.ln(30);
|
|
|
|
// Supported features
|
|
try page.setFont(.helvetica_bold, 14);
|
|
try page.cell(0, 20, "Supported Features:", pdf.Border.none, .left, false);
|
|
page.ln(25);
|
|
|
|
try page.setFont(.helvetica, 11);
|
|
page.setFillColor(pdf.Color.rgb(0, 128, 0));
|
|
try page.cell(20, 16, "[OK]", pdf.Border.none, .left, false);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 16, "JPEG images (RGB, Grayscale, CMYK)", pdf.Border.none, .left, false);
|
|
page.ln(18);
|
|
|
|
page.setFillColor(pdf.Color.rgb(0, 128, 0));
|
|
try page.cell(20, 16, "[OK]", pdf.Border.none, .left, false);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 16, "Direct passthrough (no re-encoding)", pdf.Border.none, .left, false);
|
|
page.ln(18);
|
|
|
|
page.setFillColor(pdf.Color.rgb(0, 128, 0));
|
|
try page.cell(20, 16, "[OK]", pdf.Border.none, .left, false);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 16, "Automatic dimension detection", pdf.Border.none, .left, false);
|
|
page.ln(18);
|
|
|
|
page.setFillColor(pdf.Color.rgb(0, 128, 0));
|
|
try page.cell(20, 16, "[OK]", pdf.Border.none, .left, false);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 16, "Aspect ratio preservation", pdf.Border.none, .left, false);
|
|
page.ln(18);
|
|
|
|
page.setFillColor(pdf.Color.rgb(255, 165, 0));
|
|
try page.cell(20, 16, "[--]", pdf.Border.none, .left, false);
|
|
page.setFillColor(pdf.Color.black);
|
|
try page.cell(0, 16, "PNG images (metadata only, not yet embedded)", pdf.Border.none, .left, false);
|
|
}
|
|
|
|
// Footer
|
|
page.setXY(50, 50);
|
|
try page.setFont(.helvetica, 9);
|
|
page.setFillColor(pdf.Color.medium_gray);
|
|
try page.cell(0, 15, "Generated with zpdf - Pure Zig PDF Library", pdf.Border.none, .center, false);
|
|
|
|
// Save
|
|
const filename = "image_demo.pdf";
|
|
try doc.save(filename);
|
|
|
|
std.debug.print("Created: {s}\n", .{filename});
|
|
std.debug.print("Done!\n", .{});
|
|
}
|