mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +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": {
|
"@imageEditTitle": {
|
||||||
"description": "Title of the image editor"
|
"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",
|
"categoriesLabel": "Categories",
|
||||||
"searchLandingPeopleListEmptyText": "Press help to learn how to setup",
|
"searchLandingPeopleListEmptyText": "Press help to learn how to setup",
|
||||||
"@searchLandingPeopleListEmptyText": {
|
"@searchLandingPeopleListEmptyText": {
|
||||||
|
@ -1414,6 +1434,7 @@
|
||||||
"@collectionPlacesLabel": {
|
"@collectionPlacesLabel": {
|
||||||
"description": "Browse photos grouped by place"
|
"description": "Browse photos grouped by place"
|
||||||
},
|
},
|
||||||
|
|
||||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||||
"@errorUnauthenticated": {
|
"@errorUnauthenticated": {
|
||||||
"description": "Error message when server responds with HTTP401"
|
"description": "Error message when server responds with HTTP401"
|
||||||
|
|
|
@ -129,6 +129,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -298,6 +303,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -348,6 +358,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -379,6 +394,11 @@
|
||||||
"settingsMemoriesRangeTitle",
|
"settingsMemoriesRangeTitle",
|
||||||
"settingsMemoriesRangeValueText",
|
"settingsMemoriesRangeValueText",
|
||||||
"rootPickerSkipConfirmationDialogContent2",
|
"rootPickerSkipConfirmationDialogContent2",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"showAllButtonLabel",
|
"showAllButtonLabel",
|
||||||
"gpsPlaceText",
|
"gpsPlaceText",
|
||||||
"gpsPlaceAboutDialogTitle",
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
@ -391,6 +411,11 @@
|
||||||
"settingsPhotosPageTitle",
|
"settingsPhotosPageTitle",
|
||||||
"settingsMemoriesRangeTitle",
|
"settingsMemoriesRangeTitle",
|
||||||
"settingsMemoriesRangeValueText",
|
"settingsMemoriesRangeValueText",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"gpsPlaceText",
|
"gpsPlaceText",
|
||||||
"gpsPlaceAboutDialogTitle",
|
"gpsPlaceAboutDialogTitle",
|
||||||
"gpsPlaceAboutDialogContent",
|
"gpsPlaceAboutDialogContent",
|
||||||
|
@ -447,6 +472,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -538,6 +568,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -608,6 +643,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -678,6 +718,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -748,6 +793,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -818,6 +868,11 @@
|
||||||
"imageEditColorWarmth",
|
"imageEditColorWarmth",
|
||||||
"imageEditColorTint",
|
"imageEditColorTint",
|
||||||
"imageEditTitle",
|
"imageEditTitle",
|
||||||
|
"imageEditToolbarColorLabel",
|
||||||
|
"imageEditToolbarTransformLabel",
|
||||||
|
"imageEditTransformOrientation",
|
||||||
|
"imageEditTransformOrientationClockwise",
|
||||||
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
|
|
@ -15,6 +15,7 @@ import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/url_launcher_util.dart';
|
import 'package:nc_photos/url_launcher_util.dart';
|
||||||
import 'package:nc_photos/widget/handler/permission_handler.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/color_toolbar.dart';
|
||||||
|
import 'package:nc_photos/widget/image_editor/transform_toolbar.dart';
|
||||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
|
||||||
class ImageEditorArguments {
|
class ImageEditorArguments {
|
||||||
|
@ -120,13 +121,24 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
),
|
),
|
||||||
ColorToolbar(
|
if (_activeTool == _ToolType.color)
|
||||||
initialState: _colorFilters,
|
ColorToolbar(
|
||||||
onActiveFiltersChanged: (colorFilters) {
|
initialState: _colorFilters,
|
||||||
_colorFilters = colorFilters.toList();
|
onActiveFiltersChanged: (colorFilters) {
|
||||||
_applyFilters();
|
_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 {
|
Future<void> _onBackButton(BuildContext context) async {
|
||||||
if (!_isModified) {
|
if (!_isModified) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -203,7 +250,10 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ImageFilter> _buildFilterList() {
|
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 {
|
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;
|
bool _isDoneInit = false;
|
||||||
late final Rgba8Image _src;
|
late final Rgba8Image _src;
|
||||||
Rgba8Image? _dst;
|
Rgba8Image? _dst;
|
||||||
|
var _activeTool = _ToolType.color;
|
||||||
|
|
||||||
var _colorFilters = <ColorArguments>[];
|
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/contrast.cpp
|
||||||
filter/curve.cpp
|
filter/curve.cpp
|
||||||
filter/hslhsv.cpp
|
filter/hslhsv.cpp
|
||||||
|
filter/orientation.cpp
|
||||||
filter/saturation.cpp
|
filter/saturation.cpp
|
||||||
filter/tint.cpp
|
filter/tint.cpp
|
||||||
filter/warmth.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())
|
"saturation" -> Saturation((json["weight"] as Double).toFloat())
|
||||||
"warmth" -> Warmth((json["weight"] as Double).toFloat())
|
"warmth" -> Warmth((json["weight"] as Double).toFloat())
|
||||||
"tint" -> Tint((json["weight"] as Double).toFloat())
|
"tint" -> Tint((json["weight"] as Double).toFloat())
|
||||||
|
"orientation" -> Orientation(json["degree"] as Int)
|
||||||
else -> throw IllegalArgumentException(
|
else -> throw IllegalArgumentException(
|
||||||
"Unknown type: ${json["type"]}"
|
"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);
|
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 {
|
class ImageProcessor {
|
||||||
static Future<void> zeroDce(
|
static Future<void> zeroDce(
|
||||||
String fileUrl,
|
String fileUrl,
|
||||||
|
|
Loading…
Reference in a new issue