From 7ffe4a55b90a6b9cb71606d94043d35ddeba2761 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Tue, 6 Sep 2022 14:36:27 +0800 Subject: [PATCH] Change orientation of an image in editor --- app/lib/l10n/app_en.arb | 21 ++ app/lib/l10n/untranslated-messages.txt | 55 ++++ app/lib/widget/image_editor.dart | 136 +++++++++- .../image_editor/transform_toolbar.dart | 253 ++++++++++++++++++ plugin/android/src/main/cpp/CMakeLists.txt | 1 + .../src/main/cpp/filter/orientation.cpp | 136 ++++++++++ .../plugin/ImageProcessorChannelHandler.kt | 1 + .../plugin/image_processor/Orientation.kt | 18 ++ plugin/lib/src/image_processor.dart | 12 + 9 files changed, 624 insertions(+), 9 deletions(-) create mode 100644 app/lib/widget/image_editor/transform_toolbar.dart create mode 100644 plugin/android/src/main/cpp/filter/orientation.cpp create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/Orientation.kt diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index fc09f379..4c365b90 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1326,6 +1326,26 @@ "@imageEditTitle": { "description": "Title of the image editor" }, + "imageEditToolbarColorLabel": "Color", + "@imageEditToolbarColorLabel": { + "description": "Label of the color tools. These can be used to adjust the color of an image" + }, + "imageEditToolbarTransformLabel": "Transform", + "@imageEditToolbarTransformLabel": { + "description": "Label of the transformation tools. These can be used to transform an image, e.g., rotate it" + }, + "imageEditTransformOrientation": "Orientation", + "@imageEditTransformOrientation": { + "description": "Change the orientation of the image, 90 degree per step" + }, + "imageEditTransformOrientationClockwise": "cw", + "@imageEditTransformOrientationClockwise": { + "description": "Indicate a clockwise rotation. This text must be short as there's only minimal space" + }, + "imageEditTransformOrientationCounterclockwise": "ccw", + "@imageEditTransformOrientationCounterclockwise": { + "description": "Indicate a counterclockwise rotation. This text must be short as there's only minimal space" + }, "categoriesLabel": "Categories", "searchLandingPeopleListEmptyText": "Press help to learn how to setup", "@searchLandingPeopleListEmptyText": { @@ -1414,6 +1434,7 @@ "@collectionPlacesLabel": { "description": "Browse photos grouped by place" }, + "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { "description": "Error message when server responds with HTTP401" diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 6b0aad57..d88ac539 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -129,6 +129,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -298,6 +303,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -348,6 +358,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -379,6 +394,11 @@ "settingsMemoriesRangeTitle", "settingsMemoriesRangeValueText", "rootPickerSkipConfirmationDialogContent2", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "showAllButtonLabel", "gpsPlaceText", "gpsPlaceAboutDialogTitle", @@ -391,6 +411,11 @@ "settingsPhotosPageTitle", "settingsMemoriesRangeTitle", "settingsMemoriesRangeValueText", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "gpsPlaceText", "gpsPlaceAboutDialogTitle", "gpsPlaceAboutDialogContent", @@ -447,6 +472,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -538,6 +568,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -608,6 +643,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -678,6 +718,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -748,6 +793,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", @@ -818,6 +868,11 @@ "imageEditColorWarmth", "imageEditColorTint", "imageEditTitle", + "imageEditToolbarColorLabel", + "imageEditToolbarTransformLabel", + "imageEditTransformOrientation", + "imageEditTransformOrientationClockwise", + "imageEditTransformOrientationCounterclockwise", "categoriesLabel", "searchLandingPeopleListEmptyText", "searchLandingCategoryVideosLabel", diff --git a/app/lib/widget/image_editor.dart b/app/lib/widget/image_editor.dart index e87f6fc7..bc5540d6 100644 --- a/app/lib/widget/image_editor.dart +++ b/app/lib/widget/image_editor.dart @@ -15,6 +15,7 @@ import 'package:nc_photos/theme.dart'; import 'package:nc_photos/url_launcher_util.dart'; import 'package:nc_photos/widget/handler/permission_handler.dart'; import 'package:nc_photos/widget/image_editor/color_toolbar.dart'; +import 'package:nc_photos/widget/image_editor/transform_toolbar.dart'; import 'package:nc_photos_plugin/nc_photos_plugin.dart'; class ImageEditorArguments { @@ -120,13 +121,24 @@ class _ImageEditorState extends State { ) : Container(), ), - ColorToolbar( - initialState: _colorFilters, - onActiveFiltersChanged: (colorFilters) { - _colorFilters = colorFilters.toList(); - _applyFilters(); - }, - ), + if (_activeTool == _ToolType.color) + ColorToolbar( + initialState: _colorFilters, + onActiveFiltersChanged: (colorFilters) { + _colorFilters = colorFilters.toList(); + _applyFilters(); + }, + ) + else if (_activeTool == _ToolType.transform) + TransformToolbar( + initialState: _transformFilters, + onActiveFiltersChanged: (transformFilters) { + _transformFilters = transformFilters.toList(); + _applyFilters(); + }, + ), + const SizedBox(height: 4), + _buildToolBar(context), ], ), ), @@ -156,6 +168,41 @@ class _ImageEditorState extends State { ], ); + Widget _buildToolBar(BuildContext context) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + const SizedBox(width: 16), + _ToolButton( + icon: Icons.palette_outlined, + label: L10n.global().imageEditToolbarColorLabel, + isSelected: _activeTool == _ToolType.color, + onPressed: () { + setState(() { + _activeTool = _ToolType.color; + }); + }, + ), + _ToolButton( + icon: Icons.transform_outlined, + label: L10n.global().imageEditToolbarTransformLabel, + isSelected: _activeTool == _ToolType.transform, + onPressed: () { + setState(() { + _activeTool = _ToolType.transform; + }); + }, + ), + const SizedBox(width: 16), + ], + ), + ), + ); + } + Future _onBackButton(BuildContext context) async { if (!_isModified) { Navigator.of(context).pop(); @@ -203,7 +250,10 @@ class _ImageEditorState extends State { } List _buildFilterList() { - return _colorFilters.map((f) => f.toImageFilter()).toList(); + return [ + ..._transformFilters.map((f) => f.toImageFilter()), + ..._colorFilters.map((f) => f.toImageFilter()), + ]; } Future _applyFilters() async { @@ -213,11 +263,79 @@ class _ImageEditorState extends State { }); } - bool get _isModified => _colorFilters.isNotEmpty; + bool get _isModified => + _transformFilters.isNotEmpty || _colorFilters.isNotEmpty; bool _isDoneInit = false; late final Rgba8Image _src; Rgba8Image? _dst; + var _activeTool = _ToolType.color; var _colorFilters = []; + var _transformFilters = []; +} + +enum _ToolType { + color, + transform, +} + +class _ToolButton extends StatelessWidget { + const _ToolButton({ + Key? key, + required this.icon, + required this.label, + required this.onPressed, + this.isSelected = false, + }) : super(key: key); + + @override + build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onPressed, + child: Container( + decoration: BoxDecoration( + color: isSelected ? Colors.white24 : null, + // borderRadius: const BorderRadius.all(Radius.circular(24)), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isSelected + ? Colors.white + : AppTheme.unfocusedIconColorDark, + size: 18, + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + color: isSelected + ? Colors.white + : AppTheme.unfocusedIconColorDark, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + final IconData icon; + final String label; + final VoidCallback? onPressed; + final bool isSelected; } diff --git a/app/lib/widget/image_editor/transform_toolbar.dart b/app/lib/widget/image_editor/transform_toolbar.dart new file mode 100644 index 00000000..f2fc1dc2 --- /dev/null +++ b/app/lib/widget/image_editor/transform_toolbar.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/theme.dart'; +import 'package:nc_photos/widget/image_editor/toolbar_button.dart'; +import 'package:nc_photos_plugin/nc_photos_plugin.dart'; + +enum TransformToolType { + orientation, +} + +abstract class TransformArguments { + ImageFilter toImageFilter(); + + TransformToolType _getToolType(); +} + +class TransformToolbar extends StatefulWidget { + const TransformToolbar({ + Key? key, + required this.initialState, + required this.onActiveFiltersChanged, + }) : super(key: key); + + @override + createState() => _TransformToolbarState(); + + final List initialState; + final ValueChanged> onActiveFiltersChanged; +} + +class _TransformToolbarState extends State { + @override + initState() { + super.initState(); + for (final s in widget.initialState) { + _filters[s._getToolType()] = s; + } + } + + @override + build(BuildContext context) => Column( + children: [ + _buildFilterOption(context), + _buildFilterBar(context), + ], + ); + + Widget _buildFilterOption(BuildContext context) { + Widget? child; + switch (_selectedFilter) { + case TransformToolType.orientation: + child = _buildOrientationOption(context); + break; + + case null: + child = null; + break; + } + return Container( + height: 80, + alignment: Alignment.bottomCenter, + child: child, + ); + } + + Widget _buildFilterBar(BuildContext context) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Material( + type: MaterialType.transparency, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + const SizedBox(width: 16), + ToolbarButton( + icon: Icons.rotate_90_degrees_ccw_outlined, + label: L10n.global().imageEditTransformOrientation, + onPressed: _onOrientationPressed, + isSelected: _selectedFilter == TransformToolType.orientation, + activationOrder: + _filters.containsKey(TransformToolType.orientation) + ? -1 + : null, + ), + const SizedBox(width: 16), + ], + ), + ), + ), + ); + } + + Widget _buildOrientationOption(BuildContext context) { + final value = + (_filters[TransformToolType.orientation] as _OrientationArguments) + .value; + return Padding( + padding: const EdgeInsets.all(8), + child: Wrap( + children: [ + Flex( + direction: Axis.horizontal, + children: [ + Flexible( + flex: 1, + fit: FlexFit.tight, + child: _OrientationButton( + label: + "180\n${L10n.global().imageEditTransformOrientationCounterclockwise}", + isSelected: value == 180, + onPressed: () => _onOrientationOptionPressed(180), + ), + ), + Flexible( + flex: 1, + fit: FlexFit.tight, + child: _OrientationButton( + label: + "90\n${L10n.global().imageEditTransformOrientationCounterclockwise}", + isSelected: value == 90, + onPressed: () => _onOrientationOptionPressed(90), + ), + ), + Flexible( + flex: 1, + fit: FlexFit.tight, + child: _OrientationButton( + label: "0\n ", + isSelected: value == 0, + onPressed: () => _onOrientationOptionPressed(0), + ), + ), + Flexible( + flex: 1, + fit: FlexFit.tight, + child: _OrientationButton( + label: + "90\n${L10n.global().imageEditTransformOrientationClockwise}", + isSelected: value == -90, + onPressed: () => _onOrientationOptionPressed(-90), + ), + ), + Flexible( + flex: 1, + fit: FlexFit.tight, + child: _OrientationButton( + label: + "180\n${L10n.global().imageEditTransformOrientationClockwise}", + isSelected: value == -180, + onPressed: () => _onOrientationOptionPressed(-180), + ), + ), + ], + ), + ], + ), + ); + } + + void _onFilterPressed(TransformToolType type, TransformArguments defArgs) { + if (_selectedFilter == type) { + // deactivate filter + setState(() { + _selectedFilter = null; + _filters.remove(type); + }); + } else { + setState(() { + _selectedFilter = type; + _filters[type] ??= defArgs; + }); + } + _notifyFiltersChanged(); + } + + void _onOrientationPressed() => _onFilterPressed( + TransformToolType.orientation, const _OrientationArguments(0)); + + void _onOrientationOptionPressed(int value) { + setState(() { + _filters[TransformToolType.orientation] = _OrientationArguments(value); + }); + _notifyFiltersChanged(); + } + + void _notifyFiltersChanged() { + widget.onActiveFiltersChanged.call(_filters.values); + } + + final _filters = {}; + TransformToolType? _selectedFilter; +} + +class _OrientationArguments implements TransformArguments { + const _OrientationArguments(this.value); + + @override + toImageFilter() => TransformOrientationFilter(value); + + @override + _getToolType() => TransformToolType.orientation; + + final int value; +} + +class _OrientationButton extends StatelessWidget { + const _OrientationButton({ + Key? key, + required this.label, + required this.onPressed, + this.isSelected = false, + }) : super(key: key); + + @override + build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onPressed, + child: Container( + decoration: BoxDecoration( + color: isSelected + ? AppTheme.primarySwatchDark[500]!.withOpacity(0.7) + : null, + // borderRadius: const BorderRadius.all(Radius.circular(24)), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + alignment: Alignment.center, + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? Colors.white + : AppTheme.unfocusedIconColorDark, + ), + ), + ), + ), + ), + ), + ); + } + + final String label; + final VoidCallback? onPressed; + final bool isSelected; +} diff --git a/plugin/android/src/main/cpp/CMakeLists.txt b/plugin/android/src/main/cpp/CMakeLists.txt index 432f59c1..66150d92 100644 --- a/plugin/android/src/main/cpp/CMakeLists.txt +++ b/plugin/android/src/main/cpp/CMakeLists.txt @@ -38,6 +38,7 @@ add_library( # Sets the name of the library. filter/contrast.cpp filter/curve.cpp filter/hslhsv.cpp + filter/orientation.cpp filter/saturation.cpp filter/tint.cpp filter/warmth.cpp diff --git a/plugin/android/src/main/cpp/filter/orientation.cpp b/plugin/android/src/main/cpp/filter/orientation.cpp new file mode 100644 index 00000000..f28f365b --- /dev/null +++ b/plugin/android/src/main/cpp/filter/orientation.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include + +#include "../exception.h" +#include "../log.h" +#include "../util.h" + +using namespace plugin; +using namespace std; + +namespace { + +class Orientation { +public: + std::vector apply(const uint8_t *rgba8, const size_t width, + const size_t height, const int degree); + +private: + std::vector apply90Ccw(const uint8_t *rgba8, const size_t width, + const size_t height); + std::vector apply90Cw(const uint8_t *rgba8, const size_t width, + const size_t height); + std::vector apply180(const uint8_t *rgba8, const size_t width, + const size_t height); + + static constexpr const char *TAG = "Orientation"; +}; + +} // namespace + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_com_nkming_nc_1photos_plugin_image_1processor_Orientation_applyNative( + JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height, + jint degree) { + try { + initOpenMp(); + RaiiContainer cRgba8( + [&]() { return env->GetByteArrayElements(rgba8, nullptr); }, + [&](jbyte *obj) { + env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT); + }); + const auto result = Orientation().apply( + reinterpret_cast(cRgba8.get()), width, height, degree); + auto resultAry = env->NewByteArray(result.size()); + env->SetByteArrayRegion(resultAry, 0, result.size(), + reinterpret_cast(result.data())); + return resultAry; + } catch (const exception &e) { + throwJavaException(env, e.what()); + return nullptr; + } +} + +namespace { + +vector Orientation::apply(const uint8_t *rgba8, const size_t width, + const size_t height, const int degree) { + LOGI(TAG, "[apply] degree: %d", degree); + if (degree == 0) { + // shortcut + return vector(rgba8, rgba8 + width * height * 4); + } + + if (degree == 90) { + return apply90Ccw(rgba8, width, height); + } else if (degree == -90) { + return apply90Cw(rgba8, width, height); + } else { + return apply180(rgba8, width, height); + } +} + +vector Orientation::apply90Ccw(const uint8_t *rgba8, + const size_t width, + const size_t height) { + vector output(width * height * 4); +#pragma omp parallel for + for (size_t y = 0; y < height; ++y) { + const auto yI = y * width * 4; + for (size_t x = 0; x < width; ++x) { + const auto p = x * 4 + yI; + const auto desY = width - x - 1; + const auto desX = y; + const auto desP = (desY * height + desX) * 4; + output[desP + 0] = rgba8[p + 0]; + output[desP + 1] = rgba8[p + 1]; + output[desP + 2] = rgba8[p + 2]; + output[desP + 3] = rgba8[p + 3]; + } + } + return output; +} + +vector Orientation::apply90Cw(const uint8_t *rgba8, const size_t width, + const size_t height) { + vector output(width * height * 4); +#pragma omp parallel for + for (size_t y = 0; y < height; ++y) { + const auto yI = y * width * 4; + for (size_t x = 0; x < width; ++x) { + const auto p = x * 4 + yI; + const auto desY = width - x - 1; + const auto desX = height - y - 1; + const auto desP = (desY * height + desX) * 4; + output[desP + 0] = rgba8[p + 0]; + output[desP + 1] = rgba8[p + 1]; + output[desP + 2] = rgba8[p + 2]; + output[desP + 3] = rgba8[p + 3]; + } + } + return output; +} + +vector Orientation::apply180(const uint8_t *rgba8, const size_t width, + const size_t height) { + vector output(width * height * 4); +#pragma omp parallel for + for (size_t y = 0; y < height; ++y) { + const auto yI = y * width * 4; + for (size_t x = 0; x < width; ++x) { + const auto p = x * 4 + yI; + const auto desY = height - y - 1; + const auto desX = width - x - 1; + const auto desP = (desY * width + desX) * 4; + output[desP + 0] = rgba8[p + 0]; + output[desP + 1] = rgba8[p + 1]; + output[desP + 2] = rgba8[p + 2]; + output[desP + 3] = rgba8[p + 3]; + } + } + return output; +} + +} // namespace diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt index 30c85dce..7ca1aeb9 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt @@ -237,6 +237,7 @@ interface ImageFilter { "saturation" -> Saturation((json["weight"] as Double).toFloat()) "warmth" -> Warmth((json["weight"] as Double).toFloat()) "tint" -> Tint((json["weight"] as Double).toFloat()) + "orientation" -> Orientation(json["degree"] as Int) else -> throw IllegalArgumentException( "Unknown type: ${json["type"]}" ) diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/Orientation.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/Orientation.kt new file mode 100644 index 00000000..86d87e79 --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/Orientation.kt @@ -0,0 +1,18 @@ +package com.nkming.nc_photos.plugin.image_processor + +import com.nkming.nc_photos.plugin.ImageFilter +import kotlin.math.abs + +class Orientation(val degree: Int) : ImageFilter { + override fun apply(rgba8: Rgba8Image): Rgba8Image { + val data = applyNative(rgba8.pixel, rgba8.width, rgba8.height, degree) + return Rgba8Image( + data, if (abs(degree) == 90) rgba8.height else rgba8.width, + if (abs(degree) == 90) rgba8.width else rgba8.height + ) + } + + private external fun applyNative( + rgba8: ByteArray, width: Int, height: Int, degree: Int + ): ByteArray +} diff --git a/plugin/lib/src/image_processor.dart b/plugin/lib/src/image_processor.dart index 6f6e61e6..0bb00332 100644 --- a/plugin/lib/src/image_processor.dart +++ b/plugin/lib/src/image_processor.dart @@ -44,6 +44,18 @@ class ColorTintFilter extends _SingleWeightFilter { const ColorTintFilter(double weight) : super("tint", weight); } +class TransformOrientationFilter implements ImageFilter { + const TransformOrientationFilter(this.degree); + + @override + toJson() => { + "type": "orientation", + "degree": degree, + }; + + final int degree; +} + class ImageProcessor { static Future zeroDce( String fileUrl,