Rust CLI 水印神器:从小白到命令行大师的图像魔法之旅

Rust CLI 水印神器:从小白到命令行大师的图像魔法之旅

Photos provided by Unsplash OR Pexels

引言:命令行下的水印艺术革命

在数字化浪潮中,图像水印不仅是保护内容的盾牌,更是个性化表达的画笔。从入门级文字叠加,到高级边框背景融合,Rust 以其安全高效的特性,让这一切变得触手可及。现在,我们将这些功能改造为本地命令行工具(CLI),让你只需一行命令,就能批量或单张处理图片。CLI 模式的优势显而易见:自动化脚本集成、批量处理高效、无需 GUI 依赖,适合开发者、摄影师或批量任务场景。基于之前的实战内容,我们使用 clap crate 解析参数,实现自定义字体、大小、位置、文字边框、图片边框、背景、版权、技术支持等全套功能。

这份指南专为小白设计,由浅入深,从安装起步,到完整 CLI 实战。无论你是 Rust 新手还是 CLI 初学者,都能通过详细理论、完整代码一步步上手。准备好在终端释放图像魔法了吗?让我们开启这场 Rust CLI 水印的奇幻之旅吧!

第一部分:基础入门 - 项目安装与 CLI 基本使用

理论基础

CLI 工具的核心是命令行参数解析,使用 clap crate 简洁定义选项。图像处理仍依赖 image(加载/保存)、imageproc(绘图)、rusttype(字体渲染)。基本流程:解析参数 -> 加载图像/字体 -> 应用水印 -> 保存输出。最佳实践:默认参数简化使用;支持输入/输出路径;错误处理用 anyhow 提供友好提示。

  • clap crate:自动生成帮助文档,支持子命令、默认值、验证。
  • 字体嵌入:用 include_bytes! 内嵌 TTF 文件,避免路径依赖。
  • 位置坐标:(x,y) 从左上角起始,支持相对定位(如 “bottom-right”)。
  • 潜在问题:路径无效——用 Path::exists() 检查;多线程安全——CLI 单进程无需额外处理。

这种基础 CLI 适合快速单张水印。

实例代码:基础 CLI 工具

  1. 创建项目:cargo new rust_watermark_cli --bin --edition=2024
  2. 编辑 Cargo.toml
[package]
name = "rust_watermark_cli"
version = "0.1.0"
edition = "2024"

[dependencies]
image = "0.25"
imageproc = "0.25"
rusttype = "0.9"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
  1. 下载字体(如 FreeSans.ttf)到项目根目录。
  2. 编辑 src/main.rs(基础版本,只添加简单文字水印):
use anyhow::Result;
use clap::Parser;
use image::{RgbImage, Rgb};
use imageproc::drawing::{draw_text_mut, text_size};
use rusttype::{Font, Scale};
use std::path::{Path, PathBuf};

#[derive(Parser, Debug)]
#[command(version, about = "Rust CLI 水印工具 - 基础版")]
struct Args {
    /// 输入图像路径
    #[arg(short, long)]
    input: PathBuf,

    /// 输出图像路径
    #[arg(short, long)]
    output: PathBuf,

    /// 水印文字(默认:"水印")
    #[arg(short, long, default_value = "水印")]
    text: String,

    /// 字体大小(默认:32.0)
    #[arg(short, long, default_value = "32.0")]
    size: f32,

    /// X 坐标(默认:50)
    #[arg(long, default_value = "50")]
    x: i32,

    /// Y 坐标(默认:50)
    #[arg(long, default_value = "50")]
    y: i32,
}

fn main() -> Result<()> {
    let args = Args::parse();

    // 加载图像
    let mut image = image::open(&args.input)?.to_rgb8();

    // 加载字体
    let font_data: &[u8] = include_bytes!("../FreeSans.ttf");
    let font = Font::try_from_bytes(font_data).ok_or(anyhow::anyhow!("字体加载失败"))?;

    // 绘制水印
    let scale = Scale { x: args.size, y: args.size };
    let color = Rgb([255u8, 0, 0]); // 红色
    draw_text_mut(&mut image, color, args.x, args.y, scale, &font, &args.text);

    // 保存
    image.save(&args.output)?;

    println!("水印添加完成!输出:{:?}", args.output);
    Ok(())
}
  1. 运行:cargo build --release,然后 ./target/release/rust_watermark_cli -i input.jpg -o output.jpg -t "Hello" -s 40 --x 100 --y 100

解释

  • clap::Parser 定义参数,自动生成 --help
  • 基础绘制用 draw_text_mut,参数从 CLI 获取。
  • 错误如路径无效会 panic,提供提示。

第二部分:自定义水印 - 支持位置相对与文字边框

理论基础

扩展 CLI 支持相对位置(如 “bottom-right”),通过计算图像尺寸实现。文字边框:多次 offset 绘制 outline。理论:位置解析用 enum 或 match;边框厚度参数化。最佳实践:验证参数(如大小 >0);支持颜色解析(RGB 字符串)。

  • 相对位置:计算 x/y 如 width - text_width - offset。
  • 边框实现:循环 dx/dy 绘制,厚度控制循环范围。
  • CLI 扩展:添加 flags 如 --position bottom-right

实例代码:添加相对位置与边框

扩展 Args 和 main:

// ... (接上例导入)

#[derive(Parser, Debug)]
struct Args {
    // ... (原有参数)
    /// 位置模式 (top-left, bottom-right 等,默认:absolute)
    #[arg(long, default_value = "absolute")]
    position: String,

    /// 文字边框厚度 (默认:0)
    #[arg(long, default_value = "0")]
    outline_thickness: i32,

    /// 文字边框颜色 (RGB, 如 "0,0,0",默认:黑色)
    #[arg(long, default_value = "0,0,0")]
    outline_color: String,
}

fn parse_rgb(color_str: &str) -> Result<Rgb<u8>> {
    let parts: Vec<u8> = color_str.split(',').map(|s| s.trim().parse().unwrap()).collect();
    Ok(Rgb([parts[0], parts[1], parts[2]]))
}

fn add_text_with_outline(image: &mut RgbImage, text: &str, font: &Font<'_>, size: f32, mut x: i32, mut y: i32, color: Rgb<u8>, outline_color: Rgb<u8>, thickness: i32, position: &str) {
    let scale = Scale { x: size, y: size };
    let (width, height) = text_size(scale, font, text);

    // 相对位置调整
    match position {
        "bottom-right" => {
            x = image.width() as i32 - width as i32 - 10;
            y = image.height() as i32 - height as i32 - 10;
        }
        // 添加更多如 "center"
        _ => {}, // absolute
    }

    // 绘制 outline
    for dx in -thickness..=thickness {
        for dy in -thickness..=thickness {
            if dx != 0 || dy != 0 {
                draw_text_mut(image, outline_color, x + dx, y + dy, scale, font, text);
            }
        }
    }

    // 绘制填充
    draw_text_mut(image, color, x, y, scale, font, text);
}

fn main() -> Result<()> {
    let args = Args::parse();
    let mut image = image::open(&args.input)?.to_rgb8();
    let font = Font::try_from_bytes(include_bytes!("../FreeSans.ttf")).unwrap();

    let color = Rgb([255, 0, 0]);
    let outline_color = parse_rgb(&args.outline_color)?;

    add_text_with_outline(&mut image, &args.text, &font, args.size, args.x, args.y, color, outline_color, args.outline_thickness, &args.position);

    image.save(&args.output)?;
    Ok(())
}

解释--position bottom-right 自动调整坐标。边框用 --outline-thickness 1 --outline-color "255,255,255"

第三部分:高级功能 - 图片边框、背景与额外文字

理论基础

图片边框:用 draw_hollow_rect_mut 绘制矩形。背景:创建新图像填充渐变,overlay 原图。版权/技术支持:额外文字参数,位置固定如底角。CLI:添加 flags 如 --border-thickness--background-color--copyright

  • 背景渐变:线性插值 RGB。
  • 多文字:复用绘制函数,添加参数。
  • 最佳实践:可选参数用 Option;批量模式用子命令。

实例代码:完整高级 CLI

扩展 Args 和函数。

use imageproc::drawing::draw_hollow_rect_mut;
use imageproc::rect::Rect;
use image::imageops::overlay;
use image::ImageBuffer;

// ... (接上例)

#[derive(Parser, Debug)]
struct Args {
    // ... (原有)
    /// 图片边框厚度 (默认:0)
    #[arg(long, default_value = "0")]
    border_thickness: u32,

    /// 图片边框颜色 (RGB, 默认:"0,255,0")
    #[arg(long, default_value = "0,255,0")]
    border_color: String,

    /// 背景填充 (起始 RGB, 如 "255,255,255",启用渐变背景)
    #[arg(long)]
    bg_start: Option<String>,

    /// 背景结束 RGB (与 bg_start 配对)
    #[arg(long)]
    bg_end: Option<String>,

    /// 背景填充宽度 (默认:20)
    #[arg(long, default_value = "20")]
    padding: u32,

    /// 版权文字
    #[arg(long)]
    copyright: Option<String>,

    /// 技术支持文字
    #[arg(long)]
    tech_support: Option<String>,
}

fn add_image_border(image: &mut RgbImage, color: Rgb<u8>, thickness: u32) {
    if thickness == 0 { return; }
    let rect = Rect::at(0, 0).of_size(image.width(), image.height());
    draw_hollow_rect_mut(image, rect, color);
}

fn add_background(original: &RgbImage, bg_start: Rgb<u8>, bg_end: Rgb<u8>, padding: u32) -> RgbImage {
    let new_width = original.width() + 2 * padding;
    let new_height = original.height() + 2 * padding;
    let mut bg = ImageBuffer::new(new_width, new_height);

    for y in 0..new_height {
        let ratio = y as f32 / new_height as f32;
        let r = (bg_start[0] as f32 * (1.0 - ratio) + bg_end[0] as f32 * ratio) as u8;
        let g = (bg_start[1] as f32 * (1.0 - ratio) + bg_end[1] as f32 * ratio) as u8;
        let b = (bg_start[2] as f32 * (1.0 - ratio) + bg_end[2] as f32 * ratio) as u8;
        for x in 0..new_width {
            bg.put_pixel(x, y, Rgb([r, g, b]));
        }
    }

    overlay(&mut bg, original, padding as i64, padding as i64);
    bg
}

fn main() -> Result<()> {
    let args = Args::parse();
    let mut image = image::open(&args.input)?.to_rgb8();
    let font = Font::try_from_bytes(include_bytes!("../FreeSans.ttf")).unwrap();
    let color = Rgb([255, 0, 0]);
    let outline_color = parse_rgb(&args.outline_color)?;

    // 背景
    if let (Some(start_str), Some(end_str)) = (&args.bg_start, &args.bg_end) {
        let bg_start = parse_rgb(start_str)?;
        let bg_end = parse_rgb(end_str)?;
        image = add_background(&image, bg_start, bg_end, args.padding);
    }

    // 主水印
    add_text_with_outline(&mut image, &args.text, &font, args.size, args.x, args.y, color, outline_color, args.outline_thickness, &args.position);

    // 版权和技术支持
    if let Some(copyright) = &args.copyright {
        add_text_with_outline(&mut image, copyright, &font, 20.0, 10, image.height() as i32 - 30, Rgb([128, 128, 128]), Rgb([0, 0, 0]), 1, "absolute");
    }
    if let Some(tech) = &args.tech_support {
        add_text_with_outline(&mut image, tech, &font, 20.0, image.width() as i32 - 200, image.height() as i32 - 30, Rgb([128, 128, 128]), Rgb([0, 0, 0]), 1, "absolute");
    }

    // 边框
    let border_color = parse_rgb(&args.border_color)?;
    add_image_border(&mut image, border_color, args.border_thickness);

    image.save(&args.output)?;
    Ok(())
}

解释--bg-start "255,255,255" --bg-end "200,200,200" --copyright "版权 © 2025" --tech-support "支持: Rust" 添加高级元素。

第四部分:批量模式与优化 - 生产级 CLI

理论基础

批量:添加子命令 batch,用 walkdir 遍历目录。优化:进度条用 indicatif;并行用 rayon。最佳实践:日志输出;配置文件支持。

  • 子命令:clap 支持 subcommands。
  • 批量实现:递归目录,处理每个图像。

实例代码:添加批量子命令

扩展为子命令结构。

use clap::{Parser, Subcommand};
use walkdir::WalkDir;
use rayon::prelude::*;
use indicatif::{ProgressBar, ProgressStyle};

// ... (函数如上)

#[derive(Parser)]
#[command(version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 单张处理
    Single(Args), // Args 如上

    /// 批量处理
    Batch {
        /// 输入目录
        #[arg(short, long)]
        input_dir: PathBuf,

        /// 输出目录
        #[arg(short, long)]
        output_dir: PathBuf,

        // 其他参数如 text, size 等
        #[arg(short, long, default_value = "水印")]
        text: String,
        // ... 类似
    },
}

fn process_batch(args: &BatchArgs) -> Result<()> { // 假设 BatchArgs 结构体
    let files: Vec<PathBuf> = WalkDir::new(&args.input_dir).into_iter().filter_map(|e| e.ok()).filter(|e| e.file_type().is_file()).map(|e| e.path().to_owned()).collect();
    let pb = ProgressBar::new(files.len() as u64);
    pb.set_style(ProgressStyle::default_bar().template("{bar:40} {pos}/{len}").unwrap());

    files.par_iter().try_for_each(|file| -> Result<()> {
        let mut image = image::open(file)?.to_rgb8();
        // 应用所有水印函数(如 add_text_with_outline 等,使用 args 参数)
        let rel = file.strip_prefix(&args.input_dir)?;
        let out_path = args.output_dir.join(rel);
        std::fs::create_dir_all(out_path.parent().unwrap())?;
        image.save(out_path)?;
        pb.inc(1);
        Ok(())
    })?;
    Ok(())
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    match &cli.command {
        Commands::Single(args) => { /* 单张处理 */ }
        Commands::Batch(args) => process_batch(args)?,
    }
    Ok(())
}

解释rust_watermark_cli batch -i dir_in -o dir_out 批量处理。

参考资料

通过这份指南,你已掌握 Rust CLI 水印的核心。终端魔法,永不落幕!

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)