mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Change orientation of an image in editor
This commit is contained in:
parent
08f372d691
commit
7ffe4a55b9
9 changed files with 624 additions and 9 deletions
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ImageEditor> {
|
|||
)
|
||||
: 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<ImageEditor> {
|
|||
],
|
||||
);
|
||||
|
||||
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<void> _onBackButton(BuildContext context) async {
|
||||
if (!_isModified) {
|
||||
Navigator.of(context).pop();
|
||||
|
@ -203,7 +250,10 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
}
|
||||
|
||||
List<ImageFilter> _buildFilterList() {
|
||||
return _colorFilters.map((f) => f.toImageFilter()).toList();
|
||||
return [
|
||||
..._transformFilters.map((f) => f.toImageFilter()),
|
||||
..._colorFilters.map((f) => f.toImageFilter()),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _applyFilters() async {
|
||||
|
@ -213,11 +263,79 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
});
|
||||
}
|
||||
|
||||
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 = <ColorArguments>[];
|
||||
var _transformFilters = <TransformArguments>[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
253
app/lib/widget/image_editor/transform_toolbar.dart
Normal file
253
app/lib/widget/image_editor/transform_toolbar.dart
Normal file
|
@ -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<TransformArguments> initialState;
|
||||
final ValueChanged<Iterable<TransformArguments>> onActiveFiltersChanged;
|
||||
}
|
||||
|
||||
class _TransformToolbarState extends State<TransformToolbar> {
|
||||
@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, TransformArguments>{};
|
||||
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;
|
||||
}
|
|
@ -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
|
||||
|
|
136
plugin/android/src/main/cpp/filter/orientation.cpp
Normal file
136
plugin/android/src/main/cpp/filter/orientation.cpp
Normal file
|
@ -0,0 +1,136 @@
|
|||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../util.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
class Orientation {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const int degree);
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> apply90Ccw(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height);
|
||||
std::vector<uint8_t> apply90Cw(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height);
|
||||
std::vector<uint8_t> 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<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = Orientation().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, degree);
|
||||
auto resultAry = env->NewByteArray(result.size());
|
||||
env->SetByteArrayRegion(resultAry, 0, result.size(),
|
||||
reinterpret_cast<const int8_t *>(result.data()));
|
||||
return resultAry;
|
||||
} catch (const exception &e) {
|
||||
throwJavaException(env, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
vector<uint8_t> 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<uint8_t>(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<uint8_t> Orientation::apply90Ccw(const uint8_t *rgba8,
|
||||
const size_t width,
|
||||
const size_t height) {
|
||||
vector<uint8_t> 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<uint8_t> Orientation::apply90Cw(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height) {
|
||||
vector<uint8_t> 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<uint8_t> Orientation::apply180(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height) {
|
||||
vector<uint8_t> 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
|
|
@ -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"]}"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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<void> zeroDce(
|
||||
String fileUrl,
|
||||
|
|
Loading…
Reference in a new issue