diff --git a/example/assets/img/controllers.jpg b/example/assets/img/controllers.jpg new file mode 100644 index 0000000..1e2f495 Binary files /dev/null and b/example/assets/img/controllers.jpg differ diff --git a/example/warden.yaml b/example/warden.yaml index 23d405b..23fac83 100644 --- a/example/warden.yaml +++ b/example/warden.yaml @@ -21,6 +21,8 @@ assets: source: example/assets directories: - img + compress: + quality: 20 tasks: frontend: diff --git a/lib/asset_mover.dart b/lib/asset_mover.dart index c239ba6..3134c2e 100644 --- a/lib/asset_mover.dart +++ b/lib/asset_mover.dart @@ -3,6 +3,7 @@ import "package:ansi_styles/ansi_styles.dart"; import "package:path/path.dart" as p; import "package:warden/assets.dart"; import "package:warden/destination.dart"; +import "package:warden/file_compressor.dart"; /// The `AssetMover` class is responsible for copying third-party frontend assets /// (like JavaScript or CSS files) from a `node_modules` source directory to a public @@ -16,14 +17,20 @@ import "package:warden/destination.dart"; class AssetMover { final Destination destination; final Asset assets; + final FileCompressor fileCompressor; late Directory nodeModules; late Directory outputDir; + bool shouldCompress = false; AssetMover({ required this.destination, required this.assets, + required this.fileCompressor, }) { outputDir = Directory(destination.destination); + if (assets.compress["use"] != null) { + shouldCompress = assets.compress["use"]; + } } /// Copies each file defined in `dependencies.files` from the `node_modules` directory @@ -47,7 +54,7 @@ class AssetMover { _move(nonJSFiles, dependencySrc); } - void moveAssets() { + moveAssets() async { for (final dirName in assets.directories) { final sourceDir = Directory(p.join(assets.source, dirName)); final destDir = Directory(p.join(outputDir.path, dirName)); @@ -63,8 +70,17 @@ class AssetMover { final targetFile = File(p.join(destDir.path, relative)); targetFile.createSync(recursive: true); targetFile.writeAsBytesSync(entity.readAsBytesSync()); - print( + if (shouldCompress) { + await _compress(targetFile.path); + print( + "${AnsiStyles.cyan("✔ copied asset:\n")}" + "${AnsiStyles.magenta("\t ◉ [${entity.path} ${AnsiStyles.cyanBright.bold("${_getSize(entity.path)}kb")}] ->\n")}" + "${AnsiStyles.magenta("\t ◉ [${targetFile.path} ${AnsiStyles.cyanBright.bold("${_getSize(targetFile.path)}kb")}]\n")}" + ); + } else { + print( "${AnsiStyles.cyan("✔ copied asset: ")}${AnsiStyles.magenta("[${entity.path} -> ${targetFile.path}]")}"); + } } } } @@ -88,4 +104,15 @@ class AssetMover { AnsiStyles.green("✔ moved [${source.path} -> ${destination.path}]")); } } + + Future _compress(String filename) async { + await fileCompressor.compress(filename); + } + + /// Returns file size in mb + int _getSize(String filename) { + final file = File(filename); + double size = file.lengthSync() / 1024; + return double.parse(size.toStringAsFixed(0)).toInt(); + } } diff --git a/lib/assets.dart b/lib/assets.dart index c689c4a..f77d3e6 100644 --- a/lib/assets.dart +++ b/lib/assets.dart @@ -1,14 +1,21 @@ class Asset { String source; List directories; + /// { quality: 0-100, use: true/false } + Map compress; Asset({ required this.source, required this.directories, + required this.compress, }); @override String toString() { - return "Asset(source: $source, directories: $directories)"; + return "Asset(" + "source: $source, " + "directories: $directories, " + "compress: $compress" + ")"; } } diff --git a/lib/dependency.dart b/lib/dependency.dart index 696b6bf..048c2be 100644 --- a/lib/dependency.dart +++ b/lib/dependency.dart @@ -24,8 +24,8 @@ class Dependency { assetMover.moveFilesExclSuffix(".js", files, source); } - void moveAssets() { - assetMover.moveAssets(); + moveAssets() async { + await assetMover.moveAssets(); } void bundleFiles(StringBuffer buffer) { diff --git a/lib/file_compressor.dart b/lib/file_compressor.dart new file mode 100644 index 0000000..d75cc8b --- /dev/null +++ b/lib/file_compressor.dart @@ -0,0 +1,82 @@ +import 'dart:io'; +import 'package:image/image.dart' as img; + +class FileCompressor { + + List fileTypes = [ + "jpg", + "png", + "jpeg", + "gif", + "bmp", + "tga", + "tiff", + ]; + /// quality can be set from 1-100 - default 80 + int quality = 80; + + FileCompressor({required this.quality}) { + quality = quality.clamp(1, 100).toInt(); + } + + Future compress(String filename) async { + final file = File(filename); + final bytes = await file.readAsBytes(); + final image = img.decodeImage(bytes)!; + final fileType = _getFileExt(filename); + if (!_validate(fileType)) { + + return; + } + switch (fileType) { + case "jpg": + case "jpeg": { + final compressed = img.encodeJpg(image, quality: quality); + await File(filename).writeAsBytes(compressed); + break; + } + case "png": { + // level: min = 0, max = 9, default = 6 + final compressed = img.encodePng(image, level: _round()); + await File(filename).writeAsBytes(compressed); + break; + } + case "gif": { + final compressed = img.encodeGif(image); + await File(filename).writeAsBytes(compressed); + break; + } + case "bmp": { + final compressed = img.encodeBmp(image); + await File(filename).writeAsBytes(compressed); + break; + } + case "tga": { + final compressed = img.encodeTga(image); + await File(filename).writeAsBytes(compressed); + break; + } + case "tiff": { + final compressed = img.encodeTiff(image); + await File(filename).writeAsBytes(compressed); + break; + } + default: { + // No op... + return; + } + } + } + + String _getFileExt(String fileName) { + return fileName.split(".").last.toLowerCase(); + } + + bool _validate(String ext) { + return fileTypes.contains(ext); + } + + int _round() { + return ((100 - quality) / 100 * 9).round().clamp(0, 9).toInt(); + } +} \ No newline at end of file diff --git a/lib/warden.dart b/lib/warden.dart index 60c63a1..17bcbec 100644 --- a/lib/warden.dart +++ b/lib/warden.dart @@ -4,6 +4,7 @@ import "package:path/path.dart" as p; import "package:warden/environment.dart"; import "package:warden/exceptions.dart"; import "package:warden/excluder.dart"; +import "package:warden/file_compressor.dart"; import "package:warden/logger.dart"; import "package:warden/main_file.dart"; import "package:warden/mode.dart"; @@ -127,7 +128,7 @@ class Warden { await processor.run(); } // pre bundle the initial run - _bundleAndMoveFiles(stopwatch); + await _bundleAndMoveFiles(stopwatch); } _runWatcher(Watcher watcher) { @@ -155,7 +156,7 @@ class Warden { } // Wait for all processes to run & then re bundle file await Future.wait(futures); - _bundleAndMoveFiles(stopwatch); + await _bundleAndMoveFiles(stopwatch); } on ProcessingCompileException catch (e) { if (debug) { log.info(e.toString()); @@ -187,7 +188,7 @@ class Warden { await processor.run(); } // pre bundle the initial run - _bundleAndMoveFiles(stopwatch); + await _bundleAndMoveFiles(stopwatch); watcher.events.listen((event) async { final normalized = p.normalize(event.path); @@ -202,11 +203,11 @@ class Warden { } // Wait for all processes to run & then re bundle file await Future.wait(futures); - _bundleAndMoveFiles(stopwatch); + await _bundleAndMoveFiles(stopwatch); }); } - void _bundleAndMoveFiles(Stopwatch stopwatch) { + _bundleAndMoveFiles(Stopwatch stopwatch) async { bundler.destroyBundleFile(); // Initiate the String buffer bundler.start(); @@ -220,7 +221,7 @@ class Warden { dependency.moveAllFiles(); } if (assets.source != "") { - dependency.moveAssets(); + await dependency.moveAssets(); } } // Bundle the main file @@ -287,11 +288,25 @@ class Warden { if (dependency["main"] != null) { mainFile = dependency["main"] as String; } + + // Setup file compression from asset yaml options + int quality = 100; + if (assets.compress["quality"] != null) { + quality = assets.compress["quality"]; + } + FileCompressor fileCompressor = FileCompressor( + quality: quality, + ); + dependencies.add(Dependency( source: dependency["source"] as String, bundle: bundle, files: List.from(dependency["files"]), - assetMover: AssetMover(destination: destination, assets: assets), + assetMover: AssetMover( + destination: destination, + assets: assets, + fileCompressor: fileCompressor, + ), bundler: Bundler(destination, dependencyMainFile: mainFile, debug: debug), )); } @@ -317,6 +332,7 @@ class Warden { _setAssets(dynamic yamlMap) { var assetResult = yamlMap["assets"]; String source = ""; + Map compress = {}; List directories = []; if (assetResult != null && assetResult["source"] != null) { source = assetResult["source"]; @@ -324,9 +340,33 @@ class Warden { if (assetResult != null && assetResult["directories"] != null) { directories = List.from(assetResult["directories"]); } + if (assetResult != null && assetResult["compress"] != null) { + if (assetResult["compress"]["quality"] != null) { + compress = { + "quality": assetResult["compress"]["quality"], + "use": true, + }; + print(AnsiStyles.cyan("◆ setting file compression to [${AnsiStyles.cyanBright.bold("${compress['quality']}%")}]")); + } else { + compress = { + "quality": 80, + "use": true, + }; + print(AnsiStyles.cyan("◆ setting file compression to default [${AnsiStyles.cyanBright.bold("${compress['quality']}%")}]")); + log.info(AnsiStyles.yellow("Setting image compression quality default: ${compress["quality"]}")); + } + /// The fileCompressor is still get created but passing `use: false` will equate to a no op. + if (assetResult["compress"] == null) { + compress = { + "quality": 100, + "use": false, + }; + } + } assets = Asset( source: source, directories: directories, + compress: compress, ); } } diff --git a/pubspec.lock b/pubspec.lock index 761f58b..35dc48c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.2+1" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: "direct main" description: @@ -97,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" file: dependency: transitive description: @@ -137,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: "direct main" + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" io: dependency: transitive description: @@ -217,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" pool: dependency: transitive description: @@ -225,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" pub_semver: dependency: transitive description: @@ -401,6 +441,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: "direct main" description: @@ -410,4 +458,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index e54e0f7..5a8044b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: logging: ^1.3.0 ansi_styles: ^0.3.2+1 cli_spinner: ^1.0.4 + image: ^4.5.4 dev_dependencies: lints: ^5.0.0