creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/services/analyze/font.dart (6950B)
1import 'dart:io';
2import 'dart:math';
3import 'dart:convert';
4import 'package:flutter/foundation.dart';
5import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
6import 'package:onnxruntime/onnxruntime.dart';
7import 'package:image/image.dart' as img;
8
9class FontIdentifierService {
10 OrtSession? _session;
11 Map<String, List<double>>? _fontDb;
12 final _textRecognizer = TextRecognizer(script: TextRecognitionScript.latin);
13
14 static const int IMG_SIZE = 64;
15
16 Future<void> initialize({
17 required String modelPath,
18 required String jsonPath,
19 }) async {
20 if (_session != null) return;
21
22 try {
23 debugPrint("Initializing Font Service...");
24 OrtEnv.instance.init();
25
26 // 1. Load ONNX
27 final sessionOptions = OrtSessionOptions();
28 _session = OrtSession.fromFile(File(modelPath), sessionOptions);
29 sessionOptions.release();
30
31 // 2. Load JSON DB
32 final jsonStr = await File(jsonPath).readAsString();
33 final Map<String, dynamic> rawDb = json.decode(jsonStr);
34 _fontDb = {};
35 rawDb.forEach((key, value) {
36 _fontDb![key] = List<double>.from(value);
37 });
38 } catch (e) {
39 debugPrint("Font Service Init Error: $e");
40 }
41 }
42
43 Future<Map<String, dynamic>> analyze(
44 String imagePath, {
45 String? modelPath,
46 String? jsonPath,
47 }) async {
48 if (modelPath == null || jsonPath == null) {
49 return {'success': false, 'scores': {}, 'error': 'Paths missing'};
50 }
51 await initialize(modelPath: modelPath, jsonPath: jsonPath);
52 if (_session == null || _fontDb == null) {
53 return {'success': false, 'scores': {}, 'error': 'Init failed'};
54 }
55
56 try {
57 // 1. OCR Detection (Native Platform Call)
58 final inputImage = InputImage.fromFilePath(imagePath);
59 final recognizedText = await _textRecognizer.processImage(inputImage);
60
61 if (recognizedText.blocks.isEmpty) {
62 return {
63 'success': true,
64 'scores': {'No Text Detected': 1.0},
65 'error': null,
66 };
67 }
68
69 // 2. Load Image for Processing
70 final bytes = await File(imagePath).readAsBytes();
71 final fullImage = img.decodeImage(bytes);
72 if (fullImage == null) {
73 return {'success': false, 'scores': {}, 'error': 'Image decode failed'};
74 }
75
76 // Store counts to find dominant font
77 Map<String, double> fontCounts = {};
78 int totalBlocks = 0;
79
80 // 3. Iterate Text Blocks
81 for (TextBlock block in recognizedText.blocks) {
82 for (TextLine line in block.lines) {
83 // Crop
84 final rect = line.boundingBox;
85
86 // Safety check for bounds
87 int x = max(0, rect.left.toInt());
88 int y = max(0, rect.top.toInt());
89 int w = min(rect.width.toInt(), fullImage.width - x);
90 int h = min(rect.height.toInt(), fullImage.height - y);
91
92 if (w <= 0 || h <= 0) continue;
93
94 final crop = img.copyCrop(fullImage, x: x, y: y, width: w, height: h);
95
96 // Preprocess & Inference
97 final floatInput = _preprocessImage(crop);
98 final embedding = _runInference(floatInput);
99 final match = _findBestFont(embedding);
100
101 // Aggregate Scores
102 final fontName = match.key;
103 final conf = match.value;
104
105 if (fontCounts.containsKey(fontName)) {
106 fontCounts[fontName] = fontCounts[fontName]! + conf;
107 } else {
108 fontCounts[fontName] = conf;
109 }
110 totalBlocks++;
111 }
112 }
113
114 // Normalize scores
115 fontCounts.updateAll((key, value) => (value / totalBlocks) * 100);
116
117 // Sort
118 var sorted =
119 fontCounts.entries.toList()
120 ..sort((a, b) => b.value.compareTo(a.value));
121 Map<String, dynamic> finalScores = {};
122 for (var entry in sorted) {
123 finalScores[entry.key] = entry.value;
124 }
125
126 return {'success': true, 'scores': finalScores, 'error': null};
127 } catch (e) {
128 debugPrint("Font Analysis Failed: $e");
129 return {'success': false, 'scores': {}, 'error': e.toString()};
130 }
131 }
132
133 /// Convert crop to 64x64 Grayscale Thresholded Float32List
134 Float32List _preprocessImage(img.Image crop) {
135 // Resize to 64x64
136 final resized = img.copyResize(crop, width: IMG_SIZE, height: IMG_SIZE);
137 final pixels = Float32List(IMG_SIZE * IMG_SIZE);
138
139 // 1. Calculate Mean Brightness (Adaptive Threshold)
140 double totalLum = 0;
141 for (final pixel in resized) {
142 totalLum += pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114;
143 }
144 double mean = totalLum / (IMG_SIZE * IMG_SIZE);
145
146 // 2. Binarize (Inverted: Text=1.0, BG=0.0)
147 int idx = 0;
148 for (int y = 0; y < IMG_SIZE; y++) {
149 for (int x = 0; x < IMG_SIZE; x++) {
150 final p = resized.getPixel(x, y);
151 final lum = p.r * 0.299 + p.g * 0.587 + p.b * 0.114;
152 // If pixel is darker than mean, it's text (1.0)
153 pixels[idx++] = (lum < mean) ? 1.0 : 0.0;
154 }
155 }
156 return pixels;
157 }
158
159 List<double> _runInference(Float32List imageFloats) {
160 OrtValueTensor? imgTensor;
161 OrtValueTensor? charTensor;
162 OrtRunOptions? runOptions;
163 List<OrtValue?>? outputs;
164
165 try {
166 // Input 1: Image [1, 64, 64, 1]
167 imgTensor = OrtValueTensor.createTensorWithDataList(imageFloats, [
168 1,
169 64,
170 64,
171 1,
172 ]);
173
174 // Input 2: Dummy Char [1, 26, 1] (Required by FANNet architecture)
175 final dummyFloats = Float32List(26 * 1);
176 charTensor = OrtValueTensor.createTensorWithDataList(dummyFloats, [
177 1,
178 26,
179 1,
180 ]);
181
182 runOptions = OrtRunOptions();
183
184 // Run
185 outputs = _session!.run(runOptions, {
186 'image_input': imgTensor,
187 'char_input': charTensor,
188 });
189
190 // Output Flattening
191 final rawOutput = outputs[0]?.value;
192 final List<double> flatOutput = [];
193
194 void flatten(dynamic d) {
195 if (d is num) {
196 flatOutput.add(d.toDouble());
197 } else if (d is List) {
198 for (var i in d) {
199 flatten(i);
200 }
201 }
202 }
203
204 flatten(rawOutput);
205
206 return flatOutput;
207 } finally {
208 imgTensor?.release();
209 charTensor?.release();
210 runOptions?.release();
211 outputs?.forEach((e) => e?.release());
212 }
213 }
214
215 MapEntry<String, double> _findBestFont(List<double> embedding) {
216 String bestFont = "Unknown";
217 double minDist = double.infinity;
218
219 _fontDb!.forEach((fontName, fontFeat) {
220 double sum = 0;
221 for (int i = 0; i < min(embedding.length, fontFeat.length); i++) {
222 double diff = embedding[i] - fontFeat[i];
223 sum += diff * diff;
224 }
225 double dist = sqrt(sum);
226
227 if (dist < minDist) {
228 minDist = dist;
229 bestFont = fontName;
230 }
231 });
232
233 return MapEntry(bestFont, 1 / (1 + minDist));
234 }
235
236 void dispose() {
237 _session?.release();
238 _textRecognizer.close();
239 }
240}