todo-app/lib/images/image_processing.dart
Krzysztof kuhy Rudnicki 0798f7b1d0 Attach images to notes, stored on dufs and kept offline
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 14:11:15 +02:00

67 lines
2.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// Shrinks a picked or pasted image to what a note needs, before it is stored
/// or uploaded.
///
/// Phone photos are 3–8 MB with GPS coordinates in their EXIF. Every image is
/// re-encoded here: the recorded rotation is applied to the pixels first (so
/// dropping EXIF does not leave a portrait photo lying on its side), the long
/// side is capped at [kMaxImageSide], and the result is a JPEG with no
/// metadata at all.
///
/// Pure Dart (`package:image`), so the Android app and the desktop wrapper
/// run the same code; the web page never decodes images, it hands the raw
/// bytes to the wrapper.
library;
import 'dart:isolate';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:image/image.dart' as img;
/// Longest side, in pixels, of a stored image.
const kMaxImageSide = 2560;
/// JPEG quality of a stored image.
const kJpegQuality = 85;
/// Re-encodes [bytes] as a metadata-free JPEG no larger than [kMaxImageSide]
/// on its long side, or returns null when [bytes] is not a decodable image.
Uint8List? prepareImage(Uint8List bytes) {
final decoded = _decode(bytes);
if (decoded == null) return null;
var image = img.bakeOrientation(decoded);
final longSide = math.max(image.width, image.height);
if (longSide > kMaxImageSide) {
image = image.width >= image.height
? img.copyResize(image, width: kMaxImageSide)
: img.copyResize(image, height: kMaxImageSide);
}
if (image.hasAlpha) image = _flattenOntoWhite(image);
image.exif = img.ExifData();
return img.encodeJpg(image, quality: kJpegQuality);
}
/// [prepareImage] off the calling isolate: decoding a 12 MP photo takes long
/// enough to drop frames if it runs on the UI isolate.
Future<Uint8List?> prepareImageInBackground(Uint8List bytes) =>
Isolate.run(() => prepareImage(bytes));
/// `decodeImage` returns null for most non-images but *throws* (a
/// RangeError from its PSD sniffer, seen on a 3-byte input) for others; both
/// mean "not an image" here.
img.Image? _decode(Uint8List bytes) {
try {
return img.decodeImage(bytes);
} on Object {
return null;
}
}
/// JPEG has no alpha: without this a transparent screenshot's clear pixels
/// come out as whatever colour their RGB happened to hold, usually black.
img.Image _flattenOntoWhite(img.Image image) {
final background = img.Image(width: image.width, height: image.height)
..clear(img.ColorRgb8(255, 255, 255));
return img.compositeImage(background, image);
}