注册

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

今天收到一个需求,要求在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.int0()).expect("Failed to load image");

if let Some(size) = resize {
debug!("开始格式化尺寸:{:?}", size);
img = img.resize(size.width, size.height, size.filter.int0());
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…


作者:梁典典
来源:juejin.cn/post/7412486655862734874

0 个评论

要回复文章请先登录注册