first commit

This commit is contained in:
2026-01-20 16:34:54 +07:00
commit 2ac772b6de
4186 changed files with 123824 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class ImageCleanupService {
/// Hapus SEMUA file sementara
Future<void> cleanAllTemp(String tempDir) async {
final dir = Directory(tempDir);
if (!await dir.exists()) return;
for (final file in dir.listSync()) {
if (file is File) {
await file.delete();
}
}
}
/// Simpan maksimal N foto terakhir
Future<void> keepLastN({required String tempDir, int maxFiles = 10}) async {
final dir = Directory(tempDir);
if (!await dir.exists()) return;
final files =
dir
.listSync()
.whereType<File>()
.where((f) => f.path.contains('final_'))
.toList()
..sort(
(a, b) => a.statSync().modified.compareTo(b.statSync().modified),
);
if (files.length <= maxFiles) return;
for (final file in files.take(files.length - maxFiles)) {
await file.delete();
}
}
/// Hapus file tertentu (aman)
Future<void> deleteIfExists(String? path) async {
if (path == null) return;
final file = File(path);
if (await file.exists()) {
await file.delete();
}
}
}
Future<void> cleanTempOnStart() async {
final dir = await getTemporaryDirectory();
final cleanup = ImageCleanupService();
await cleanup.cleanAllTemp(dir.path);
}

View File

@@ -0,0 +1,83 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;
/// PUBLIC API
class ImageCompositor {
Future<String> composeFinalA5({
required String imagePath,
required Uint8List twibbonBytes,
required String tempDir,
required bool isFrontCamera,
}) {
return compute(_finalIsolate, {
'imagePath': imagePath,
'twibbonBytes': twibbonBytes,
'tempDir': tempDir,
'isFrontCamera': isFrontCamera,
});
}
}
Future<String> _finalIsolate(Map<String, dynamic> args) async {
const int a5Width = 1748;
const int a5Height = 2480;
final String imagePath = args['imagePath'];
final Uint8List twibbonBytes = args['twibbonBytes'];
final String tempDir = args['tempDir'];
final bool isFrontCamera = args['isFrontCamera'];
// 1⃣ Decode foto kamera
final raw = img.decodeImage(await File(imagePath).readAsBytes());
if (raw == null) throw Exception('Invalid photo');
// 2⃣ Samakan dengan preview kamera depan
if (isFrontCamera) {
img.flipHorizontal(raw);
}
// 3⃣ Crop ke rasio A5
final cropped = _cropToA5(raw);
// 4⃣ Resize ke resolusi CETAK A5
final photoA5 = img.copyResize(
cropped,
width: a5Width,
height: a5Height,
interpolation: img.Interpolation.average,
);
// 5⃣ Decode & resize twibbon SEKALI
final twibbon = img.decodeImage(twibbonBytes)!;
final twibbonA5 = img.copyResize(twibbon, width: a5Width, height: a5Height);
// 6⃣ Composite
img.compositeImage(photoA5, twibbonA5);
// 7⃣ Encode JPEG (print-ready)
final jpgBytes = img.encodeJpg(photoA5, quality: 90);
// 8⃣ Simpan FINAL FILE
final file = File(
'$tempDir/final_${DateTime.now().millisecondsSinceEpoch}.jpg',
);
await file.writeAsBytes(jpgBytes);
return file.path;
}
img.Image _cropToA5(img.Image src) {
const double a5Ratio = 148 / 210;
final srcRatio = src.width / src.height;
if (srcRatio > a5Ratio) {
final newWidth = (src.height * a5Ratio).round();
final x = ((src.width - newWidth) / 2).round();
return img.copyCrop(src, x: x, y: 0, width: newWidth, height: src.height);
} else {
final newHeight = (src.width / a5Ratio).round();
final y = ((src.height - newHeight) / 2).round();
return img.copyCrop(src, x: 0, y: y, width: src.width, height: newHeight);
}
}