From f495aa40c9eba771cbf2d75b2e19fd8b1dd0f183 Mon Sep 17 00:00:00 2001 From: scorsin-oai Date: Wed, 13 May 2026 14:11:38 -0500 Subject: [PATCH] Added support for generating app icons (#104) ## Description This change adds support for generating app icons at build time, that we can just do: ```python valdi_application( name = "hello_world", icons = valdi_application_icons(src = "app_assets/icon.svg"), # [redacted] ) ``` and the icon gets generated for each platform depending on what we are compiling for, in different variants. Codex can quickly generate SVGs for the icons for the sample apps it makes, it will avoid having a large list of sample apps with default icons. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] Documentation improvement - [ ] Performance optimization - [ ] Test improvement - [ ] Other (please describe) ## Testing - [ ] Tests pass locally (`bazel test //...`) - [ ] Added/updated tests for changes (if applicable) - [ ] Tested on multiple platforms (iOS/Android/Web/macOS as applicable) - [ ] Manual testing performed (describe below) ### Testing Details ## Checklist - [ ] Code follows project style guidelines - [ ] Documentation updated (if needed) - [ ] No breaking changes (or documented in description) - [ ] Commit messages follow [conventional format](../CONTRIBUTING.md#commit-messages) - [ ] No secrets, API keys, or internal URLs included ## Related Issues ## Additional Context (cherry picked from commit 4c867976ed487735998f87583c682a62e62028dd) --- apps/helloworld/BUILD.bazel | 9 +- bzl/valdi/valdi_android_application.bzl | 20 ++++ bzl/valdi/valdi_android_application_icons.bzl | 108 ++++++++++++++++++ bzl/valdi/valdi_application.bzl | 52 +++++++++ bzl/valdi/valdi_application_icons.bzl | 19 +++ bzl/valdi/valdi_application_icons_helper.bzl | 47 ++++++++ bzl/valdi/valdi_ios_application.bzl | 10 +- bzl/valdi/valdi_ios_application_icons.bzl | 82 +++++++++++++ bzl/valdi/valdi_macos_application.bzl | 10 ++ bzl/valdi/valdi_macos_application_icons.bzl | 72 ++++++++++++ docs/docs/workflow-appstore-release.md | 39 ++++++- .../src/image_toolbox/ImageToolbox.cpp | 59 +++++++++- .../src/image_toolbox/ImageToolbox.hpp | 3 +- .../src/image_toolbox_c/image_toolbox.cpp | 3 +- .../compiler_toolbox/CompilerToolbox.cpp | 5 +- 15 files changed, 527 insertions(+), 11 deletions(-) create mode 100644 bzl/valdi/valdi_android_application_icons.bzl create mode 100644 bzl/valdi/valdi_application_icons.bzl create mode 100644 bzl/valdi/valdi_application_icons_helper.bzl create mode 100644 bzl/valdi/valdi_ios_application_icons.bzl create mode 100644 bzl/valdi/valdi_macos_application_icons.bzl diff --git a/apps/helloworld/BUILD.bazel b/apps/helloworld/BUILD.bazel index 4b70e59b..74f1da2a 100644 --- a/apps/helloworld/BUILD.bazel +++ b/apps/helloworld/BUILD.bazel @@ -1,12 +1,15 @@ -load("//bzl/valdi:valdi_application.bzl", "valdi_application") +load( + "//bzl/valdi:valdi_application.bzl", + "valdi_application", + "valdi_application_icons", +) load("//bzl/valdi:valdi_exported_library.bzl", "valdi_exported_library") valdi_application( name = "hello_world", android_activity_theme_name = "Theme.MyApp.Launch", - android_app_icon_name = "app_icon", android_resource_files = glob(["app_assets/android/**"]), - ios_app_icons = glob(["app_assets/ios/Icons.xcassets/**"]), + icons = valdi_application_icons(src = "app_assets/icon.png"), ios_bundle_id = "com.snap.valdi.helloworld", ios_families = ["iphone"], root_component_path = "App@hello_world/src/HelloWorldApp", diff --git a/bzl/valdi/valdi_android_application.bzl b/bzl/valdi/valdi_android_application.bzl index ff4f536c..e9f24dbe 100644 --- a/bzl/valdi/valdi_android_application.bzl +++ b/bzl/valdi/valdi_android_application.bzl @@ -2,8 +2,16 @@ load("@rules_android//rules:rules.bzl", "android_binary") load("@rules_kotlin//kotlin:android.bzl", "kt_android_library") load("@valdi//valdi:valdi.bzl", "valdi_android_aar") load("//bzl:expand_template.bzl", "expand_template") +load( + "//bzl/valdi:valdi_android_application_icons.bzl", + "generate_valdi_android_application_icons", + _valdi_android_application_icons = "valdi_android_application_icons", +) load("//bzl/valdi/source_set:utils.bzl", "source_set_select") +def valdi_android_application_icons(src, round_src = None): + return _valdi_android_application_icons(src = src, round_src = round_src) + def _make_xml_compound_substitution(key_values): output = [] @@ -20,6 +28,7 @@ def valdi_android_application( app_manifest = None, assets = None, assets_dir = None, + app_icons = None, resource_files = None, icon_name = None, round_icon_name = None, @@ -30,6 +39,17 @@ def valdi_android_application( src_activity_target = "{}_activitygen".format(name) aar_target = "{}_aar".format(name) + generated_app_icons = generate_valdi_android_application_icons( + name, + app_icons, + resource_files, + icon_name, + round_icon_name, + ) + resource_files = generated_app_icons.resource_files + icon_name = generated_app_icons.icon_name + round_icon_name = generated_app_icons.round_icon_name + expand_template( name = src_activity_target, src = "@valdi//bzl/valdi/app_templates:StartActivity.kt.tpl", diff --git a/bzl/valdi/valdi_android_application_icons.bzl b/bzl/valdi/valdi_android_application_icons.bzl new file mode 100644 index 00000000..72315b16 --- /dev/null +++ b/bzl/valdi/valdi_android_application_icons.bzl @@ -0,0 +1,108 @@ +load( + "//bzl/valdi:valdi_application_icons_helper.bzl", + "convert_icon", + "is_application_icons", + "make_application_icons", + "toolbox_attr", +) + +def valdi_android_application_icons(src, round_src = None): + return make_application_icons("android", src, round_src = round_src) + +_ANDROID_ICON_DENSITIES = [ + ("mipmap-mdpi", 48), + ("mipmap-hdpi", 72), + ("mipmap-xhdpi", 96), + ("mipmap-xxhdpi", 144), + ("mipmap-xxxhdpi", 192), +] + +def _android_application_icons_impl(ctx): + outputs = [] + + for density, pixel_size in _ANDROID_ICON_DENSITIES: + output = ctx.actions.declare_file("{}/{}/app_icon.png".format(ctx.attr.resource_root, density)) + outputs.append(output) + convert_icon(ctx, ctx.file.src, output, pixel_size) + + round_src = ctx.file.round_src if ctx.file.round_src else ctx.file.src + round_output = ctx.actions.declare_file("{}/{}/round_app_icon.png".format(ctx.attr.resource_root, density)) + outputs.append(round_output) + convert_icon(ctx, round_src, round_output, pixel_size, round_icon = not ctx.file.round_src) + + return [DefaultInfo(files = depset(outputs))] + +_android_application_icons = rule( + implementation = _android_application_icons_impl, + attrs = { + "resource_root": attr.string(mandatory = True), + "src": attr.label(allow_single_file = True, mandatory = True), + "round_src": attr.label(allow_single_file = True), + "_toolbox": toolbox_attr(), + }, +) + +_ANDROID_RESOURCE_DIR_PREFIXES = [ + "anim", + "animator", + "color", + "drawable", + "font", + "layout", + "menu", + "mipmap", + "raw", + "transition", + "values", + "xml", +] + +def _is_android_resource_dir(path_segment): + for prefix in _ANDROID_RESOURCE_DIR_PREFIXES: + if path_segment == prefix or path_segment.startswith("{}-".format(prefix)): + return True + + return False + +def _infer_android_resource_root(resource_files, default_root): + for resource_file in resource_files or []: + if type(resource_file) != "string": + continue + + path_segments = resource_file.split("/") + for index, path_segment in enumerate(path_segments): + if _is_android_resource_dir(path_segment): + return "/".join(path_segments[:index]) + + return default_root + +def generate_valdi_android_application_icons(name, app_icons, resource_files, icon_name, round_icon_name): + if app_icons == None: + return struct( + resource_files = resource_files, + icon_name = icon_name, + round_icon_name = round_icon_name, + ) + + if not is_application_icons(app_icons, "android"): + fail("android app_icons must be created with valdi_android_application_icons()") + + target_name = "{}_generated_app_icons".format(name) + resource_root = _infer_android_resource_root(resource_files, target_name) + kwargs = { + "name": target_name, + "resource_root": resource_root, + "src": app_icons.src, + } + if app_icons.round_src: + kwargs["round_src"] = app_icons.round_src + _android_application_icons(**kwargs) + + resolved_resource_files = list(resource_files or []) + resolved_resource_files.append(":{}".format(target_name)) + + return struct( + resource_files = resolved_resource_files, + icon_name = icon_name or "app_icon", + round_icon_name = round_icon_name or "round_app_icon", + ) diff --git a/bzl/valdi/valdi_application.bzl b/bzl/valdi/valdi_application.bzl index 81bc43b1..3b9d61ba 100644 --- a/bzl/valdi/valdi_application.bzl +++ b/bzl/valdi/valdi_application.bzl @@ -1,13 +1,43 @@ load("//bzl/valdi:suffixed_deps.bzl", "get_suffixed_deps") load("//bzl/valdi:valdi_android_application.bzl", "valdi_android_application") +load( + "//bzl/valdi:valdi_android_application_icons.bzl", + _valdi_android_application_icons = "valdi_android_application_icons", +) load("//bzl/valdi:valdi_ios_application.bzl", "valdi_ios_application") +load( + "//bzl/valdi:valdi_ios_application_icons.bzl", + _valdi_ios_application_icons = "valdi_ios_application_icons", +) load("//bzl/valdi:valdi_macos_application.bzl", "valdi_macos_application") +load( + "//bzl/valdi:valdi_macos_application_icons.bzl", + _valdi_macos_application_icons = "valdi_macos_application_icons", +) +load( + "//bzl/valdi:valdi_application_icons.bzl", + _valdi_application_icons = "valdi_application_icons", +) load("//bzl/valdi:valdi_module.bzl", "valdi_hotreload") +def valdi_application_icons(src, round_src = None): + return _valdi_application_icons(src = src, round_src = round_src) + +def valdi_ios_application_icons(src): + return _valdi_ios_application_icons(src = src) + +def valdi_android_application_icons(src, round_src = None): + return _valdi_android_application_icons(src = src, round_src = round_src) + +def valdi_macos_application_icons(src): + return _valdi_macos_application_icons(src = src) + def valdi_application( name, title, root_component_path, + icons = None, + app_icons = None, ios_bundle_id = None, ios_info_plist = None, ios_families = None, @@ -18,10 +48,12 @@ def valdi_application( android_assets = None, android_assets_dir = None, android_resource_files = None, + android_app_icons = None, android_app_manifest = None, android_app_icon_name = None, android_round_app_icon_name = None, android_activity_theme_name = None, + macos_app_icons = None, desktop_window_width = 600, desktop_window_height = 800, desktop_window_resizable = True, @@ -29,6 +61,24 @@ def valdi_application( deps = []): resolved_ios_bundle_id = ios_bundle_id if ios_bundle_id else "com.snap.valdi.{}".format(name) resolved_android_package = android_package if android_package else "com.snap.valdi.{}".format(name) + resolved_app_icons = icons if icons != None else app_icons + + if icons != None and app_icons != None: + fail("Only one of icons or app_icons may be specified") + + if resolved_app_icons != None: + if not hasattr(resolved_app_icons, "_valdi_application_icons") or resolved_app_icons._valdi_application_icons != "all": + fail("icons must be created with valdi_application_icons()") + + if ios_app_icons == None: + ios_app_icons = _valdi_ios_application_icons(src = resolved_app_icons.src) + if android_app_icons == None: + android_app_icons = _valdi_android_application_icons( + src = resolved_app_icons.src, + round_src = resolved_app_icons.round_src, + ) + if macos_app_icons == None: + macos_app_icons = _valdi_macos_application_icons(src = resolved_app_icons.src) valdi_ios_application( name = "{}_ios".format(name), @@ -51,6 +101,7 @@ def valdi_application( package = resolved_android_package, assets = android_assets, assets_dir = android_assets_dir, + app_icons = android_app_icons, app_manifest = android_app_manifest, resource_files = android_resource_files, icon_name = android_app_icon_name, @@ -68,6 +119,7 @@ def valdi_application( window_width = desktop_window_width, window_height = desktop_window_height, window_resizable = desktop_window_resizable, + app_icons = macos_app_icons, deps = get_suffixed_deps(deps, "_native"), ) diff --git a/bzl/valdi/valdi_application_icons.bzl b/bzl/valdi/valdi_application_icons.bzl new file mode 100644 index 00000000..23e53561 --- /dev/null +++ b/bzl/valdi/valdi_application_icons.bzl @@ -0,0 +1,19 @@ +load( + "//bzl/valdi:valdi_android_application_icons.bzl", + "valdi_android_application_icons", +) +load( + "//bzl/valdi:valdi_application_icons_helper.bzl", + "make_application_icons", +) +load( + "//bzl/valdi:valdi_ios_application_icons.bzl", + "valdi_ios_application_icons", +) +load( + "//bzl/valdi:valdi_macos_application_icons.bzl", + "valdi_macos_application_icons", +) + +def valdi_application_icons(src, round_src = None): + return make_application_icons("all", src, round_src = round_src) diff --git a/bzl/valdi/valdi_application_icons_helper.bzl b/bzl/valdi/valdi_application_icons_helper.bzl new file mode 100644 index 00000000..e1e31b21 --- /dev/null +++ b/bzl/valdi/valdi_application_icons_helper.bzl @@ -0,0 +1,47 @@ +def make_application_icons(platform, src, round_src = None): + return struct( + _valdi_application_icons = platform, + src = src, + round_src = round_src, + ) + +def is_application_icons(value, platform): + return hasattr(value, "_valdi_application_icons") and value._valdi_application_icons == platform + +def toolbox_attr(): + return attr.label( + default = Label("//valdi/compiler/toolbox:valdi_compiler_toolbox"), + executable = True, + cfg = "exec", + ) + +def convert_icon(ctx, src, output, size, round_icon = False): + args = ctx.actions.args() + args.add("image_convert") + args.add("-i", src) + args.add("-o", output) + args.add("-w", str(size)) + args.add("-h", str(size)) + if round_icon: + args.add("--round") + + ctx.actions.run( + executable = ctx.executable._toolbox, + inputs = [src], + outputs = [output], + arguments = [args], + mnemonic = "ValdiApplicationIcon", + progress_message = "Generating application icon {}".format(output.short_path), + ) + +def write_apple_contents_json(ctx, output, images): + ctx.actions.write( + output = output, + content = json.encode_indent({ + "images": images, + "info": { + "author": "xcode", + "version": 1, + }, + }) + "\n", + ) diff --git a/bzl/valdi/valdi_ios_application.bzl b/bzl/valdi/valdi_ios_application.bzl index f04f00fb..e0a35252 100644 --- a/bzl/valdi/valdi_ios_application.bzl +++ b/bzl/valdi/valdi_ios_application.bzl @@ -1,6 +1,14 @@ load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application") load("@build_bazel_rules_apple//apple:versioning.bzl", "apple_bundle_version") load("//bzl:expand_template.bzl", "expand_template") +load( + "//bzl/valdi:valdi_ios_application_icons.bzl", + "generate_valdi_ios_application_icons", + _valdi_ios_application_icons = "valdi_ios_application_icons", +) + +def valdi_ios_application_icons(src): + return _valdi_ios_application_icons(src = src) def make_short_version(version): components = version.split(".") @@ -87,7 +95,7 @@ def valdi_ios_application( deps = [":{}".format(src_target)], minimum_os_version = minimum_os_version, provisioning_profile = provisioning_profile, - app_icons = app_icons, + app_icons = generate_valdi_ios_application_icons(name, app_icons), version = resolved_version, tags = ["valdi_ios_application"], visibility = ["//visibility:public"], diff --git a/bzl/valdi/valdi_ios_application_icons.bzl b/bzl/valdi/valdi_ios_application_icons.bzl new file mode 100644 index 00000000..1ea1fb74 --- /dev/null +++ b/bzl/valdi/valdi_ios_application_icons.bzl @@ -0,0 +1,82 @@ +load( + "//bzl/valdi:valdi_application_icons_helper.bzl", + "convert_icon", + "is_application_icons", + "make_application_icons", + "toolbox_attr", + "write_apple_contents_json", +) + +def valdi_ios_application_icons(src): + return make_application_icons("ios", src) + +_IOS_ICON_ENTRIES = [ + ("iphone", "60x60", "3x", 180), + ("iphone", "40x40", "2x", 80), + ("iphone", "40x40", "3x", 120), + ("iphone", "60x60", "2x", 120), + ("iphone", "29x29", "2x", 58), + ("iphone", "29x29", "1x", 29), + ("iphone", "29x29", "3x", 87), + ("iphone", "20x20", "2x", 40), + ("iphone", "20x20", "3x", 60), + ("ios-marketing", "1024x1024", "1x", 1024), + ("ipad", "40x40", "2x", 80), + ("ipad", "76x76", "2x", 152), + ("ipad", "29x29", "2x", 58), + ("ipad", "29x29", "1x", 29), + ("ipad", "40x40", "1x", 40), + ("ipad", "83.5x83.5", "2x", 167), + ("ipad", "20x20", "1x", 20), + ("ipad", "20x20", "2x", 40), +] + +def _ios_application_icons_impl(ctx): + icon_dir = "{}/Assets.xcassets/AppIcon.appiconset".format(ctx.label.name) + outputs_by_size = {} + images = [] + + for idiom, point_size, scale, pixel_size in _IOS_ICON_ENTRIES: + filename = "{}.png".format(pixel_size) + if pixel_size not in outputs_by_size: + output = ctx.actions.declare_file("{}/{}".format(icon_dir, filename)) + outputs_by_size[pixel_size] = output + convert_icon(ctx, ctx.file.src, output, pixel_size) + + images.append({ + "expected-size": str(pixel_size), + "filename": filename, + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": idiom, + "scale": scale, + "size": point_size, + }) + + contents = ctx.actions.declare_file("{}/Contents.json".format(icon_dir)) + write_apple_contents_json(ctx, contents, images) + + return [DefaultInfo(files = depset(list(outputs_by_size.values()) + [contents]))] + +_ios_application_icons = rule( + implementation = _ios_application_icons_impl, + attrs = { + "src": attr.label(allow_single_file = True, mandatory = True), + "_toolbox": toolbox_attr(), + }, +) + +def generate_valdi_ios_application_icons(name, app_icons): + if app_icons == None: + return app_icons + + if not is_application_icons(app_icons, "ios"): + if hasattr(app_icons, "_valdi_application_icons"): + fail("ios app_icons must be created with valdi_ios_application_icons()") + return app_icons + + target_name = "{}_generated_app_icons".format(name) + _ios_application_icons( + name = target_name, + src = app_icons.src, + ) + return [":{}".format(target_name)] diff --git a/bzl/valdi/valdi_macos_application.bzl b/bzl/valdi/valdi_macos_application.bzl index a98f02c2..3211ff09 100644 --- a/bzl/valdi/valdi_macos_application.bzl +++ b/bzl/valdi/valdi_macos_application.bzl @@ -1,5 +1,13 @@ load("@build_bazel_rules_apple//apple:macos.bzl", "macos_application") load("//bzl:expand_template.bzl", "expand_template") +load( + "//bzl/valdi:valdi_macos_application_icons.bzl", + "generate_valdi_macos_application_icons", + _valdi_macos_application_icons = "valdi_macos_application_icons", +) + +def valdi_macos_application_icons(src): + return _valdi_macos_application_icons(src = src) def valdi_macos_application( name, @@ -9,6 +17,7 @@ def valdi_macos_application( window_width, window_height, window_resizable, + app_icons = None, deps = []): main_target = "{}_maingen".format(name) plist_target = "{}_plist".format(name) @@ -51,6 +60,7 @@ def valdi_macos_application( infoplists = [":{}".format(plist_target)], deps = [":{}".format(src_target)], minimum_os_version = "15.0", + app_icons = generate_valdi_macos_application_icons(name, app_icons), tags = ["valdi_macos_application"], visibility = ["//visibility:public"], ) diff --git a/bzl/valdi/valdi_macos_application_icons.bzl b/bzl/valdi/valdi_macos_application_icons.bzl new file mode 100644 index 00000000..340e62f3 --- /dev/null +++ b/bzl/valdi/valdi_macos_application_icons.bzl @@ -0,0 +1,72 @@ +load( + "//bzl/valdi:valdi_application_icons_helper.bzl", + "convert_icon", + "is_application_icons", + "make_application_icons", + "toolbox_attr", + "write_apple_contents_json", +) + +def valdi_macos_application_icons(src): + return make_application_icons("macos", src) + +_MACOS_ICON_ENTRIES = [ + ("mac", "16x16", "1x", 16), + ("mac", "16x16", "2x", 32), + ("mac", "32x32", "1x", 32), + ("mac", "32x32", "2x", 64), + ("mac", "128x128", "1x", 128), + ("mac", "128x128", "2x", 256), + ("mac", "256x256", "1x", 256), + ("mac", "256x256", "2x", 512), + ("mac", "512x512", "1x", 512), + ("mac", "512x512", "2x", 1024), +] + +def _macos_application_icons_impl(ctx): + icon_dir = "{}/Assets.xcassets/AppIcon.appiconset".format(ctx.label.name) + outputs_by_size = {} + images = [] + + for idiom, point_size, scale, pixel_size in _MACOS_ICON_ENTRIES: + filename = "{}.png".format(pixel_size) + if pixel_size not in outputs_by_size: + output = ctx.actions.declare_file("{}/{}".format(icon_dir, filename)) + outputs_by_size[pixel_size] = output + convert_icon(ctx, ctx.file.src, output, pixel_size) + + images.append({ + "filename": filename, + "idiom": idiom, + "scale": scale, + "size": point_size, + }) + + contents = ctx.actions.declare_file("{}/Contents.json".format(icon_dir)) + write_apple_contents_json(ctx, contents, images) + + return [DefaultInfo(files = depset(list(outputs_by_size.values()) + [contents]))] + +_macos_application_icons = rule( + implementation = _macos_application_icons_impl, + attrs = { + "src": attr.label(allow_single_file = True, mandatory = True), + "_toolbox": toolbox_attr(), + }, +) + +def generate_valdi_macos_application_icons(name, app_icons): + if app_icons == None: + return app_icons + + if not is_application_icons(app_icons, "macos"): + if hasattr(app_icons, "_valdi_application_icons"): + fail("macos app_icons must be created with valdi_macos_application_icons()") + return app_icons + + target_name = "{}_generated_app_icons".format(name) + _macos_application_icons( + name = target_name, + src = app_icons.src, + ) + return [":{}".format(target_name)] diff --git a/docs/docs/workflow-appstore-release.md b/docs/docs/workflow-appstore-release.md index 09b79ad3..f979808c 100644 --- a/docs/docs/workflow-appstore-release.md +++ b/docs/docs/workflow-appstore-release.md @@ -41,6 +41,43 @@ ls app_icons/ios/Assets.xcassets/AppIcon.appiconset 1024.png 120.png 152.png 167.png 180.png 20.png 29.png 40.png 58.png 60.png 80.png 87.png Contents.json ``` +You can also generate app icons from a single source image at build time: + +```py +load( + "//bzl/valdi:valdi_application.bzl", + "valdi_application", + "valdi_application_icons", +) + +valdi_application( + name = "hello_world", + icons = valdi_application_icons(src = "app_icon.png"), +) +``` + +If platform-specific source images are needed, use the platform icon macros directly: + +```py +load( + "//bzl/valdi:valdi_application.bzl", + "valdi_application", + "valdi_android_application_icons", + "valdi_ios_application_icons", + "valdi_macos_application_icons", +) + +valdi_application( + name = "hello_world", + ios_app_icons = valdi_ios_application_icons(src = "ios_icon.png"), + android_app_icons = valdi_android_application_icons( + src = "android_icon.png", + round_src = "android_round_icon.png", + ), + macos_app_icons = valdi_macos_application_icons(src = "macos_icon.png"), +) +``` + Build your app in release mode using the `valdi package` command: ```sh valdi package ios --build_config release --output_path hello_world.ipa @@ -96,4 +133,4 @@ valdi package android --build_config release --output_path hello_world.apk ``` If the `valdi` CLI asks you to choose between an unsigned and a signed apk, choose the unsigned one. -You can then submit the `apk` to the Google Play Store. \ No newline at end of file +You can then submit the `apk` to the Google Play Store. diff --git a/libs/image_toolbox/src/image_toolbox/ImageToolbox.cpp b/libs/image_toolbox/src/image_toolbox/ImageToolbox.cpp index d1621eba..79dc1487 100644 --- a/libs/image_toolbox/src/image_toolbox/ImageToolbox.cpp +++ b/libs/image_toolbox/src/image_toolbox/ImageToolbox.cpp @@ -1,6 +1,11 @@ #include "ImageToolbox.hpp" #include "SVGRenderer.hpp" +#include "snap_drawing/cpp/Drawing/DisplayList/DisplayList.hpp" +#include "snap_drawing/cpp/Drawing/DrawingContext.hpp" +#include "snap_drawing/cpp/Drawing/GraphicsContext/BitmapGraphicsContext.hpp" +#include "snap_drawing/cpp/Drawing/Surface/DrawableSurfaceCanvas.hpp" #include "snap_drawing/cpp/Utils/Image.hpp" +#include "snap_drawing/cpp/Utils/Path.hpp" #include "valdi_core/cpp/Utils/DiskUtils.hpp" #include "valdi_core/cpp/Utils/Format.hpp" #include "valdi_core/cpp/Utils/Function.hpp" @@ -48,6 +53,42 @@ static Valdi::Result> loadImage(const Valdi::StringBox& imageFilePath }); } +static Valdi::Result> clipImageToCircle(const Ref& image) { + auto width = image->width(); + auto height = image->height(); + + DrawingContext drawingContext(width, height); + + Path clipPath; + clipPath.addOval(Rect::makeXYWH(0, 0, width, height), true); + drawingContext.clipPath(clipPath); + + auto imageRect = Rect::makeXYWH(0, 0, image->width(), image->height()); + auto targetRect = Rect::makeXYWH(0, 0, width, height); + drawingContext.drawImage(*image, imageRect, targetRect, nullptr); + + auto displayList = Valdi::makeShared(Size(width, height), TimePoint(0.0)); + displayList->appendLayerContent(drawingContext.finish(), 1.0); + + BitmapGraphicsContext bitmapContext; + auto surface = bitmapContext.createBitmapSurface( + Valdi::BitmapInfo(width, height, Valdi::ColorTypeRGBA8888, Valdi::AlphaTypePremul, 0)); + + auto canvas = surface->prepareCanvas(); + if (!canvas) { + return canvas.moveError(); + } + + displayList->draw(canvas.value(), kDisplayListAllPlaneIndexes, true); + + auto snapshot = canvas.value().snapshot(); + if (!snapshot) { + return snapshot.moveError(); + } + + return snapshot.moveValue(); +} + Valdi::Result getImageInfo(const Valdi::StringBox& imageFilePath) { auto imageSize = processImage>( imageFilePath, @@ -89,7 +130,8 @@ Valdi::Result convertImage(const Valdi::StringBox& inputImageF const Valdi::StringBox& outputImageFilePath, const std::optional& outputWidth, const std::optional& outputHeight, - double qualityRatio) { + double qualityRatio, + bool roundOutput) { Valdi::Path outputPath(outputImageFilePath.toStringView()); snap::drawing::EncodedImageFormat outputFormat; @@ -128,10 +170,23 @@ Valdi::Result convertImage(const Valdi::StringBox& inputImageF newWidth = static_cast(round(newHeight * ratio)); } - if (newWidth != image->width() && newHeight != image->height()) { + if (newWidth != image->width() || newHeight != image->height()) { image = image->resized(newWidth, newHeight); } + if (roundOutput) { + if (newWidth != newHeight) { + return Valdi::Error("Round image output requires equal width and height"); + } + + auto clippedImage = clipImageToCircle(image); + if (!clippedImage) { + return clippedImage.moveError(); + } + + image = clippedImage.moveValue(); + } + auto encodeResult = image->encode(outputFormat, qualityRatio); if (!encodeResult) { return encodeResult.moveError(); diff --git a/libs/image_toolbox/src/image_toolbox/ImageToolbox.hpp b/libs/image_toolbox/src/image_toolbox/ImageToolbox.hpp index ef050b0a..fde06b08 100644 --- a/libs/image_toolbox/src/image_toolbox/ImageToolbox.hpp +++ b/libs/image_toolbox/src/image_toolbox/ImageToolbox.hpp @@ -19,6 +19,7 @@ Valdi::Result convertImage(const Valdi::StringBox& inputImageF const Valdi::StringBox& outputImageFilePath, const std::optional& outputWidth, const std::optional& outputHeight, - double qualityRatio); + double qualityRatio, + bool roundOutput); } // namespace snap::imagetoolbox diff --git a/libs/image_toolbox/src/image_toolbox_c/image_toolbox.cpp b/libs/image_toolbox/src/image_toolbox_c/image_toolbox.cpp index 1d5d57be..49bc1fb0 100644 --- a/libs/image_toolbox/src/image_toolbox_c/image_toolbox.cpp +++ b/libs/image_toolbox/src/image_toolbox_c/image_toolbox.cpp @@ -36,7 +36,8 @@ int imagetoolbox_convert( Valdi::StringCache::getGlobal().makeStringFromLiteral(output_path), {outputWidth}, {outputHeight}, - 1.0); + 1.0, + false); if (result) { return 0; diff --git a/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CompilerToolbox.cpp b/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CompilerToolbox.cpp index dfdb2d63..ca1e37cd 100644 --- a/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CompilerToolbox.cpp +++ b/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CompilerToolbox.cpp @@ -100,6 +100,7 @@ static int imageConvert(Arguments& arguments) { auto width = parser.addArgument("-w")->setDescription("The output width"); auto height = parser.addArgument("-h")->setDescription("The output height"); auto quality = parser.addArgument("-q")->setDescription("The quality ratio between 0 and 1, applicable for JPG"); + auto round = parser.addArgument("--round")->setDescription("Clip the output image to a circle")->setAsFlag(); auto result = parser.parse(arguments); if (!result) { @@ -124,8 +125,8 @@ static int imageConvert(Arguments& arguments) { } } - auto convertResult = - snap::imagetoolbox::convertImage(input->value(), output->value(), outputWidth, outputHeight, qualityRatio); + auto convertResult = snap::imagetoolbox::convertImage( + input->value(), output->value(), outputWidth, outputHeight, qualityRatio, round->hasValue()); if (!convertResult) { return onError(convertResult.error()); }