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()); }