用rust写个flutter插件并上传 pub.dev

1,229 阅读5分钟

今天收到一个需求,要求在flutter端把任意类型的图片,转换成bmp类型的图片,然后把 bmp位图发送到条码打印机,生成的 bmp图片是 1 位深度的,只有黑白两种像素颜色

包已经上传到 pub.dev,pub.dev/packages/ld…

效果图

image.png image.png

1.生成插件包

crates.io地址: crates.io/crates/frb_… 安装命令

cargo install frb_plugin_tool

使用很简单,输入frb_plugin_tool即可

image.png

按照提示输入插件名 创建后的项目目录大概像这样

image.png

2. 编写 rust代码

我这里图片转 bmp工具用的是rust image这个包

添加依赖

cd rust && cargo add image

然后在 src/api目录下添加image.rs文件

use std::{io::Cursor, time::Instant};

use bytesize::ByteSize;
use humantime::format_duration;
use image::{GrayImage, Luma};
use indicatif::ProgressBar;
use log::debug;

use super::entitys::{LddImageType, ResizeOpt};

///任意图像转 1 位深度的数据
pub fn convert_to_1bit_bmp(
    input_data: &[u8],
    image_type: LddImageType,
    resize: Option<ResizeOpt>,
    is_apply_ordered_dithering: Option<bool>,
) -> Vec<u8> {
    let use_ordered_dithering = is_apply_ordered_dithering.map_or(false, |v| v);
    let start = Instant::now();
    debug!("开始转换,数据大小:{:?}", ByteSize(input_data.len() as u64));
    let mut img =
        image::load(Cursor::new(input_data), image_type.into()).expect("Failed to load image");

    if let Some(size) = resize {
        debug!("开始格式化尺寸:{:?}", size);
        img = img.resize(size.width, size.height, size.filter.into());
        debug!("✅格式化尺寸完成");
    }

    let mut gray_img = img.to_luma8(); // 转换为灰度图像
    if use_ordered_dithering {
        debug!("✅使用 h4x4a 抖动算法");
        gray_img = apply_ordered_dithering(&gray_img);
    }

    let (width, height) = gray_img.dimensions();
    let row_size = ((width + 31) / 32) * 4; // 每行字节数 4 字节对齐
    let mut bmp_data = vec![0u8; row_size as usize * height as usize];

    // 创建进度条
    let progress_bar = ProgressBar::new(height as u64);

    // 二值化处理并填充 BMP 数据(1 位深度)
    let threshold = 128;
    for y in 0..height {
        let inverted_y = height - 1 - y; // 倒置行顺序
        for x in 0..width {
            let pixel = gray_img.get_pixel(x, y)[0];
            if pixel >= threshold {
                bmp_data[inverted_y as usize * (row_size as usize) + (x / 8) as usize] |=
                    1 << (7 - (x % 8));
            }
        }
        progress_bar.inc(1); // 每处理一行,进度条增加一格
    }

    progress_bar.finish_with_message("Conversion complete!");

    // BMP 文件头和 DIB 信息头
    let file_size = 14 + 40 + 8 + bmp_data.len(); // 文件头 + DIB 头 + 调色板 + 位图数据
    let bmp_header = vec![
        0x42,
        0x4D, // "BM"
        (file_size & 0xFF) as u8,
        ((file_size >> 8) & 0xFF) as u8,
        ((file_size >> 16) & 0xFF) as u8,
        ((file_size >> 24) & 0xFF) as u8,
        0x00,
        0x00, // 保留字段
        0x00,
        0x00, // 保留字段
        54 + 8,
        0x00,
        0x00,
        0x00, // 数据偏移(54 字节 + 调色板大小)
    ];

    let dib_header = vec![
        40,
        0x00,
        0x00,
        0x00, // DIB 头大小(40 字节)
        (width & 0xFF) as u8,
        ((width >> 8) & 0xFF) as u8,
        ((width >> 16) & 0xFF) as u8,
        ((width >> 24) & 0xFF) as u8,
        (height & 0xFF) as u8,
        ((height >> 8) & 0xFF) as u8,
        ((height >> 16) & 0xFF) as u8,
        ((height >> 24) & 0xFF) as u8,
        1,
        0x00, // 颜色平面数
        1,
        0x00, // 位深度(1 位)
        0x00,
        0x00,
        0x00,
        0x00, // 无压缩
        0x00,
        0x00,
        0x00,
        0x00, // 图像大小(可为 0,表示无压缩)
        0x13,
        0x0B,
        0x00,
        0x00, // 水平分辨率(2835 像素/米)
        0x13,
        0x0B,
        0x00,
        0x00, // 垂直分辨率(2835 像素/米)
        0x02,
        0x00,
        0x00,
        0x00, // 调色板颜色数(2)
        0x00,
        0x00,
        0x00,
        0x00, // 重要颜色数(0 表示所有颜色都重要)
    ];

    // 调色板(黑白)
    let palette = vec![
        0x00, 0x00, 0x00, 0x00, // 黑色
        0xFF, 0xFF, 0xFF, 0x00, // 白色
    ];

    // 将所有部分组合成 BMP 文件数据
    let mut bmp_file_data = Vec::with_capacity(file_size);
    bmp_file_data.extend(bmp_header);
    bmp_file_data.extend(dib_header);
    bmp_file_data.extend(palette);
    bmp_file_data.extend(bmp_data);

    let duration = start.elapsed(); // 计算耗时
    debug!(
        "✅转换完成,数据大小:{:?},耗时:{}",
        ByteSize(bmp_file_data.len() as u64),
        format_duration(duration)
    );
    bmp_file_data
}

// 有序抖动矩阵(4x4 Bayer 矩阵)
const DITHER_MATRIX: [[f32; 4]; 4] = [
    [0.0, 8.0, 2.0, 10.0],
    [12.0, 4.0, 14.0, 6.0],
    [3.0, 11.0, 1.0, 9.0],
    [15.0, 7.0, 13.0, 5.0],
];

//h4x4a 抖动算法
fn apply_ordered_dithering(image: &GrayImage) -> GrayImage {
    let (width, height) = image.dimensions();
    let mut dithered_image = GrayImage::new(width, height);

    for y in 0..height {
        for x in 0..width {
            let pixel = image.get_pixel(x, y)[0];
            let threshold = DITHER_MATRIX[(y % 4) as usize][(x % 4) as usize] / 16.0 * 255.0;
            let new_pixel_value = if pixel as f32 > threshold { 255 } else { 0 };
            dithered_image.put_pixel(x, y, Luma([new_pixel_value]));
        }
    }

    dithered_image
}

生成 dart代码,在项目根目录下执行

flutter_rust_bridge_codegen generate

会在dart lib下生成对应的文件 image.png

在项目中使用

编写 example , main.dart.

import 'dart:io';
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:ldd_bmp/api/entitys.dart';
import 'package:ldd_bmp/api/image.dart';
import 'package:ldd_bmp/ldd_bmp.dart';
import 'dart:async';

import 'package:path_provider/path_provider.dart';

const reSize = ResizeOpt(
  width: 200,
  height: 200,
  filter: LddFilterType.nearest,
);

Future<void> main() async {
  await bmpSdkInit();
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  File? file;
  Uint8List? bmpData;
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Native Packages'),
        ),
        body: SingleChildScrollView(
          child: Column(
            children: [
              FilledButton(onPressed: selectFile, child: const Text('选择文件')),
              if (file != null)
                Image.file(
                  file!,
                  width: 200,
                  height: 200,
                ),
              ElevatedButton(
                  onPressed: file == null
                      ? null
                      : () async {
                          final bts = await file!.readAsBytes();
                          bmpData = await convertTo1BitBmp(
                              inputData: bts,
                              imageType: LddImageType.jpeg,
                              isApplyOrderedDithering: true,
                              resize: const ResizeOpt(
                                width: 200,
                                height: 200,
                                filter: LddFilterType.nearest,
                              ));
                          setState(() {});
                        },
                  child: const Text("转换")),
              ElevatedButton(
                  onPressed: bmpData == null
                      ? null
                      : () {
                          saveImageToFile(bmpData!);
                        },
                  child: const Text("保存图片"))
            ],
          ),
        ),
        floatingActionButton: bmpData != null
            ? ConstrainedBox(
                constraints:
                    const BoxConstraints(maxHeight: 300, maxWidth: 300),
                child: Card(
                  elevation: 10,
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      children: [
                        const Text('转换结果'),
                        Image.memory(bmpData!),
                      ],
                    ),
                  ),
                ),
              )
            : null,
      ),
    );
  }

  Future<void> selectFile() async {
    setState(() {
      file = null;
    });
    FilePickerResult? result = await FilePicker.platform.pickFiles();
    if (result != null) {
      file = File(result.files.single.path!);
      setState(() {});
    } else {
      // User canceled the picker
    }
  }
}

Future<void> saveImageToFile(Uint8List imageData) async {
  // 获取应用程序的文档目录
  final directory = await getApplicationDocumentsDirectory();

  // 设置文件路径和文件名
  final filePath = '${directory.path}/image.bmp';

  // 创建一个文件对象
  final file = File(filePath);

  // 将Uint8List数据写入文件
  await file.writeAsBytes(imageData);

  print('Image saved to $filePath');
}

image.png

转换速度还是挺快的,运行效果

image.png

上传到 pub.dev

这个包已经上传到仓库了,可以直接使用 pub.dev/packages/ld…