From d100221876db657365be195533baacba8a4dd95d Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Sat, 6 Dec 2025 21:08:52 +0100 Subject: [PATCH] generate LVGL 9 binary Signed-off-by: Peter Siegmund --- server/cinema/bin/server.dart | 2 +- server/cinema/lib/common/image_resizer.dart | 45 ++++++++++++ .../lib/common/lvgl_image_converter.dart | 68 +++++++++++++++++++ .../{middlewares => middleware}/cors.dart | 0 .../data/repositories/tmdb_image_loader.dart | 16 ++++- 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 server/cinema/lib/common/image_resizer.dart create mode 100644 server/cinema/lib/common/lvgl_image_converter.dart rename server/cinema/lib/feature/{middlewares => middleware}/cors.dart (100%) diff --git a/server/cinema/bin/server.dart b/server/cinema/bin/server.dart index 84d5ba1..02e197e 100644 --- a/server/cinema/bin/server.dart +++ b/server/cinema/bin/server.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:cinema/common/env_not_found_exception.dart'; -import 'package:cinema/feature/middlewares/cors.dart'; +import 'package:cinema/feature/middleware/cors.dart'; import 'package:cinema/feature/poster/data/repositories/image_loader.dart'; import 'package:cinema/feature/poster/data/services/poster.service.dart'; import 'package:cinema/feature/root/data/service/root.service.dart'; diff --git a/server/cinema/lib/common/image_resizer.dart b/server/cinema/lib/common/image_resizer.dart new file mode 100644 index 0000000..dac8d4d --- /dev/null +++ b/server/cinema/lib/common/image_resizer.dart @@ -0,0 +1,45 @@ +import 'package:image/image.dart' as img; +import 'package:injectable/injectable.dart'; + +@singleton +class ImageResizer { + static const int maxWidth = 480; + static const int maxHeight = 320; + + img.Image resizeToFit(img.Image image) { + final w = image.width; + final h = image.height; + + if (w <= maxWidth && h <= maxHeight) { + return image; + } + + final aspectRatio = w / h; + int newWidth; + int newHeight; + + if (w > h) { + newWidth = maxWidth; + newHeight = (maxWidth / aspectRatio).round(); + if (newHeight > maxHeight) { + newHeight = maxHeight; + newWidth = (maxHeight * aspectRatio).round(); + } + } else { + newHeight = maxHeight; + newWidth = (maxHeight * aspectRatio).round(); + if (newWidth > maxWidth) { + newWidth = maxWidth; + newHeight = (maxWidth / aspectRatio).round(); + } + } + + return img.copyResize( + image, + width: newWidth, + height: newHeight, + interpolation: img.Interpolation.linear, + ); + } +} + diff --git a/server/cinema/lib/common/lvgl_image_converter.dart b/server/cinema/lib/common/lvgl_image_converter.dart new file mode 100644 index 0000000..bf1a078 --- /dev/null +++ b/server/cinema/lib/common/lvgl_image_converter.dart @@ -0,0 +1,68 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:image/image.dart' as img; +import 'package:injectable/injectable.dart'; + +import 'image_resizer.dart'; + +@singleton +class LvglImageConverter { + final ImageResizer _imageResizer; + + LvglImageConverter(this._imageResizer); + static const int _magic = 0x19; + static const int _colorFormatRgb565 = 0x12; + static const int _flags = 0x00; + + int _toRgb565(int r, int g, int b) { + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + + Future convertPngToBin(String imagePath, String binPath) async { + final imageFile = File(imagePath); + if (!await imageFile.exists()) { + throw FileSystemException('Image file not found', imagePath); + } + + final bytes = await imageFile.readAsBytes(); + var image = img.decodeImage(bytes); + if (image == null) { + throw FormatException('Failed to decode image: $imagePath'); + } + + image = _imageResizer.resizeToFit(image); + + final w = image.width; + final h = image.height; + final stride = (w * 16 + 7) ~/ 8; + + final header = ByteData(12); + header.setUint8(0, _magic); + header.setUint8(1, _colorFormatRgb565); + header.setUint16(2, _flags, Endian.little); + header.setUint16(4, w, Endian.little); + header.setUint16(6, h, Endian.little); + header.setUint16(8, stride, Endian.little); + header.setUint16(10, 0, Endian.little); // reserved + + final body = ByteData(w * h * 2); + var offset = 0; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + final pixel = image.getPixel(x, y); + final rgb565 = _toRgb565(pixel.r.toInt(), pixel.g.toInt(), pixel.b.toInt()); + body.setUint16(offset, rgb565, Endian.little); + offset += 2; + } + } + + final binFile = File(binPath); + await binFile.parent.create(recursive: true); + await binFile.writeAsBytes([ + ...header.buffer.asUint8List(), + ...body.buffer.asUint8List(), + ]); + } +} + diff --git a/server/cinema/lib/feature/middlewares/cors.dart b/server/cinema/lib/feature/middleware/cors.dart similarity index 100% rename from server/cinema/lib/feature/middlewares/cors.dart rename to server/cinema/lib/feature/middleware/cors.dart diff --git a/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart b/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart index c11c17f..7a435f9 100644 --- a/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart +++ b/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cinema/common/dio_module.dart'; import 'package:cinema/common/domain/serializers.dart'; +import 'package:cinema/common/lvgl_image_converter.dart'; import 'package:cinema/feature/poster/data/repositories/image_loader.dart'; import 'package:cinema/feature/poster/domain/movie.dart'; import 'package:cinema/feature/poster/domain/tmdb_trending_response.dart'; @@ -12,9 +13,14 @@ import 'package:injectable/injectable.dart'; class TmDBImageLoader implements ImageLoader { final Dio api; final Dio images; + final LvglImageConverter lvglConverter; final String imageBaseUrl = 'https://image.tmdb.org/t/p/w500'; - TmDBImageLoader(@Named(dioAPI) this.api, @Named(dioIMAGES) this.images); + TmDBImageLoader( + @Named(dioAPI) this.api, + @Named(dioIMAGES) this.images, + this.lvglConverter, + ); @override Future> getPosterURIs({String? language = 'de'}) async { @@ -41,8 +47,8 @@ class TmDBImageLoader implements ImageLoader { @override Future downloadImages(Movie movie) async { - await _downloadImage(movie.poster, "cache/movie/${movie.id}/poster.png"); - await _downloadImage(movie.backdrop, "cache/movie/${movie.id}/backdrop.png"); + await _downloadImage(movie.poster, "cache/movie/${movie.id}/poster.jpeg"); + await _downloadImage(movie.backdrop, "cache/movie/${movie.id}/backdrop.jpeg"); return true; } @@ -55,6 +61,10 @@ class TmDBImageLoader implements ImageLoader { await file.parent.create(recursive: true); final response = await images.get(url, options: Options(responseType: ResponseType.bytes)); await file.writeAsBytes(response.data); + + final binFilename = filename.replaceAll('.jpeg', '.bin'); + await lvglConverter.convertPngToBin(filename, binFilename); + return true; } }