Skip to content

DapengSusu/thunbor-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

thunbor 图片服务器

thunbor 图片服务器的 Rust 实现,可以提供一系列的图片转换服务。

开发流程

思路分析

我们需要使用一种简洁且可扩展的方式,来描述对图片进行的一系列 有序操作。这些操作的数据结构如下:

// 解析出来的图片处理的参数
struct ImageSpec {
    specs: Vec<Spec>
}

// 每个参数的是我们支持的某种处理方式
enum Spec {
    Resize(Resize),
    Crop(Crop),
    ...
}

// 处理图片的 resize
struct Resize {
    width: u32,
    height: u32
}

有了数据结构,那我们如何设计一个任何客户端可以使用的、体现在 URL 上的接口,使其能够解析成我们设计的数据结构呢?

将一系列 Spec 放在一个 Vec 中,使用 protobuf 将其序列化为字节流,再使用 base64 转码放在 URL 中,得到可扩展的图片处理参数

image spec 的 protobuf 消息定义:

message ImageSpec { repeated Spec specs = 1; }

message Spec {
  oneof data {
    Resize resize = 1;
    Crop crop = 2;
    ...
  }
}

...

得到的 url 类似:

http://localhost:3000/image/CgoKCAjYBBCgBiADCgY6BAgUEBQKBDICCAM/<encoded origin url>

可优化的一点:为原始图片的获取过程添加一个 LRU (Least Recently Used)缓存。

定义 protobuf 并编译

在项目根目录创建 abi.proto 文件,内容如下:

syntax = "proto3";

package abi;

// 一个 ImageSpec 是一组图片处理方式的集合,服务器按照 spec 的顺序处理
message ImageSpec { repeated Spec specs = 1; }

// 一个 spec 可以是以下处理方式之一
message Spec {
  oneof data {
    Resize resize = 1;
    Crop crop = 2;
    FlipHorizontal fliph = 3;
    FlipVertical flipv = 4;
    Contrast contrast = 5;
    Filter filter = 6;
    Watermark watermark = 7;
  }
}

// 图片改变大小
message Resize {
  uint32 width = 1;
  uint32 height = 2;

  enum Mode {
    MODE_UNSPECIFIED = 0;
    MODE_NORMAL = 1;
    MODE_SEAM_CARVE = 2;
  }
  Mode mode = 3;

  enum SampleFilter {
    SAMPLE_FILTER_UNSPECIFIED = 0;
    SAMPLE_FILTER_NEAREST = 1;
    SAMPLE_FILTER_TRIANGLE = 2;
    SAMPLE_FILTER_CATMULL_ROM = 3;
    SAMPLE_FILTER_GAUSSIAN = 4;
    SAMPLE_FILTER_LANCZOS3 = 5;
  }
  SampleFilter filter = 4;
}

// 图片截取
message Crop {
  uint32 x = 1;
  uint32 y = 2;
  uint32 width = 3;
  uint32 height = 4;
}

// 水平翻转
message FlipHorizontal {}

// 垂直翻转
message FlipVertical {}

// 对比度调整
message Contrast { float contrast = 1; }

// 图片滤镜
message Filter {
  enum Filter {
    FILTER_UNSPECIFIED = 0;
    FILTER_OCEANIC = 1;
    FILTER_ISLANDS = 2;
    FILTER_MARINE = 3;

    // more: https://docs.rs/photon-rs/latest/photon_rs/filters/fn.filter.html
  }
  Filter filter = 1;
}

// 图片水印
message Watermark {
  uint32 x = 1;
  uint32 y = 2;
}

编译 protobuf 文件需要引入两个 crate,在 Cargo.toml 里加入(或使用 cargo add xxx

[dependencies]
prost = "0.14"

[build-dependencies]
anyhow = "1"
prost-build = "0.14"

同时在根目录创建一个 build.rs 文件,这里引入了 anyhow 这个 crate(放在 [build-dependencies] 下)

fn main() -> anyhow::Result<()> {
    let out_dir = "src/pb";

    println!("cargo::rerun-if-changed=abi.proto");

    std::fs::create_dir_all(out_dir)?;
    prost_build::Config::new()
        .out_dir(out_dir)
        .compile_protos(&["abi.proto"], &["."])?;

    Ok(())
}

编译后可以在 src/pb 目录里找到 abi.rs 文件,文件名与 proto 文件里定义的 package 名一致。

编写辅助函数

接下来在 src 下的 pb 目录同级创建 pb.rs ,引用 abi.rs ,并为 ImageSpec 提供转换支持或快速创建

// src/pb.rs
use base64::prelude::*;

mod abi;

pub use abi::*;
use prost::Message;

impl ImageSpec {
    pub fn from_specs(specs: Vec<Spec>) -> Self {
        Self { specs }
    }
}

// 让 ImageSpec 可以生成一个字符串
impl From<&ImageSpec> for String {
    fn from(image_spec: &ImageSpec) -> Self {
        let data = image_spec.encode_to_vec();

        BASE64_URL_SAFE_NO_PAD.encode(data)
    }
}

// 让 ImageSpec 可以通过一个字符串创建
impl TryFrom<&str> for ImageSpec {
    type Error = anyhow::Error;

    fn try_from(str: &str) -> Result<Self, Self::Error> {
        let data = BASE64_URL_SAFE_NO_PAD.decode(str)?;
        let image_spec = Self::decode(&data[..])?;

        Ok(image_spec)
    }
}

impl Spec {
    pub fn new_resize_seam_carve(width: u32, height: u32) -> Self {
        Self {
            data: Some(spec::Data::Resize(Resize {
                width,
                height,
                mode: resize::Mode::SeamCarve as i32,
                filter: resize::SampleFilter::Unspecified as i32,
            })),
        }
    }

    pub fn new_resize(width: u32, height: u32, filter: resize::SampleFilter) -> Self {
        Self {
            data: Some(spec::Data::Resize(Resize {
                width,
                height,
                mode: resize::Mode::Normal as i32,
                filter: filter as i32,
            })),
        }
    }

    pub fn new_filter(filter: filter::Filter) -> Self {
        Self {
            data: Some(spec::Data::Filter(Filter {
                filter: filter as i32,
            })),
        }
    }

    pub fn new_watermark(x: u32, y: u32) -> Self {
        Self {
            data: Some(spec::Data::Watermark(Watermark { x, y })),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn encoded_spec_should_be_decoded() {
        let spec1 = Spec::new_resize(300, 300, resize::SampleFilter::Gaussian);
        let spec2 = Spec::new_filter(filter::Filter::Oceanic);
        let spec3 = Spec::new_watermark(100, 100);
        let image_spec = ImageSpec::from_specs(vec![spec1, spec2, spec3]);
        let s: String = (&image_spec).into();

        assert_eq!(image_spec, s.as_str().try_into().unwrap());
    }
}

这里需要引入 photon-rs 提供图片效果和 base64 支持 base64 编解码,还需要 anyhow 快速处理错误。

引入 Http 服务器

引入 axum 作为服务器,serde 将参数反序列化,tokio 提供异步支持,flexi_loggerlog 用于输出日志,percent-encoding 提供 URL 编解码。

[dependencies]
anyhow = "1"
axum = "0.8"
base64 = "0.22"
flexi_logger = "0.31"
log = "0.4"
percent-encoding = "2" # URL编码/解码
photon-rs = "0.3" # 图片效果
prost = "0.14"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }

[build-dependencies]
anyhow = "1"
prost-build = "0.14"

要使用 tokio 提供的异步功能,main 函数需要改为:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
	...
}

添加 #[tokio::main] 标注,并定义为 async 函数,并使用 anyhow::Result<()> 统一处理错误,也可以使用 Result<(), dyn Box<std::error::Error>>

main 函数中首先初始化日志,这里使用了 flexi_logger 配合官方的 log,也可以使用其它日志库。

接下来构建路由,路由的路径为:"/image/{spec}/{url}",并使用 generate_handler 处理 get 请求。

最后我们创建 TcpListener 绑定 localhost,监听 3000 端口,启动服务器。

创建一个参数 Params 类型,并派生宏 Deserializeaxum 会自动解析

最终 main.rs 代码如下:

use std::net::SocketAddr;

use axum::{Router, extract::Path, http::StatusCode, routing};
use flexi_logger::Logger;
use log::{debug, info};
use serde::Deserialize;
use tokio::net::TcpListener;

#[derive(Deserialize)]
struct Params {
    spec: String,
    url: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化日志
    Logger::try_with_env_or_str("debug")?.start()?;

    // 构建路由
    let router = Router::new().route("/image/{spec}/{url}", routing::get(generate_handler));

    // 启动 Web 服务器
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    info!("Listening on http://{}", addr);

    // 处理服务器关机
    let shutdown_signal = async {
        tokio::signal::ctrl_c().await.unwrap();
        info!("Server shutting down...");
    };

    axum::serve(listener, router)
        .with_graceful_shutdown(shutdown_signal)
        .await?;

    Ok(())
}

async fn generate_handler(Path(Params { spec, url }): Path<Params>) -> Result<String, StatusCode> {
    debug!("{}", spec);
    debug!("{}", url);
    let url = percent_encoding::percent_decode_str(&url).decode_utf8_lossy();
    let spec: thunbor::ImageSpec = spec
        .as_str()
        .try_into()
        .map_err(|_| StatusCode::BAD_REQUEST)?;

    Ok(format!("url:{}\nspec:{:#?}", url, spec))
}

generate_handler 中目前我们只是将参数解析出来。

另外写了一个 example 用于测试,快速验证编解码的正确性,获得编码后的图片 url 和 specs,使用的图片 url 链接为:https://images.pexels.com/photos/2470905/pexels-photo-2470905.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260

// examples/image_spce.rs
use thunbor::*;

fn main() -> anyhow::Result<()> {
    let image_spec = ImageSpec::from_specs(vec![
        Spec::new_resize(600, 800, resize::SampleFilter::CatmullRom),
        Spec::new_filter(filter::Filter::Marine),
        Spec::new_watermark(20, 20),
    ]);
    let url = "https://images.pexels.com/photos/2470905/pexels-photo-2470905.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260";

    let encoded_specs_str: String = (&image_spec).into();
    let encoded_url =
        percent_encoding::utf8_percent_encode(url, percent_encoding::NON_ALPHANUMERIC).to_string();

    println!("encoded specs:\n{}", encoded_specs_str);
    println!("encoded url:\n{}\n", encoded_url);

    let decoded_specs: ImageSpec = encoded_specs_str.as_str().try_into()?;
    let decoded_url = percent_encoding::percent_decode_str(&encoded_url).decode_utf8_lossy();

    // println!("decoded specs:\n{:#?}", decoded_specs);
    // println!("decoded url:\n{}", decoded_url);
    assert_eq!(decoded_specs, image_spec);
    assert_eq!(decoded_url, url);

    Ok(())
}

输出结果如下:

encoded specs:
CgwKCgjYBBCgBhgBIAMKBDICCAMKBjoECBQQFA
encoded url:
https%3A%2F%2Fimages%2Epexels%2Ecom%2Fphotos%2F2470905%2Fpexels%2Dphoto%2D2470905%2Ejpeg%3Fauto%3Dcompress%26cs%3Dtinysrgb%26dpr%3D2%26h%3D750%26w%3D1260

启动服务器,使用 example 中的数据作为 spec 和 url,使用 httpie 工具验证功能:

httpie get "http://localhost:3000/image/CgwKCgjYBBCgBhgBIAMKBDICCAMKBjoECBQQFA/https%3A%2F%2Fimages%2Epexels%2Ecom%2Fphotos%2F2470905%2Fpexels%2Dphoto%2D2470905%2Ejpeg%3Fauto%3Dcompress%26cs%3Dtinysrgb%26dpr%3D2%26h%3D750%26w%3D1260"

应该能正常返回解析后的 ImageSpec ,并与我们 example 中保持一致。

获取源图并缓存

需要引入 LRU cache(使用 lru crate) 来缓存源图,axum 中我们在创建路由后使用 layer 添加全局状态,这个全局状态目前就是 LRU cache,在内存中缓存网络请求获得的源图。

use lru::LruCache;

// 添加 `bytes` crate
type Cache = Arc<Mutex<LruCache<u64, bytes::Bytes>>>;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
	//...

	let cache: Cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1024).unwrap())));

    // 构建路由
    let router = Router::new()
        .route("/image/{spec}/{url}", routing::get(generate_handler))
        .layer(Extension(cache));

	//...
}

添加一个 retrieve_image 函数,对于图片的网络请求,先对 URL 做个哈希,在 LRU 缓存中查找,找不到才用 reqwest 发送请求。

async fn retrieve_image(url: &str, cache: Cache) -> anyhow::Result<bytes::Bytes> {
    let mut hasher = DefaultHasher::new();

    url.hash(&mut hasher);

    let key = hasher.finish();
    let guard = &mut cache.lock().await;
    let data = match guard.get(&key) {
        Some(v) => {
            info!("Match cache: {}", key);
            v.to_owned()
        }
        None => {
            info!("Retrieve url");

            // 添加 `reqwest` crate
            let response = reqwest::get(url).await?;
            let data = response.bytes().await?;

            guard.put(key, data.clone());
            data
        }
    };

    Ok(data)
}

修改 generate_handler 函数,注意 Extension(cache): Extension<Cache> 参数的写法。

async fn generate_handler(
    Path(Params { spec, url }): Path<Params>,
    Extension(cache): Extension<Cache>,
) -> Result<(HeaderMap, Vec<u8>), StatusCode> {
    let url: &str = &percent_encoding::percent_decode_str(&url).decode_utf8_lossy();
    let _spec: thunbor::ImageSpec = spec
        .as_str()
        .try_into()
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    let data = retrieve_image(url, cache)
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?;

    // TODO: 处理图片

    let mut headers = HeaderMap::new();

    headers.insert("content-type", HeaderValue::from_static("image/jpeg"));

    Ok((headers, data.to_vec()))
}

再来写一个辅助测试用的函数,运行后服务器打印一个 test url,点击在浏览器打开可以得到一张和源图一模一样的图片:

fn print_test_url(url: &str) {
    let image_spec = thunbor::ImageSpec::from_specs(vec![
        thunbor::Spec::new_resize(600, 800, thunbor::resize::SampleFilter::CatmullRom),
        thunbor::Spec::new_filter(thunbor::filter::Filter::Marine),
        thunbor::Spec::new_watermark(20, 20),
    ]);
    let specs_str: String = (&image_spec).into();
    let test_image = percent_encode(url.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string();

    println!(
        "test url: http://localhost:3000/image/{}/{}",
        specs_str, test_image
    );
}

同时,目前的日志输出太过精简,使用了自定义的日志格式,显示时间戳,并使用 colored crate 提供了颜色支持:

fn init_logger() -> anyhow::Result<()> {
    Logger::try_with_env_or_str("info")?
        .format(|w, now, record| {
            let level = match record.level() {
                log::Level::Error => record.level().to_string().red(),
                log::Level::Warn => record.level().to_string().yellow(),
                log::Level::Info => record.level().to_string().green(),
                log::Level::Debug => record.level().to_string().blue(),
                log::Level::Trace => record.level().to_string().purple(),
            };
            write!(
                w,
                "[{}] {} [{}] {}",
                now.format("%Y-%m-%d %H:%M:%S%.3f"),
                level,
                record.module_path().unwrap_or("<unnamed>"),
                &record.args()
            )
        })
        .start()?;

    Ok(())
}

最终的 main 函数如下:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化日志
    init_logger()?;

    let cache: Cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1024).unwrap())));

    // 构建路由
    let router = Router::new()
        .route("/image/{spec}/{url}", routing::get(generate_handler))
        .layer(Extension(cache));

    // 启动 Web 服务器
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    print_test_url(
        "https://images.pexels.com/photos/2470905/pexels-photo-2470905.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    );

    info!("Listening on http://{}", addr);

    // 处理服务器关机
    let shutdown_signal = async {
        tokio::signal::ctrl_c().await.unwrap();
        info!("Server shutting down...");
    };

    axum::serve(listener, router)
        .with_graceful_shutdown(shutdown_signal)
        .await?;

    Ok(())
}

Cargo.toml 文件内容如下:

[dependencies]
anyhow = "1"
axum = "0.8"
base64 = "0.22"
bytes = "1"
colored = "3"
flexi_logger = "0.31"
log = "0.4"
lru = "0.16"
percent-encoding = "2" # URL编码/解码
photon-rs = "0.3" # 图片效果
prost = "0.14"
reqwest = "0.12"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }

[build-dependencies]
anyhow = "1"
prost-build = "0.14"

图片处理

我们准备使用 photon 库,photon 是一个 Rust 实现的图片处理的库(GitHub),但为了以后能够扩展使用其它图片处理库,我们设计一个 Engine trait,它提供两个方法:apply 方法对 engine 按照 specs 进行一系列有序的处理,generate 方法从 engine 中生成目标图片:

// Engine trait:未来可以添加更多的 engine,主流程只需要替换 engine
pub trait Engine {
    // 对 engine 按照 specs 进行一系列有序的处理
    fn apply(&mut self, specs: &[Spec]);
    // 从 engine 中生成目标图片,注意这里用的是 self,而非 self 的引用
    fn generate(self, format: ImageOutputFormat) -> Vec<u8>;
}

对于 apply 方法的实现,我们可以再设计一个 trait,这样可以为每个 Spec 生成对应处理:

// SpecTransform:未来如果添加更多的 spec,只需要实现它即可
pub trait SpecTransform<T> {
    // 对图片使用 op 做 transform
    fn transform(&mut self, op: T);
}

首先创建 src/engien.rs ,添加 trait 的定义:

// src/engien.rs
use image::ImageFormat;

use crate::pb::Spec;

/// 图片处理引擎
pub trait Engine {
    /// 对 engine 按照 specs 进行一系列有序处理
    fn apply(&mut self, specs: &[Spec]);

    /// 从 engine 中生成目标图片
    fn generate(self, format: ImageFormat) -> Vec<u8>;
}

/// 为每个 Spce 生成相应处理,所有 spec 实现它即可
pub trait SpecTransform<T> {
    /// 对图片使用 op 做 transform
    fn transform(&mut self, op: T);
}

然后创建 src/engine/photon.rs ,对 Photon 实现 Engine trait,并为目前的Spec实现 SpecTransform trait:

// src/engine/photon.rs
use std::io::Cursor;
use std::sync::LazyLock;

use image::{DynamicImage, ImageBuffer, ImageFormat};
use photon_rs::{PhotonImage, effects, filters, multiple, transform};

use crate::engine::{Engine, SpecTransform};
use crate::pb::*;

/// 预加载的水印文件
static WATERMARK: LazyLock<PhotonImage> = LazyLock::new(|| {
    let data = include_bytes!("../../assets/rust-logo.png");
    let watermark = photon_rs::native::open_image_from_bytes(data).unwrap();

    transform::resize(&watermark, 64, 64, transform::SamplingFilter::Nearest)
});

// 将我们的 SampleFilter 转换为 photon 的 SamplingFilter
impl From<resize::SampleFilter> for transform::SamplingFilter {
    fn from(filter: resize::SampleFilter) -> Self {
        match filter {
            resize::SampleFilter::Unspecified => transform::SamplingFilter::Nearest,
            resize::SampleFilter::Nearest => transform::SamplingFilter::Nearest,
            resize::SampleFilter::Triangle => transform::SamplingFilter::Triangle,
            resize::SampleFilter::CatmullRom => transform::SamplingFilter::CatmullRom,
            resize::SampleFilter::Gaussian => transform::SamplingFilter::Gaussian,
            resize::SampleFilter::Lanczos3 => transform::SamplingFilter::Lanczos3,
        }
    }
}

// photon-rs 相应方法里需要字符串
impl filter::Filter {
    pub fn as_str(&self) -> Option<&'static str> {
        match self {
            filter::Filter::Unspecified => None,
            filter::Filter::Oceanic => Some("oceanic"),
            filter::Filter::Islands => Some("islands"),
            filter::Filter::Marine => Some("marine"),
        }
    }
}

/// Photon engine
pub struct Photon(PhotonImage);

// 从 Bytes 转换成 Photon 结构
impl TryFrom<bytes::Bytes> for Photon {
    type Error = anyhow::Error;

    fn try_from(bytes: bytes::Bytes) -> Result<Self, Self::Error> {
        Ok(Self(PhotonImage::new_from_byteslice(bytes.to_vec())))
    }
}

// 为 Photon engine 实现 Engine trait
impl Engine for Photon {
    fn apply(&mut self, specs: &[Spec]) {
        for spec in specs {
            match spec.data {
                Some(spec::Data::Resize(ref v)) => self.transform(v),
                Some(spec::Data::Crop(ref v)) => self.transform(v),
                Some(spec::Data::Fliph(ref v)) => self.transform(v),
                Some(spec::Data::Flipv(ref v)) => self.transform(v),
                Some(spec::Data::Contrast(ref v)) => self.transform(v),
                Some(spec::Data::Filter(ref v)) => self.transform(v),
                Some(spec::Data::Watermark(ref v)) => self.transform(v),
                // 对于目前不认识的 spec,不做任何处理
                _ => {}
            }
        }
    }

    fn generate(self, format: ImageFormat) -> Vec<u8> {
        image_to_buf_with_format(self.0, format)
    }
}

// 为 Photon engine 实现支持的 Spec

impl SpecTransform<&Resize> for Photon {
    fn transform(&mut self, op: &Resize) {
        self.0 = match op.mode() {
            resize::Mode::Unspecified => panic!("Unsupported resize mode"),
            resize::Mode::Normal => {
                transform::resize(&self.0, op.width, op.height, op.filter().into())
            }
            resize::Mode::SeamCarve => transform::seam_carve(&self.0, op.width, op.height),
        };
    }
}

impl SpecTransform<&Crop> for Photon {
    fn transform(&mut self, op: &Crop) {
        self.0 = transform::crop(&self.0, op.x1, op.y1, op.x2, op.y2);
    }
}

impl SpecTransform<&FlipHorizontal> for Photon {
    fn transform(&mut self, _op: &FlipHorizontal) {
        transform::fliph(&mut self.0);
    }
}

impl SpecTransform<&FlipVertical> for Photon {
    fn transform(&mut self, _op: &FlipVertical) {
        transform::flipv(&mut self.0);
    }
}

impl SpecTransform<&Contrast> for Photon {
    fn transform(&mut self, op: &Contrast) {
        effects::adjust_contrast(&mut self.0, op.contrast);
    }
}

impl SpecTransform<&Filter> for Photon {
    fn transform(&mut self, op: &Filter) {
        let filter = op.filter();
        match filter {
            filter::Filter::Unspecified => panic!("Unsupported filter"),
            _ => filters::filter(&mut self.0, filter.as_str().unwrap()),
        }
    }
}

impl SpecTransform<&Watermark> for Photon {
    fn transform(&mut self, op: &Watermark) {
        multiple::watermark(&mut self.0, &WATERMARK, op.x, op.y);
    }
}

// 将 Image 按照指定格式进行转换
fn image_to_buf_with_format(image: PhotonImage, format: ImageFormat) -> Vec<u8> {
    let raw_pixels = image.get_raw_pixels();
    let img_width = image.get_width();
    let img_height = image.get_height();

    let img_buffer = ImageBuffer::from_vec(img_width, img_height, raw_pixels).unwrap();
    let dyn_image = DynamicImage::ImageRgba8(img_buffer);
    let mut writer = Cursor::new(Vec::with_capacity(dyn_image.as_bytes().len()));

    dyn_image.write_to(&mut writer, format).unwrap();

    writer.into_inner()
}

最后,修改 main.rs 中的 generate_handler 函数,处理之前留下的 TODO

async fn generate_handler(
    Path(Params { spec, url }): Path<Params>,
    Extension(cache): Extension<Cache>,
) -> Result<(HeaderMap, Vec<u8>), StatusCode> {
    let url: &str = &percent_encoding::percent_decode_str(&url).decode_utf8_lossy();
    let spec: pb::ImageSpec = spec
        .as_str()
        .try_into()
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    let data = retrieve_image(url, cache)
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?;

    // 使用 Image engine 处理图片
    let mut engine: Photon = data
        .try_into()
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    engine.apply(&spec.specs);

    let image = engine.generate(image::ImageFormat::Jpeg);

    info!("Finished processing image, size: {}", image.len());

    let mut headers = HeaderMap::new();

    headers.insert("content-type", HeaderValue::from_static("image/jpeg"));

    Ok((headers, image))
}

将图片链接替换为:https://images.pexels.com/photos/1562477/pexels-photo-1562477.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260,也可以使用你喜欢的图片和水印:

print_test_url("https://images.pexels.com/photos/1562477/pexels-photo-1562477.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260");

最终项目结构:

.
└── abi.proto
└── assets
    └── rust-logo.png
    └── thunbor.webp
└── build.rs
└── Cargo.toml
└── examples
    └── image_spce.rs
└── LICENSE-MIT.txt
└── README.md
└── src
    └── engine
        └── photon.rs
    └── engine.rs
    └── lib.rs
    └── main.rs
    └── pb
        └── abi.rs
    └── pb.rs

About

Rust 实现的 thunbor 图片处理服务器,参照陈天老师《Rust编程第一课》基础篇第6讲实现:https://time.geekbang.org/column/article/413634

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages