import 'dart:io'; import 'dart:typed_data'; import 'package:args/args.dart'; import 'package:image/image.dart'; import 'package:logging/logging.dart'; import 'models/screenshot_args.dart'; import 'models/screenshot_device.dart'; import 'models/screenshot_layout.dart'; import 'utils/parse_configs.dart'; final _configs = parseScreenshotConfigs(); Future main(List args) async { final parser = ArgParser() ..addFlag('verbose', abbr: 'v', help: 'Verbose output') ..addOption('platform', abbr: 'p', help: 'Device platform', mandatory: true) ..addOption('device', abbr: 'd', help: 'device_snake_name', mandatory: true) ..addOption('layout', abbr: 'l', help: 'Device layout', mandatory: true); final ArgResults argResults = parser.parse(args); if (argResults['verbose'] as bool) { Logger.root.level = Level.ALL; } else { Logger.root.level = Level.INFO; } final platform = argResults["platform"] as String; final device = argResults["device"] as String; final layout = ScreenshotLayout.values.firstWhere((e) => e.name == argResults["layout"] as String); Directory('screenshots/generated/raw/$platform/$device').listSync().forEach((filePath) async { final screenshotName = filePath.path.split('/').last.replaceAll('.png', ''); final screenshotBytes = File(filePath.path).readAsBytesSync(); final screenshot = decodePng(Uint8List.fromList(screenshotBytes))!; final screenshotArgs = ScreenshotArgs.fromRawName( name: screenshotName, deviceName: device, platformFolder: platform, ); final file = await File(screenshotArgs.toPath(layout.name)).create(recursive: true); file.writeAsBytesSync( encodePng( screenshot.convertToStoreScreenshot( args: screenshotArgs, layout: layout, ), ), ); }); return 0; } extension ScreenshotImage on Image { Image convertToStoreScreenshot({ required ScreenshotArgs args, required ScreenshotLayout layout, }) { if (_configs[args.nameWithTheme] == null) { return this; } return _addSystemOverlay( screenshotDevices[args.deviceName]!, isDark: args.isDark, ) ._addDeviceFrame( screenshotDevices[args.deviceName]!, args.backgroundColor, ) ._applyLayout( layout, _configs[args.nameWithTheme]!.title, _configs[args.nameWithTheme]!.subtitle, isDark: args.isDark, ); } Image _addSystemOverlay(ScreenshotDevice device, {required bool isDark}) { final path = isDark ? device.systemOverlayPathDark : device.systemOverlayPathLight; final statusBar = copyResize( decodePng(File(path).readAsBytesSync())!, width: width, ); return compositeImage(this, statusBar); } Image _addDeviceFrame(ScreenshotDevice device, String color) { final backgroundColor = ColorRgba8( int.parse(color.substring(2, 4), radix: 16), int.parse(color.substring(4, 6), radix: 16), int.parse(color.substring(6, 8), radix: 16), int.parse(color.substring(0, 2), radix: 16), ); final screenshotRounded = copyCrop( this, x: 0, y: 0, width: width, height: height, ); final frame = decodePng(File(device.deviceFramePath).readAsBytesSync())!; final expandedScreenshot = copyExpandCanvas( copyExpandCanvas( screenshotRounded, newWidth: screenshotRounded.width + device.screenshotFrameOffset.dx, newHeight: screenshotRounded.height + device.screenshotFrameOffset.dy, position: ExpandCanvasPosition.bottomRight, backgroundColor: backgroundColor, ), newWidth: frame.width, newHeight: frame.height, position: ExpandCanvasPosition.topLeft, backgroundColor: backgroundColor, ); return compositeImage(expandedScreenshot, frame); } Image _applyLayout(ScreenshotLayout layout, String title, String subtitle, {required bool isDark}) { final textImage = _drawTitles(layout, title, subtitle, isDark: isDark); final maxFrameHeight = layout.size.height - (layout.contentPadding.top + textImage.height + 84 + layout.contentPadding.bottom); int maxFrameWidth = layout.size.width - (layout.contentPadding.left + layout.contentPadding.right); if (maxFrameWidth * height / width > maxFrameHeight) { maxFrameWidth = maxFrameHeight * width ~/ height; } final scaledScreenshot = copyResize(this, width: maxFrameWidth); final draft = copyExpandCanvas( copyExpandCanvas( scaledScreenshot, newWidth: scaledScreenshot.width + (layout.size.width - scaledScreenshot.width) ~/ 2, newHeight: scaledScreenshot.height + layout.contentPadding.bottom, position: ExpandCanvasPosition.topLeft, backgroundColor: getPixel(0, 0), ), newWidth: layout.size.width, newHeight: layout.size.height, position: ExpandCanvasPosition.bottomRight, backgroundColor: getPixel(0, 0), ); return compositeImage( draft, textImage, dstX: layout.contentPadding.left, dstY: layout.contentPadding.top, ); } Image _drawTitles(ScreenshotLayout layout, String title, String subtitle, {required bool isDark}) { final titleFont = BitmapFont.fromZip(File(isDark ? layout.titleFontDarkPath : layout.titleFontPath).readAsBytesSync()); final subtitleFont = BitmapFont.fromZip(File(isDark ? layout.subtitleFontDarkPath : layout.subtitleFontPath).readAsBytesSync()); final textImage = fill( Image( height: titleFont.lineHeight + 36 + subtitleFont.lineHeight * 2, width: layout.size.width - (layout.contentPadding.left + layout.contentPadding.right), ), color: getPixel(0, 0), ); drawString( textImage, title, font: titleFont, y: 0, ); int subtitleDy = titleFont.lineHeight + 36; subtitle.split('\n').forEach((line) { drawString( textImage, line, font: subtitleFont, y: subtitleDy, ); subtitleDy += subtitleFont.lineHeight; }); return textImage; } }