main
dart 117 lines 3.25 KB
Raw
1 import 'dart:io';
2 import 'dart:typed_data';
3 import 'dart:ui' as ui;
4
5 enum ImageCompressionLevel { low, medium, high }
6
7 class _CompressionConfig {
8 const _CompressionConfig(this.maxPayloadBytes, this.dimensions);
9
10 final int maxPayloadBytes;
11 final List<int> dimensions;
12 }
13
14 /// Resize/compress images so vision-model requests stay under payload limits.
15 class ImageUploadUtils {
16 ImageUploadUtils._();
17
18 /// Kept for callers that don't care about the configured level.
19 static const maxPayloadBytes = 750000;
20 static const maxBase64Chars = 1000000;
21
22 static const Map<ImageCompressionLevel, _CompressionConfig> _configs = {
23 ImageCompressionLevel.low: _CompressionConfig(1500000, [
24 1920,
25 1536,
26 1280,
27 1024,
28 ]),
29 ImageCompressionLevel.medium: _CompressionConfig(750000, [
30 1536,
31 1280,
32 1024,
33 768,
34 512,
35 ]),
36 ImageCompressionLevel.high: _CompressionConfig(400000, [
37 1024,
38 768,
39 512,
40 384,
41 256,
42 ]),
43 };
44
45 static Future<Uint8List> prepareImageBytes(
46 File file, {
47 bool enabled = true,
48 ImageCompressionLevel level = ImageCompressionLevel.medium,
49 }) async {
50 var bytes = await file.readAsBytes();
51 if (!enabled) return bytes;
52
53 final config = _configs[level]!;
54 if (bytes.length <= config.maxPayloadBytes) return bytes;
55
56 for (final dimension in config.dimensions) {
57 final resized = await _resizeToPng(bytes, dimension);
58 if (resized.length <= config.maxPayloadBytes) return resized;
59 bytes = resized;
60 }
61 return bytes;
62 }
63
64 static Future<File> prepareImageFile(
65 File source, {
66 bool enabled = true,
67 ImageCompressionLevel level = ImageCompressionLevel.medium,
68 }) async {
69 final bytes = await prepareImageBytes(
70 source,
71 enabled: enabled,
72 level: level,
73 );
74 final original = source.path.split(Platform.pathSeparator).last;
75 final dot = original.lastIndexOf('.');
76 final base = dot > 0 ? original.substring(0, dot) : original;
77 final outPath =
78 '${source.parent.path}${Platform.pathSeparator}upload_${DateTime.now().millisecondsSinceEpoch}_$base.png';
79 final out = File(outPath);
80 await out.writeAsBytes(bytes);
81 return out;
82 }
83
84 static Future<Uint8List> _resizeToPng(
85 Uint8List bytes,
86 int maxDimension,
87 ) async {
88 final decodeCodec = await ui.instantiateImageCodec(bytes);
89 final decodeFrame = await decodeCodec.getNextFrame();
90 final origWidth = decodeFrame.image.width;
91 final origHeight = decodeFrame.image.height;
92 decodeFrame.image.dispose();
93 int targetWidth, targetHeight;
94 if (origWidth >= origHeight) {
95 targetWidth = maxDimension;
96 targetHeight = (origHeight * maxDimension / origWidth).round().clamp(
97 1,
98 maxDimension,
99 );
100 } else {
101 targetHeight = maxDimension;
102 targetWidth = (origWidth * maxDimension / origHeight).round().clamp(
103 1,
104 maxDimension,
105 );
106 }
107 final codec = await ui.instantiateImageCodec(
108 bytes,
109 targetWidth: targetWidth,
110 targetHeight: targetHeight,
111 );
112 final frame = await codec.getNextFrame();
113 final data = await frame.image.toByteData(format: ui.ImageByteFormat.png);
114 frame.image.dispose();
115 return data?.buffer.asUint8List() ?? bytes;
116 }
117 }