Compare commits

...

10 commits

Author SHA1 Message Date
Joe Ardent
dc500c1432 add mobile icon image 2024-04-26 10:41:29 -07:00
Joe Ardent
be1e6e3f1e add logo, do more aggressive batching 2023-08-30 10:11:35 -07:00
Joe Ardent
d8ed7acddd batch calls to raptor check for better memory behavior.
It will still OOM because the barcode scanner will fill up memomry with unprocessed codes, so need to turn the cam off to let it drain.
2023-08-27 09:46:43 -07:00
Joe Ardent
85c60913d5 add drop code for txconf 2023-08-25 16:35:27 -07:00
Joe Ardent
69ae823e68 update readme 2023-08-20 13:10:51 -07:00
Joe Ardent
4501e333f0 fucking works
can transfer files from desktop to mobile and save them there.
2023-08-20 12:20:33 -07:00
Joe Ardent
0086fb1774 reorg cuttle lib, add filename to txconfig 2023-08-19 11:16:01 -07:00
Joe Ardent
7c06c663d5 fix error in check_raptor 2023-08-19 09:23:12 -07:00
Joe Ardent
d4de0a9e81 have mobile rust lib working for txconfig 2023-08-18 17:12:44 -07:00
Joe Ardent
881d225202 add bytechecking for txconfig, drop repr(c) 2023-08-18 16:12:54 -07:00
26 changed files with 809 additions and 268 deletions

29
Cargo.lock generated
View file

@ -571,7 +571,18 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
dependencies = [
"bytecheck_derive",
"bytecheck_derive 0.6.11",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41502630fe304ce54cbb2f8389e017784dee2b0328147779fcbe43b9db06d35d"
dependencies = [
"bytecheck_derive 0.7.0",
"ptr_meta",
"simdutf8",
]
@ -587,6 +598,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "bytecheck_derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda88c587085bc07dc201ab9df871bd9baa5e07f7754b745e4d7194b43ac1eda"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.13.1"
@ -905,6 +927,7 @@ dependencies = [
name = "cuttle"
version = "0.1.0"
dependencies = [
"bytecheck 0.7.0",
"clap",
"eframe",
"egui_extras",
@ -2513,7 +2536,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab"
dependencies = [
"bytecheck",
"bytecheck 0.6.11",
]
[[package]]
@ -2557,7 +2580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58"
dependencies = [
"bitvec",
"bytecheck",
"bytecheck 0.6.11",
"hashbrown 0.12.3",
"ptr_meta",
"rend",

View file

@ -8,6 +8,7 @@ default = ["desktop"]
desktop = ["dep:clap", "dep:eframe", "dep:egui_extras", "dep:fast_qr", "dep:png"]
[dependencies]
bytecheck = "0.7"
clap = { version = "4.3", optional = true, features = ["derive", "env"] }
eframe = { version = "0.22", default-features = false, optional = true, features = ["default_fonts", "wgpu", "tts", "accesskit"] }
egui_extras = { version = "0.22", default-features = false, optional = true, features = ["chrono", "image"] }

View file

@ -22,15 +22,36 @@ Raptor codes for that file. Each piece of Raptor code then gets encoded as a QR
displayed for a period of time on the screen. The user then holds up the receiving computer's camera
in order to receive the QR encoded raptor codes, and, *voila*, the file has been transferred!
# Current status
# Current status (2023-08-20)
The desktop transmitting application is nearly done; already it can convert text input into QR codes
(either a single static one if the data is small enough, otherwise it will stream a never ending
loop of them), and the mobile app has been started. Even without the mobile app, it's still
already useful for transmitting short text strings to the phone, since the Android camera app will
decode any detected QR codes.
Currently, the following is done in terms of functionality:
**Desktop**
- [x] transmit short text given on the commandline with a single qr code
- [x] transmit large text given on the commandline with a stream of qr codes
- [x] transmit small files given on the commandline with a single qr code
- [x] transmit large files given on the commandline with a stream of qr codes
- [ ] receive text or files with a single qr code
- [ ] receive text or files with a stream of qr codes
**Android app**
- [ ] transmit short given text with a single qr code
- [ ] transmit large given text with a stream of qr codes
- [ ] transmit small files with a single qr code
- [ ] transmit large files with a stream of qr codes
- [x] receive text or files with a single qr code
- [x] receive text or files with a stream of qr codes
The desktop app (unix-ish only; developed on linux, untested on anything else, but should run on macos)
is closer to its final form in terms of UI/UX, though it currently can only transmit. The Android
app is about halfway done in terms of functionality, but the UI is, shall we say, "bad":
![cuttle mobile app receiving data](./mobile_receiving.png)
I might take a crack at making it less hideous before I start on the mobile transmitting/desktop
receiving side of the functionality, but maybe not! I often want to transfer a file or text from my
phone to my desktop, and I'm willing to live with an ugly UI for a little bit.
The mobile app has been started, and I hope to have it decoding QR streams soon!
# about the name

View file

@ -1,3 +1,38 @@
use rand::{seq::SliceRandom, Rng};
use raptorq::{Decoder, Encoder, EncodingPacket, ObjectTransmissionInformation};
fn main() {
//
let rng = &mut rand::thread_rng();
let len = 20_000;
let mut v = Vec::with_capacity(len);
for _ in 0..len {
v.push(rng.gen::<u8>());
}
let config = ObjectTransmissionInformation::with_defaults(len as u64, 1200);
let encoder = Encoder::new(&v, config);
let mut packets = encoder
.get_encoded_packets(10)
.iter()
.map(|p| p.serialize())
.collect::<Vec<_>>();
packets.shuffle(rng);
let mut decoder = Decoder::new(config);
let mut v2 = None;
for (i, p) in packets.iter().enumerate() {
v2 = decoder.decode(EncodingPacket::deserialize(p));
if v2.is_some() {
println!(
"recovered after {i} packets received, out of {} total",
packets.len()
);
break;
}
}
assert!(v2.is_some());
assert_eq!(v2.unwrap(), v);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
org.gradle.daemon=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View file

@ -5,9 +5,16 @@ gen:
flutter_rust_bridge_codegen
lint:
cd native && cargo fmt
dart format .
native: native-debug native-release
native-release:
cd android && ./gradlew :app:cargoBuildRelease
native-debug:
cd android && ./gradlew :app:cargoBuildDebug
clean:
flutter clean
cd native && cargo clean

View file

@ -9,22 +9,37 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import 'package:uuid/uuid.dart';
abstract class Native {
Future<Platform> platform({dynamic hint});
Future<TxConfig?> getTxConfig({required Uint8List bytes, dynamic hint});
FlutterRustBridgeTaskConstMeta get kPlatformConstMeta;
FlutterRustBridgeTaskConstMeta get kGetTxConfigConstMeta;
Future<bool> rustReleaseMode({dynamic hint});
Future<void> dropTxConfig({required TxConfig txc, dynamic hint});
FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta;
FlutterRustBridgeTaskConstMeta get kDropTxConfigConstMeta;
Future<Uint8List?> decodePackets({required List<RaptorPacket> packets, required TxConfig txconf, dynamic hint});
FlutterRustBridgeTaskConstMeta get kDecodePacketsConstMeta;
}
enum Platform {
Unknown,
AndroidBish,
Ios,
Windows,
Unix,
MacIntel,
MacApple,
Wasm,
class RaptorPacket {
final Uint8List field0;
const RaptorPacket({
required this.field0,
});
}
class TxConfig {
final int len;
final int mtu;
final String description;
final String? filename;
const TxConfig({
required this.len,
required this.mtu,
required this.description,
this.filename,
});
}

View file

@ -24,34 +24,53 @@ class NativeImpl implements Native {
/// Only valid on web/WASM platforms.
factory NativeImpl.wasm(FutureOr<WasmModule> module) => NativeImpl(module as ExternalLibrary);
NativeImpl.raw(this._platform);
Future<Platform> platform({dynamic hint}) {
Future<TxConfig?> getTxConfig({required Uint8List bytes, dynamic hint}) {
var arg0 = _platform.api2wire_uint_8_list(bytes);
return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_platform(port_),
parseSuccessData: _wire2api_platform,
constMeta: kPlatformConstMeta,
argValues: [],
callFfi: (port_) => _platform.inner.wire_get_tx_config(port_, arg0),
parseSuccessData: _wire2api_opt_box_autoadd_tx_config,
constMeta: kGetTxConfigConstMeta,
argValues: [bytes],
hint: hint,
));
}
FlutterRustBridgeTaskConstMeta get kPlatformConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "platform",
argNames: [],
FlutterRustBridgeTaskConstMeta get kGetTxConfigConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "get_tx_config",
argNames: ["bytes"],
);
Future<bool> rustReleaseMode({dynamic hint}) {
Future<void> dropTxConfig({required TxConfig txc, dynamic hint}) {
var arg0 = _platform.api2wire_box_autoadd_tx_config(txc);
return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_rust_release_mode(port_),
parseSuccessData: _wire2api_bool,
constMeta: kRustReleaseModeConstMeta,
argValues: [],
callFfi: (port_) => _platform.inner.wire_drop_tx_config(port_, arg0),
parseSuccessData: _wire2api_unit,
constMeta: kDropTxConfigConstMeta,
argValues: [txc],
hint: hint,
));
}
FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "rust_release_mode",
argNames: [],
FlutterRustBridgeTaskConstMeta get kDropTxConfigConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "drop_tx_config",
argNames: ["txc"],
);
Future<Uint8List?> decodePackets({required List<RaptorPacket> packets, required TxConfig txconf, dynamic hint}) {
var arg0 = _platform.api2wire_list_raptor_packet(packets);
var arg1 = _platform.api2wire_box_autoadd_tx_config(txconf);
return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_decode_packets(port_, arg0, arg1),
parseSuccessData: _wire2api_opt_uint_8_list,
constMeta: kDecodePacketsConstMeta,
argValues: [packets, txconf],
hint: hint,
));
}
FlutterRustBridgeTaskConstMeta get kDecodePacketsConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "decode_packets",
argNames: ["packets", "txconf"],
);
void dispose() {
@ -59,21 +78,70 @@ class NativeImpl implements Native {
}
// Section: wire2api
bool _wire2api_bool(dynamic raw) {
return raw as bool;
String _wire2api_String(dynamic raw) {
return raw as String;
}
int _wire2api_i32(dynamic raw) {
TxConfig _wire2api_box_autoadd_tx_config(dynamic raw) {
return _wire2api_tx_config(raw);
}
String? _wire2api_opt_String(dynamic raw) {
return raw == null ? null : _wire2api_String(raw);
}
TxConfig? _wire2api_opt_box_autoadd_tx_config(dynamic raw) {
return raw == null ? null : _wire2api_box_autoadd_tx_config(raw);
}
Uint8List? _wire2api_opt_uint_8_list(dynamic raw) {
return raw == null ? null : _wire2api_uint_8_list(raw);
}
TxConfig _wire2api_tx_config(dynamic raw) {
final arr = raw as List<dynamic>;
if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
return TxConfig(
len: _wire2api_u64(arr[0]),
mtu: _wire2api_u16(arr[1]),
description: _wire2api_String(arr[2]),
filename: _wire2api_opt_String(arr[3]),
);
}
int _wire2api_u16(dynamic raw) {
return raw as int;
}
Platform _wire2api_platform(dynamic raw) {
return Platform.values[raw as int];
int _wire2api_u64(dynamic raw) {
return castInt(raw);
}
int _wire2api_u8(dynamic raw) {
return raw as int;
}
Uint8List _wire2api_uint_8_list(dynamic raw) {
return raw as Uint8List;
}
void _wire2api_unit(dynamic raw) {
return;
}
}
// Section: api2wire
@protected
int api2wire_u16(int raw) {
return raw;
}
@protected
int api2wire_u8(int raw) {
return raw;
}
// Section: finalizer
class NativePlatform extends FlutterRustBridgeBase<NativeWire> {
@ -81,9 +149,61 @@ class NativePlatform extends FlutterRustBridgeBase<NativeWire> {
// Section: api2wire
@protected
ffi.Pointer<wire_uint_8_list> api2wire_String(String raw) {
return api2wire_uint_8_list(utf8.encoder.convert(raw));
}
@protected
ffi.Pointer<wire_TxConfig> api2wire_box_autoadd_tx_config(TxConfig raw) {
final ptr = inner.new_box_autoadd_tx_config_0();
_api_fill_to_wire_tx_config(raw, ptr.ref);
return ptr;
}
@protected
ffi.Pointer<wire_list_raptor_packet> api2wire_list_raptor_packet(List<RaptorPacket> raw) {
final ans = inner.new_list_raptor_packet_0(raw.length);
for (var i = 0; i < raw.length; ++i) {
_api_fill_to_wire_raptor_packet(raw[i], ans.ref.ptr[i]);
}
return ans;
}
@protected
ffi.Pointer<wire_uint_8_list> api2wire_opt_String(String? raw) {
return raw == null ? ffi.nullptr : api2wire_String(raw);
}
@protected
int api2wire_u64(int raw) {
return raw;
}
@protected
ffi.Pointer<wire_uint_8_list> api2wire_uint_8_list(Uint8List raw) {
final ans = inner.new_uint_8_list_0(raw.length);
ans.ref.ptr.asTypedList(raw.length).setAll(0, raw);
return ans;
}
// Section: finalizer
// Section: api_fill_to_wire
void _api_fill_to_wire_box_autoadd_tx_config(TxConfig apiObj, ffi.Pointer<wire_TxConfig> wireObj) {
_api_fill_to_wire_tx_config(apiObj, wireObj.ref);
}
void _api_fill_to_wire_raptor_packet(RaptorPacket apiObj, wire_RaptorPacket wireObj) {
wireObj.field0 = api2wire_uint_8_list(apiObj.field0);
}
void _api_fill_to_wire_tx_config(TxConfig apiObj, wire_TxConfig wireObj) {
wireObj.len = api2wire_u64(apiObj.len);
wireObj.mtu = api2wire_u16(apiObj.mtu);
wireObj.description = api2wire_String(apiObj.description);
wireObj.filename = api2wire_opt_String(apiObj.filename);
}
}
// ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names
@ -164,28 +284,88 @@ class NativeWire implements FlutterRustBridgeWireBase {
_lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.Pointer<ffi.Void>)>>('init_frb_dart_api_dl');
late final _init_frb_dart_api_dl = _init_frb_dart_api_dlPtr.asFunction<int Function(ffi.Pointer<ffi.Void>)>();
void wire_platform(
void wire_get_tx_config(
int port_,
ffi.Pointer<wire_uint_8_list> bytes,
) {
return _wire_platform(
return _wire_get_tx_config(
port_,
bytes,
);
}
late final _wire_platformPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_platform');
late final _wire_platform = _wire_platformPtr.asFunction<void Function(int)>();
late final _wire_get_tx_configPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Pointer<wire_uint_8_list>)>>('wire_get_tx_config');
late final _wire_get_tx_config =
_wire_get_tx_configPtr.asFunction<void Function(int, ffi.Pointer<wire_uint_8_list>)>();
void wire_rust_release_mode(
void wire_drop_tx_config(
int port_,
ffi.Pointer<wire_TxConfig> _txc,
) {
return _wire_rust_release_mode(
return _wire_drop_tx_config(
port_,
_txc,
);
}
late final _wire_rust_release_modePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_rust_release_mode');
late final _wire_rust_release_mode = _wire_rust_release_modePtr.asFunction<void Function(int)>();
late final _wire_drop_tx_configPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Pointer<wire_TxConfig>)>>('wire_drop_tx_config');
late final _wire_drop_tx_config =
_wire_drop_tx_configPtr.asFunction<void Function(int, ffi.Pointer<wire_TxConfig>)>();
void wire_decode_packets(
int port_,
ffi.Pointer<wire_list_raptor_packet> packets,
ffi.Pointer<wire_TxConfig> txconf,
) {
return _wire_decode_packets(
port_,
packets,
txconf,
);
}
late final _wire_decode_packetsPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Int64, ffi.Pointer<wire_list_raptor_packet>, ffi.Pointer<wire_TxConfig>)>>('wire_decode_packets');
late final _wire_decode_packets = _wire_decode_packetsPtr
.asFunction<void Function(int, ffi.Pointer<wire_list_raptor_packet>, ffi.Pointer<wire_TxConfig>)>();
ffi.Pointer<wire_TxConfig> new_box_autoadd_tx_config_0() {
return _new_box_autoadd_tx_config_0();
}
late final _new_box_autoadd_tx_config_0Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_TxConfig> Function()>>('new_box_autoadd_tx_config_0');
late final _new_box_autoadd_tx_config_0 =
_new_box_autoadd_tx_config_0Ptr.asFunction<ffi.Pointer<wire_TxConfig> Function()>();
ffi.Pointer<wire_list_raptor_packet> new_list_raptor_packet_0(
int len,
) {
return _new_list_raptor_packet_0(
len,
);
}
late final _new_list_raptor_packet_0Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_list_raptor_packet> Function(ffi.Int32)>>('new_list_raptor_packet_0');
late final _new_list_raptor_packet_0 =
_new_list_raptor_packet_0Ptr.asFunction<ffi.Pointer<wire_list_raptor_packet> Function(int)>();
ffi.Pointer<wire_uint_8_list> new_uint_8_list_0(
int len,
) {
return _new_uint_8_list_0(
len,
);
}
late final _new_uint_8_list_0Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_uint_8_list> Function(ffi.Int32)>>('new_uint_8_list_0');
late final _new_uint_8_list_0 = _new_uint_8_list_0Ptr.asFunction<ffi.Pointer<wire_uint_8_list> Function(int)>();
void free_WireSyncReturn(
WireSyncReturn ptr,
@ -202,6 +382,36 @@ class NativeWire implements FlutterRustBridgeWireBase {
final class _Dart_Handle extends ffi.Opaque {}
final class wire_uint_8_list extends ffi.Struct {
external ffi.Pointer<ffi.Uint8> ptr;
@ffi.Int32()
external int len;
}
final class wire_TxConfig extends ffi.Struct {
@ffi.Uint64()
external int len;
@ffi.Uint16()
external int mtu;
external ffi.Pointer<wire_uint_8_list> description;
external ffi.Pointer<wire_uint_8_list> filename;
}
final class wire_RaptorPacket extends ffi.Struct {
external ffi.Pointer<wire_uint_8_list> field0;
}
final class wire_list_raptor_packet extends ffi.Struct {
external ffi.Pointer<wire_RaptorPacket> ptr;
@ffi.Int32()
external int len;
}
typedef DartPostCObjectFnType
= ffi.Pointer<ffi.NativeFunction<ffi.Bool Function(DartPort port_id, ffi.Pointer<ffi.Void> message)>>;
typedef DartPort = ffi.Int64;

View file

@ -9,7 +9,6 @@ export 'bridge_definitions.dart';
// Re-export the bridge so it is only necessary to import this file.
export 'bridge_generated.dart';
import 'dart:io' as io;
const _base = 'native';

View file

@ -1,23 +1,33 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:typed_data';
import 'package:cuttle/utils/mlkit_utils.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:flutter/material.dart';
import 'ffi.dart' if (dart.library.html) 'ffi_web.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:intl/intl.dart';
import 'package:rxdart/rxdart.dart';
import 'ffi.dart';
import 'utils/mlkit_utils.dart';
void main() {
runApp(const MyApp());
}
enum CuttleState {
unitialized,
receiving,
received,
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'camerAwesome App',
title: 'Cuttle',
theme: ThemeData(
primarySwatch: Colors.blue,
),
@ -35,15 +45,22 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
final _barcodeScanner = BarcodeScanner(formats: [BarcodeFormat.qrCode]);
// late final AnalysisController _scannerController;
final _rxTextController = BehaviorSubject<String>();
late final Stream<String> _rxTextStream = _rxTextController.stream;
final _buffer = <String>[];
final _barcodesController = BehaviorSubject<List<String>>();
late final Stream<List<String>> _barcodesStream = _barcodesController.stream;
final _scrollController = ScrollController();
TxConfig? _txConfig;
var _cuttleState = CuttleState.unitialized;
final HashSet<RaptorPacket> _rxData = HashSet();
String _rxText = '';
int _rxCount = 1;
int _bSize = 0;
final _formatter = NumberFormat('###,###,###');
@override
void dispose() {
_barcodesController.close();
_rxTextController.close();
super.dispose();
}
@ -51,19 +68,19 @@ class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
body: CameraAwesomeBuilder.previewOnly(
zoom: 1.5,
onImageForAnalysis: (img) => _processImageBarcode(img),
imageAnalysisConfig: AnalysisConfig(
androidOptions: const AndroidAnalysisOptions.nv21(
width: 1024,
width: 600,
),
maxFramesPerSecond: 5,
autoStart: false,
maxFramesPerSecond: 20,
autoStart: true,
),
builder: (cameraModeState, previewSize, previewRect) {
return _BarcodeDisplayWidget(
barcodesStream: _barcodesStream,
scrollController: _scrollController,
analysisController: cameraModeState.analysisController!,
//_scannerController = cameraModeState.analysisController!;
return _RxTextDisplayWidget(
rxTextStream: _rxTextStream,
);
},
),
@ -76,96 +93,133 @@ class _MyHomePageState extends State<MyHomePage> {
try {
var recognizedBarCodes = await _barcodeScanner.processImage(inputImage);
for (Barcode barcode in recognizedBarCodes) {
debugPrint("Barcode: [${barcode.format}]: ${barcode.rawBytes}");
_addBarcode("[${barcode.format.name}]: ${barcode.rawValue}");
var bytes = barcode.rawBytes;
if (bytes == null) {
continue;
}
final Uint8List dbytes = bytes;
switch (_cuttleState) {
case CuttleState.unitialized:
{
var txconf = await api.getTxConfig(bytes: dbytes);
if (txconf != null) {
_txConfig = txconf;
_cuttleState = CuttleState.receiving;
final fname =
_txConfig!.filename ?? "large text on the command line";
final desc = _txConfig!.description;
final text =
'Receiving $fname, ${_formatter.format(_txConfig!.len)} bytes ($desc)';
_rxText = text;
_rxTextController.add(text);
continue;
}
// implicit else here; txconf was null
var text = barcode.rawValue;
if (text != null) {
// it's not a txconfig, and it's not a raptor packet, so it must be a regular qr code
_rxText = text;
_rxTextController.add(text);
_cuttleState = CuttleState.received;
}
}
case CuttleState.receiving:
{
if (_bSize > 0 && dbytes.length < _bSize) {
continue;
}
if (_bSize == 0) {
var txconf = await api.getTxConfig(bytes: dbytes);
if (txconf != null) {
await api.dropTxConfig(txc: txconf);
continue;
}
}
_bSize = dbytes.length;
final packet = RaptorPacket(field0: dbytes);
_rxData.add(packet);
_rxCount += 1;
final bytesTotal = _rxData.length * dbytes.length;
if (_rxCount % 40 == 0) {
_rxCount = 1;
// if we've not received at least as many bytes as txconf.len,
// we cannot have enough bytes to reconstruct, so only try to
// decode if we've gotten at least that many
if (bytesTotal > _txConfig!.len) {
final content = await api.decodePackets(
packets: _rxData.toList(), txconf: _txConfig!);
if (content != null) {
_rxData.clear();
_barcodeScanner.close();
_cuttleState = CuttleState.received;
_rxTextController.add("DONE RECEIVING $_rxText");
final f =
await _saveReceivedFile(_txConfig!.filename, content);
_rxTextController.add("Saved content to $f");
continue;
}
}
}
final pct = (100.0 * bytesTotal / _txConfig!.len).floor();
_rxTextController.add(
"$_rxText -- $pct% received (${_formatter.format(bytesTotal)} bytes)");
}
case CuttleState.received:
continue;
}
}
} catch (error) {
debugPrint("...sending image resulted error $error");
debugPrint("sending image resulted error $error");
}
}
void _addBarcode(String value) {
try {
if (_buffer.length > 300) {
_buffer.removeRange(_buffer.length - 300, _buffer.length);
}
if (_buffer.isEmpty || value != _buffer[0]) {
_buffer.insert(0, value);
_barcodesController.add(_buffer);
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 400),
curve: Curves.fastLinearToSlowEaseIn,
);
}
} catch (err) {
debugPrint("...logging error $err");
}
Future<String> _saveReceivedFile(String? filename, Uint8List bytes) async {
final Directory downloadDir = Directory('/storage/emulated/0/Download');
final String fname =
filename ?? "cuttle_${DateTime.now().millisecondsSinceEpoch}.txt";
final String path = "${downloadDir.path}/$fname";
final file = await File(path).create();
await file.writeAsBytes(bytes, flush: true);
return path;
}
}
class _BarcodeDisplayWidget extends StatefulWidget {
final Stream<List<String>> barcodesStream;
final ScrollController scrollController;
class _RxTextDisplayWidget extends StatefulWidget {
final Stream<String> rxTextStream;
final AnalysisController analysisController;
const _BarcodeDisplayWidget({
const _RxTextDisplayWidget({
// ignore: unused_element
super.key,
required this.barcodesStream,
required this.scrollController,
required this.analysisController,
required this.rxTextStream,
});
@override
State<_BarcodeDisplayWidget> createState() => _BarcodeDisplayWidgetState();
State<_RxTextDisplayWidget> createState() => _RxTextDisplayWidgetState();
}
class _BarcodeDisplayWidgetState extends State<_BarcodeDisplayWidget> {
class _RxTextDisplayWidgetState extends State<_RxTextDisplayWidget> {
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: Colors.tealAccent.withOpacity(0.7),
decoration: const BoxDecoration(
color: Colors.white,
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Material(
color: Colors.transparent,
child: CheckboxListTile(
value: widget.analysisController.enabled,
onChanged: (newValue) async {
if (widget.analysisController.enabled == true) {
await widget.analysisController.stop();
} else {
await widget.analysisController.start();
}
setState(() {});
},
title: const Text(
"Enable barcode scan",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Container(
height: 120,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamBuilder<List<String>>(
stream: widget.barcodesStream,
builder: (context, value) => !value.hasData
? const SizedBox.expand()
: ListView.separated(
padding: const EdgeInsets.only(top: 8),
controller: widget.scrollController,
itemCount: value.data!.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 4),
itemBuilder: (context, index) => Text(value.data![index]),
),
),
child: SelectionArea(
child: StreamBuilder<String>(
stream: widget.rxTextStream,
builder: (context, value) =>
!value.hasData ? const SizedBox.expand() : Text(value.data!),
)),
),
]),
),

View file

@ -6,8 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "lib"]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
cuttle = { path = "../../", default-features = false }
flutter_rust_bridge = "1"
raptorq = "1.7.0"
rkyv = { version = "0.7.42", features = ["validation"] }

View file

@ -1,59 +1,48 @@
// This is the entry point of your Rust library.
// When adding new code to your project, note that only items used
// here will be transformed to their Dart equivalents.
use raptorq::{Decoder, EncodingPacket, ObjectTransmissionInformation};
// A plain enum without any fields. This is similar to Dart- or C-style enums.
// flutter_rust_bridge is capable of generating code for enums with fields
// (@freezed classes in Dart and tagged unions in C).
pub enum Platform {
Unknown,
AndroidBish,
Ios,
Windows,
Unix,
MacIntel,
MacApple,
Wasm,
#[derive(Debug, Clone)]
pub struct TxConfig {
pub len: u64,
pub mtu: u16,
pub description: String,
pub filename: Option<String>,
}
// A function definition in Rust. Similar to Dart, the return type must always be named
// and is never inferred.
pub fn platform() -> Platform {
// This is a macro, a special expression that expands into code. In Rust, all macros
// end with an exclamation mark and can be invoked with all kinds of brackets (parentheses,
// brackets and curly braces). However, certain conventions exist, for example the
// vector macro is almost always invoked as vec![..].
//
// The cfg!() macro returns a boolean value based on the current compiler configuration.
// When attached to expressions (#[cfg(..)] form), they show or hide the expression at compile time.
// Here, however, they evaluate to runtime values, which may or may not be optimized out
// by the compiler. A variety of configurations are demonstrated here which cover most of
// the modern oeprating systems. Try running the Flutter application on different machines
// and see if it matches your expected OS.
//
// Furthermore, in Rust, the last expression in a function is the return value and does
// not have the trailing semicolon. This entire if-else chain forms a single expression.
if cfg!(windows) {
Platform::Windows
} else if cfg!(target_os = "android") {
Platform::AndroidBish
} else if cfg!(target_os = "ios") {
Platform::Ios
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Platform::MacApple
} else if cfg!(target_os = "macos") {
Platform::MacIntel
} else if cfg!(target_family = "wasm") {
Platform::Wasm
} else if cfg!(unix) {
Platform::Unix
} else {
Platform::Unknown
#[derive(Debug, Clone)]
pub struct RaptorPacket(pub Vec<u8>);
impl From<cuttle::TxConfig> for TxConfig {
fn from(value: cuttle::TxConfig) -> Self {
TxConfig {
len: value.len,
mtu: value.mtu,
description: value.description,
filename: value.filename,
}
}
}
// The convention for Rust identifiers is the snake_case,
// and they are automatically converted to camelCase on the Dart side.
pub fn rust_release_mode() -> bool {
cfg!(not(debug_assertions))
pub fn get_tx_config(bytes: Vec<u8>) -> Option<TxConfig> {
if let Ok(archive) = rkyv::check_archived_root::<cuttle::TxConfig>(&bytes) {
<cuttle::ArchivedTxConfig as rkyv::Deserialize<cuttle::TxConfig, rkyv::Infallible>>::deserialize(archive, &mut rkyv::Infallible)
.ok().map(Into::into)
} else {
None
}
}
pub fn drop_tx_config(_txc: TxConfig) {}
pub fn decode_packets(packets: Vec<RaptorPacket>, txconf: TxConfig) -> Option<Vec<u8>> {
let conf = ObjectTransmissionInformation::with_defaults(txconf.len, txconf.mtu);
let mut decoder = Decoder::new(conf);
let mut res = None;
for RaptorPacket(p) in &packets {
res = decoder.decode(EncodingPacket::deserialize(p));
if res.is_some() {
break;
}
}
res
}

View file

@ -2,23 +2,130 @@ use super::*;
// Section: wire functions
#[no_mangle]
pub extern "C" fn wire_platform(port_: i64) {
wire_platform_impl(port_)
pub extern "C" fn wire_get_tx_config(port_: i64, bytes: *mut wire_uint_8_list) {
wire_get_tx_config_impl(port_, bytes)
}
#[no_mangle]
pub extern "C" fn wire_rust_release_mode(port_: i64) {
wire_rust_release_mode_impl(port_)
pub extern "C" fn wire_drop_tx_config(port_: i64, _txc: *mut wire_TxConfig) {
wire_drop_tx_config_impl(port_, _txc)
}
#[no_mangle]
pub extern "C" fn wire_decode_packets(
port_: i64,
packets: *mut wire_list_raptor_packet,
txconf: *mut wire_TxConfig,
) {
wire_decode_packets_impl(port_, packets, txconf)
}
// Section: allocate functions
#[no_mangle]
pub extern "C" fn new_box_autoadd_tx_config_0() -> *mut wire_TxConfig {
support::new_leak_box_ptr(wire_TxConfig::new_with_null_ptr())
}
#[no_mangle]
pub extern "C" fn new_list_raptor_packet_0(len: i32) -> *mut wire_list_raptor_packet {
let wrap = wire_list_raptor_packet {
ptr: support::new_leak_vec_ptr(<wire_RaptorPacket>::new_with_null_ptr(), len),
len,
};
support::new_leak_box_ptr(wrap)
}
#[no_mangle]
pub extern "C" fn new_uint_8_list_0(len: i32) -> *mut wire_uint_8_list {
let ans = wire_uint_8_list {
ptr: support::new_leak_vec_ptr(Default::default(), len),
len,
};
support::new_leak_box_ptr(ans)
}
// Section: related functions
// Section: impl Wire2Api
impl Wire2Api<String> for *mut wire_uint_8_list {
fn wire2api(self) -> String {
let vec: Vec<u8> = self.wire2api();
String::from_utf8_lossy(&vec).into_owned()
}
}
impl Wire2Api<TxConfig> for *mut wire_TxConfig {
fn wire2api(self) -> TxConfig {
let wrap = unsafe { support::box_from_leak_ptr(self) };
Wire2Api::<TxConfig>::wire2api(*wrap).into()
}
}
impl Wire2Api<Vec<RaptorPacket>> for *mut wire_list_raptor_packet {
fn wire2api(self) -> Vec<RaptorPacket> {
let vec = unsafe {
let wrap = support::box_from_leak_ptr(self);
support::vec_from_leak_ptr(wrap.ptr, wrap.len)
};
vec.into_iter().map(Wire2Api::wire2api).collect()
}
}
impl Wire2Api<RaptorPacket> for wire_RaptorPacket {
fn wire2api(self) -> RaptorPacket {
RaptorPacket(self.field0.wire2api())
}
}
impl Wire2Api<TxConfig> for wire_TxConfig {
fn wire2api(self) -> TxConfig {
TxConfig {
len: self.len.wire2api(),
mtu: self.mtu.wire2api(),
description: self.description.wire2api(),
filename: self.filename.wire2api(),
}
}
}
impl Wire2Api<Vec<u8>> for *mut wire_uint_8_list {
fn wire2api(self) -> Vec<u8> {
unsafe {
let wrap = support::box_from_leak_ptr(self);
support::vec_from_leak_ptr(wrap.ptr, wrap.len)
}
}
}
// Section: wire structs
#[repr(C)]
#[derive(Clone)]
pub struct wire_list_raptor_packet {
ptr: *mut wire_RaptorPacket,
len: i32,
}
#[repr(C)]
#[derive(Clone)]
pub struct wire_RaptorPacket {
field0: *mut wire_uint_8_list,
}
#[repr(C)]
#[derive(Clone)]
pub struct wire_TxConfig {
len: u64,
mtu: u16,
description: *mut wire_uint_8_list,
filename: *mut wire_uint_8_list,
}
#[repr(C)]
#[derive(Clone)]
pub struct wire_uint_8_list {
ptr: *mut u8,
len: i32,
}
// Section: impl NewWithNullPtr
pub trait NewWithNullPtr {
@ -31,6 +138,37 @@ impl<T> NewWithNullPtr for *mut T {
}
}
impl NewWithNullPtr for wire_RaptorPacket {
fn new_with_null_ptr() -> Self {
Self {
field0: core::ptr::null_mut(),
}
}
}
impl Default for wire_RaptorPacket {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_TxConfig {
fn new_with_null_ptr() -> Self {
Self {
len: Default::default(),
mtu: Default::default(),
description: core::ptr::null_mut(),
filename: core::ptr::null_mut(),
}
}
}
impl Default for wire_TxConfig {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
// Section: sync execution mode utility
#[no_mangle]

View file

@ -22,24 +22,48 @@ use crate::api::*;
// Section: wire functions
fn wire_platform_impl(port_: MessagePort) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, Platform>(
fn wire_get_tx_config_impl(port_: MessagePort, bytes: impl Wire2Api<Vec<u8>> + UnwindSafe) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, Option<TxConfig>>(
WrapInfo {
debug_name: "platform",
debug_name: "get_tx_config",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || move |task_callback| Ok(platform()),
move || {
let api_bytes = bytes.wire2api();
move |task_callback| Ok(get_tx_config(api_bytes))
},
)
}
fn wire_rust_release_mode_impl(port_: MessagePort) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, bool>(
fn wire_drop_tx_config_impl(port_: MessagePort, _txc: impl Wire2Api<TxConfig> + UnwindSafe) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, ()>(
WrapInfo {
debug_name: "rust_release_mode",
debug_name: "drop_tx_config",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || move |task_callback| Ok(rust_release_mode()),
move || {
let api__txc = _txc.wire2api();
move |task_callback| Ok(drop_tx_config(api__txc))
},
)
}
fn wire_decode_packets_impl(
port_: MessagePort,
packets: impl Wire2Api<Vec<RaptorPacket>> + UnwindSafe,
txconf: impl Wire2Api<TxConfig> + UnwindSafe,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, Option<Vec<u8>>>(
WrapInfo {
debug_name: "decode_packets",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || {
let api_packets = packets.wire2api();
let api_txconf = txconf.wire2api();
move |task_callback| Ok(decode_packets(api_packets, api_txconf))
},
)
}
// Section: wrapper structs
@ -64,25 +88,38 @@ where
(!self.is_null()).then(|| self.wire2api())
}
}
impl Wire2Api<u16> for u16 {
fn wire2api(self) -> u16 {
self
}
}
impl Wire2Api<u64> for u64 {
fn wire2api(self) -> u64 {
self
}
}
impl Wire2Api<u8> for u8 {
fn wire2api(self) -> u8 {
self
}
}
// Section: impl IntoDart
impl support::IntoDart for Platform {
impl support::IntoDart for TxConfig {
fn into_dart(self) -> support::DartAbi {
match self {
Self::Unknown => 0,
Self::AndroidBish => 1,
Self::Ios => 2,
Self::Windows => 3,
Self::Unix => 4,
Self::MacIntel => 5,
Self::MacApple => 6,
Self::Wasm => 7,
}
vec![
self.len.into_into_dart().into_dart(),
self.mtu.into_into_dart().into_dart(),
self.description.into_into_dart().into_dart(),
self.filename.into_dart(),
]
.into_dart()
}
}
impl support::IntoDartExceptPrimitive for Platform {}
impl rust2dart::IntoIntoDart<Platform> for Platform {
impl support::IntoDartExceptPrimitive for TxConfig {}
impl rust2dart::IntoIntoDart<TxConfig> for TxConfig {
fn into_into_dart(self) -> Self {
self
}

View file

@ -33,19 +33,19 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
camerawesome: ^1.4.0
ffi: ^2.0.1
flutter_rust_bridge: ^1.45.0
meta: ^1.8.0
uuid: ^3.0.7
path_provider: ^2.1.0
path: ^1.8.3
mobile_scanner: ^3.4.1
camerawesome: ^1.4.0
google_mlkit_barcode_scanning: ^0.5.0
rxdart: ^0.27.7
image: ^4.0.17
google_mlkit_commons: ^0.2.0
image: ^4.0.17
meta: ^1.8.0
mobile_scanner: ^3.4.1
path: ^1.8.3
path_provider: ^2.1.0
rxdart: ^0.27.7
uuid: ^3.0.7
intl: ^0.18.1
dev_dependencies:
flutter_test:

BIN
mobile_finished.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

BIN
mobile_receiving.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

View file

@ -3,6 +3,8 @@ use std::{
time::{Duration, Instant},
};
use rand::seq::SliceRandom;
use raptorq::{Encoder, ObjectTransmissionInformation};
use rkyv::{Archive, Deserialize, Serialize};
#[cfg(feature = "desktop")]
@ -11,10 +13,11 @@ mod desktop;
mod qr_utils;
#[cfg(feature = "desktop")]
pub use qr_utils::{get_content, mk_qr_bytes, stream_bytes};
pub use qr_utils::{get_content, mk_qr_bytes};
pub type CuttleSender = std::sync::mpsc::SyncSender<Vec<u8>>;
pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
pub const STREAMING_MTU: u16 = 1200;
/// The application state
#[derive(Debug)]
@ -70,9 +73,48 @@ pub enum StreamStatus {
}
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[repr(C)]
#[archive(check_bytes)]
pub struct TxConfig {
pub len: u64,
pub mtu: u16,
pub description: String,
pub filename: Option<String>,
}
pub fn stream_bytes(
bytes: Vec<u8>,
tx: CuttleSender,
desc: &str,
filename: Option<&str>,
) -> Vec<u8> {
let len = bytes.len() as u64;
let rng = &mut rand::thread_rng();
let config = ObjectTransmissionInformation::with_defaults(len, STREAMING_MTU);
let encoder = Encoder::new(&bytes, config);
let mut packets = encoder
.get_encoded_packets(10)
.iter()
.map(|p| p.serialize())
.collect::<Vec<_>>();
packets.shuffle(rng);
std::thread::spawn(move || {
for packet in packets.iter().cycle() {
tx.send(packet.clone()).unwrap_or_default();
}
});
let txconfig = TxConfig {
len,
mtu: STREAMING_MTU,
description: desc.to_string(),
filename: filename.map(|f| f.to_string()),
};
rkyv::to_bytes::<_, 256>(&txconfig)
.expect("tried to serialize the txconfig")
.to_vec()
}

View file

@ -30,16 +30,17 @@ fn main() -> Result<(), eframe::Error> {
let cli = Cli::parse();
let description = if let Some(ref file) = cli.file {
let (description, filename) = if let Some(ref file) = cli.file {
let text = cli.text().join(" ");
let sep = if text.is_empty() { "" } else { ": " };
let file = std::path::Path::new(&file)
.file_name()
.unwrap_or_default()
.to_string_lossy();
format!("{file}{sep}{text}")
.to_string_lossy()
.to_string();
(format!("{file}{sep}{text}"), Some(file))
} else {
"text message".to_string()
("text message".to_string(), None)
};
let bytes = if let Some(ref file) = cli.file {
@ -48,7 +49,7 @@ fn main() -> Result<(), eframe::Error> {
cli.text().join(" ").bytes().collect()
};
let content = get_content(bytes, &description);
let content = get_content(bytes, &description, filename.as_deref());
let flasher = Flasher::new(description, content, cli.fps);

View file

@ -1,46 +1,11 @@
use fast_qr::convert::image::ImageBuilder;
use rand::seq::SliceRandom;
use raptorq::{Encoder, ObjectTransmissionInformation};
use crate::{Content, CuttleSender, StreamedContent, TxConfig};
pub const STREAMING_MTU: u16 = 2326;
pub fn stream_bytes(bytes: Vec<u8>, tx: CuttleSender, desc: String) -> Vec<u8> {
let len = bytes.len() as u64;
let txconfig = TxConfig {
len,
mtu: STREAMING_MTU,
description: desc.to_string(),
};
let txconfig = rkyv::to_bytes::<_, 256>(&txconfig)
.expect("tried to serialize the txconfig")
.to_vec();
std::thread::spawn(move || {
let rng = &mut rand::thread_rng();
let config = ObjectTransmissionInformation::with_defaults(len, STREAMING_MTU);
let encoder = Encoder::new(&bytes, config);
let mut packets = encoder
.get_encoded_packets(10)
.iter()
.map(|p| p.serialize())
.collect::<Vec<_>>();
packets.shuffle(rng);
for packet in packets.iter().cycle() {
tx.send(packet.clone()).unwrap_or_default();
}
});
txconfig
}
use crate::{stream_bytes, Content, StreamedContent};
/// Makes a PNG of a QR code for the given bytes, returns the bytes of the PNG.
pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec<u8> {
let qr = fast_qr::QRBuilder::new(bytes)
.ecl(fast_qr::ECL::M)
.ecl(fast_qr::ECL::L)
.build()
.unwrap();
@ -53,13 +18,13 @@ pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec<u8> {
/// Turns bytes and a description into either a single QR code, or a stream of
/// them, depending on the size of the input.
pub fn get_content(bytes: Vec<u8>, desc: &str) -> Content {
pub fn get_content(bytes: Vec<u8>, desc: &str, filename: Option<&str>) -> Content {
if bytes.len() < 2000 && fast_qr::QRBuilder::new(bytes.clone()).build().is_ok() {
let bytes = bytes.leak();
Content::Static(bytes)
} else {
let (tx, rx) = std::sync::mpsc::sync_channel(2);
let txconfig = stream_bytes(bytes, tx, desc.to_string()).leak();
let txconfig = stream_bytes(bytes, tx, desc, filename).leak();
let stream = StreamedContent::new(txconfig, rx);
Content::Streamed(stream)
}