mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-04 22:38:51 +01:00
Crop an image in editor
This commit is contained in:
parent
48d3b75607
commit
75d05e767e
10 changed files with 710 additions and 14 deletions
|
@ -1346,6 +1346,10 @@
|
||||||
"@imageEditTransformOrientationCounterclockwise": {
|
"@imageEditTransformOrientationCounterclockwise": {
|
||||||
"description": "Indicate a counterclockwise rotation. This text must be short as there's only minimal space"
|
"description": "Indicate a counterclockwise rotation. This text must be short as there's only minimal space"
|
||||||
},
|
},
|
||||||
|
"imageEditTransformCrop": "Crop",
|
||||||
|
"@imageEditTransformCrop": {
|
||||||
|
"description": "Crop the image"
|
||||||
|
},
|
||||||
"categoriesLabel": "Categories",
|
"categoriesLabel": "Categories",
|
||||||
"searchLandingPeopleListEmptyText": "Press help to learn how to setup",
|
"searchLandingPeopleListEmptyText": "Press help to learn how to setup",
|
||||||
"@searchLandingPeopleListEmptyText": {
|
"@searchLandingPeopleListEmptyText": {
|
||||||
|
|
|
@ -134,6 +134,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -308,6 +309,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -363,6 +365,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -399,6 +402,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"showAllButtonLabel",
|
"showAllButtonLabel",
|
||||||
"gpsPlaceText",
|
"gpsPlaceText",
|
||||||
"gpsPlaceAboutDialogTitle",
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
@ -416,6 +420,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"gpsPlaceText",
|
"gpsPlaceText",
|
||||||
"gpsPlaceAboutDialogTitle",
|
"gpsPlaceAboutDialogTitle",
|
||||||
"gpsPlaceAboutDialogContent",
|
"gpsPlaceAboutDialogContent",
|
||||||
|
@ -477,6 +482,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -573,6 +579,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -648,6 +655,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -723,6 +731,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -798,6 +807,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
@ -873,6 +883,7 @@
|
||||||
"imageEditTransformOrientation",
|
"imageEditTransformOrientation",
|
||||||
"imageEditTransformOrientationClockwise",
|
"imageEditTransformOrientationClockwise",
|
||||||
"imageEditTransformOrientationCounterclockwise",
|
"imageEditTransformOrientationCounterclockwise",
|
||||||
|
"imageEditTransformCrop",
|
||||||
"categoriesLabel",
|
"categoriesLabel",
|
||||||
"searchLandingPeopleListEmptyText",
|
"searchLandingPeopleListEmptyText",
|
||||||
"searchLandingCategoryVideosLabel",
|
"searchLandingCategoryVideosLabel",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api.dart';
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
@ -15,6 +16,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/crop_controller.dart';
|
||||||
import 'package:nc_photos/widget/image_editor/transform_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';
|
||||||
|
|
||||||
|
@ -113,12 +115,23 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
_buildAppBar(context),
|
_buildAppBar(context),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _isDoneInit
|
child: _isDoneInit
|
||||||
? Image(
|
? _isCropMode
|
||||||
image: (_dst ?? _src).run((obj) =>
|
? CropController(
|
||||||
PixelImage(obj.pixel, obj.width, obj.height)),
|
// crop always work on the src, otherwise we'll be
|
||||||
fit: BoxFit.contain,
|
// cropping repeatedly
|
||||||
gaplessPlayback: true,
|
image: _src,
|
||||||
)
|
initialState: _cropFilter,
|
||||||
|
onCropChanged: (cropFilter) {
|
||||||
|
_cropFilter = cropFilter;
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Image(
|
||||||
|
image: (_dst ?? _src).run((obj) =>
|
||||||
|
PixelImage(obj.pixel, obj.width, obj.height)),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
),
|
),
|
||||||
if (_activeTool == _ToolType.color)
|
if (_activeTool == _ToolType.color)
|
||||||
|
@ -136,6 +149,15 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
_transformFilters = transformFilters.toList();
|
_transformFilters = transformFilters.toList();
|
||||||
_applyFilters();
|
_applyFilters();
|
||||||
},
|
},
|
||||||
|
isCropModeChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_isCropMode = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCropToolDeactivated: () {
|
||||||
|
_cropFilter = null;
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
_buildToolBar(context),
|
_buildToolBar(context),
|
||||||
|
@ -182,7 +204,7 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
isSelected: _activeTool == _ToolType.color,
|
isSelected: _activeTool == _ToolType.color,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_activeTool = _ToolType.color;
|
_setActiveTool(_ToolType.color);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -192,7 +214,7 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
isSelected: _activeTool == _ToolType.transform,
|
isSelected: _activeTool == _ToolType.transform,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_activeTool = _ToolType.transform;
|
_setActiveTool(_ToolType.transform);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -249,9 +271,15 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setActiveTool(_ToolType tool) {
|
||||||
|
_activeTool = tool;
|
||||||
|
_isCropMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
List<ImageFilter> _buildFilterList() {
|
List<ImageFilter> _buildFilterList() {
|
||||||
return [
|
return [
|
||||||
..._transformFilters.map((f) => f.toImageFilter()),
|
if (_cropFilter != null) _cropFilter!.toImageFilter()!,
|
||||||
|
..._transformFilters.map((f) => f.toImageFilter()).whereNotNull(),
|
||||||
..._colorFilters.map((f) => f.toImageFilter()),
|
..._colorFilters.map((f) => f.toImageFilter()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -264,15 +292,19 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isModified =>
|
bool get _isModified =>
|
||||||
_transformFilters.isNotEmpty || _colorFilters.isNotEmpty;
|
_cropFilter != null ||
|
||||||
|
_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 _activeTool = _ToolType.color;
|
||||||
|
var _isCropMode = false;
|
||||||
|
|
||||||
var _colorFilters = <ColorArguments>[];
|
var _colorFilters = <ColorArguments>[];
|
||||||
var _transformFilters = <TransformArguments>[];
|
var _transformFilters = <TransformArguments>[];
|
||||||
|
TransformArguments? _cropFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _ToolType {
|
enum _ToolType {
|
||||||
|
|
494
app/lib/widget/image_editor/crop_controller.dart
Normal file
494
app/lib/widget/image_editor/crop_controller.dart
Normal file
|
@ -0,0 +1,494 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/pixel_image_provider.dart';
|
||||||
|
import 'package:nc_photos/widget/image_editor/transform_toolbar.dart';
|
||||||
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
|
||||||
|
/// Crop editor
|
||||||
|
///
|
||||||
|
/// This widget only work when width == device width!
|
||||||
|
class CropController extends StatefulWidget {
|
||||||
|
const CropController({
|
||||||
|
Key? key,
|
||||||
|
required this.image,
|
||||||
|
required this.initialState,
|
||||||
|
this.onCropChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _CropControllerState();
|
||||||
|
|
||||||
|
final Rgba8Image image;
|
||||||
|
final TransformArguments? initialState;
|
||||||
|
final ValueChanged<TransformArguments>? onCropChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CropControllerState extends State<CropController> {
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialState?.getToolType() == TransformToolType.crop) {
|
||||||
|
_initialState = widget.initialState as _CropArguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return OrientationBuilder(
|
||||||
|
builder: (context, orientation) {
|
||||||
|
_prevOrientation ??= orientation;
|
||||||
|
if (_prevOrientation != orientation) {
|
||||||
|
_onOrientationChanged(orientation);
|
||||||
|
} else {
|
||||||
|
_tryUpdateSize(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: .35,
|
||||||
|
child: Image(
|
||||||
|
image: PixelImage(widget.image.pixel, widget.image.width,
|
||||||
|
widget.image.height),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onPanStart: (_) {
|
||||||
|
_canMoveRect = true;
|
||||||
|
},
|
||||||
|
onPanUpdate: (details) {
|
||||||
|
if (!_canMoveRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
if (_size == null) return;
|
||||||
|
final pos = details.localPosition;
|
||||||
|
if (pos.dx > 0 &&
|
||||||
|
pos.dx < _size!.width &&
|
||||||
|
pos.dy > _offsetY &&
|
||||||
|
pos.dy < _size!.height + _offsetY) {
|
||||||
|
_moveRectBy(details.delta);
|
||||||
|
} else {
|
||||||
|
_canMoveRect = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
widget.onCropChanged?.call(_getCropArgs());
|
||||||
|
},
|
||||||
|
child: ClipRect(
|
||||||
|
clipper: _CropClipper(
|
||||||
|
_left,
|
||||||
|
_top + _offsetY,
|
||||||
|
_size == null ? double.infinity : _size!.width - _right,
|
||||||
|
_size == null
|
||||||
|
? double.infinity
|
||||||
|
: _size!.height - _bottom + _offsetY,
|
||||||
|
),
|
||||||
|
child: Image(
|
||||||
|
image: PixelImage(widget.image.pixel, widget.image.width,
|
||||||
|
widget.image.height),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_size != null) ...[
|
||||||
|
Positioned(
|
||||||
|
top: _top + _offsetY,
|
||||||
|
left: _left,
|
||||||
|
bottom: _bottom + _offsetY,
|
||||||
|
right: _right,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: _top + _offsetY,
|
||||||
|
left: _left,
|
||||||
|
child: GestureDetector(
|
||||||
|
onPanStart: (_) {
|
||||||
|
_topDrain.reset();
|
||||||
|
_leftDrain.reset();
|
||||||
|
},
|
||||||
|
onPanUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
if (_size == null) return;
|
||||||
|
_moveTopByDy(details.delta.dy);
|
||||||
|
_moveLeftByDx(details.delta.dx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
widget.onCropChanged?.call(_getCropArgs());
|
||||||
|
},
|
||||||
|
child: const _TouchDot(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: _top + _offsetY,
|
||||||
|
right: _right,
|
||||||
|
child: GestureDetector(
|
||||||
|
onPanStart: (_) {
|
||||||
|
_topDrain.reset();
|
||||||
|
_rightDrain.reset();
|
||||||
|
},
|
||||||
|
onPanUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
if (_size == null) return;
|
||||||
|
_moveTopByDy(details.delta.dy);
|
||||||
|
_moveRightByDx(details.delta.dx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
widget.onCropChanged?.call(_getCropArgs());
|
||||||
|
},
|
||||||
|
child: const _TouchDot(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: _bottom + _offsetY,
|
||||||
|
left: _left,
|
||||||
|
child: GestureDetector(
|
||||||
|
onPanStart: (_) {
|
||||||
|
_bottomDrain.reset();
|
||||||
|
_leftDrain.reset();
|
||||||
|
},
|
||||||
|
onPanUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
if (_size == null) return;
|
||||||
|
_moveBottomByDy(details.delta.dy);
|
||||||
|
_moveLeftByDx(details.delta.dx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
widget.onCropChanged?.call(_getCropArgs());
|
||||||
|
},
|
||||||
|
child: const _TouchDot(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: _bottom + _offsetY,
|
||||||
|
right: _right,
|
||||||
|
child: GestureDetector(
|
||||||
|
onPanStart: (_) {
|
||||||
|
_bottomDrain.reset();
|
||||||
|
_rightDrain.reset();
|
||||||
|
},
|
||||||
|
onPanUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
if (_size == null) return;
|
||||||
|
_moveBottomByDy(details.delta.dy);
|
||||||
|
_moveRightByDx(details.delta.dx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
widget.onCropChanged?.call(_getCropArgs());
|
||||||
|
},
|
||||||
|
child: const _TouchDot(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onOrientationChanged(Orientation orientation) {
|
||||||
|
_reset();
|
||||||
|
_prevOrientation = orientation;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryUpdateSize(BuildContext context) {
|
||||||
|
if (_size == null) {
|
||||||
|
final renderObj = context.findRenderObject() as RenderBox?;
|
||||||
|
if (renderObj?.hasSize == true && renderObj!.size.width > 16) {
|
||||||
|
// the renderbox height is always max
|
||||||
|
if (renderObj.size.width == MediaQuery.of(context).size.width) {
|
||||||
|
final height =
|
||||||
|
renderObj.size.width / widget.image.width * widget.image.height;
|
||||||
|
_size = Size(renderObj.size.width, height);
|
||||||
|
_offsetY = (renderObj.size.height - height) / 2;
|
||||||
|
} else {
|
||||||
|
_size = renderObj.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_log.info("[_tryUpdateSize] size = $_size, offsetY: $_offsetY");
|
||||||
|
if (_size == null) {
|
||||||
|
_log.info("[_tryUpdateSize] Schedule next");
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// apply initial state after getting size
|
||||||
|
if (_initialState != null && !_isInitialRestored) {
|
||||||
|
_restoreCropArgs(_initialState!);
|
||||||
|
_isInitialRestored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _moveTopByDy(double dy) {
|
||||||
|
if (_topDrain.isAvailable) {
|
||||||
|
dy = _topDrain.consume(dy);
|
||||||
|
}
|
||||||
|
// add distance outside of the view to drain
|
||||||
|
_topDrain.add(_addTop(dy));
|
||||||
|
}
|
||||||
|
|
||||||
|
double _addTop(double dy) {
|
||||||
|
final old = _top;
|
||||||
|
final upper = _size!.height - _bottom - _TouchDot.size * 2 - _threshold;
|
||||||
|
// ignore if image is too small to allow cropping in this axis
|
||||||
|
if (upper >= _top) {
|
||||||
|
_top = (_top + dy).clamp(0, upper);
|
||||||
|
} else if (dy < 0 && _size!.height - _bottom >= 0) {
|
||||||
|
// allow expanding only
|
||||||
|
_top = math.max(_top + dy, 0);
|
||||||
|
}
|
||||||
|
return (old + dy) - _top;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _moveLeftByDx(double dx) {
|
||||||
|
if (_leftDrain.isAvailable) {
|
||||||
|
dx = _leftDrain.consume(dx);
|
||||||
|
}
|
||||||
|
_leftDrain.add(_addLeft(dx));
|
||||||
|
}
|
||||||
|
|
||||||
|
double _addLeft(double dx) {
|
||||||
|
final old = _left;
|
||||||
|
final upper = _size!.width - _right - _TouchDot.size * 2 - _threshold;
|
||||||
|
if (upper >= _left) {
|
||||||
|
_left = (_left + dx).clamp(0, upper);
|
||||||
|
} else if (dx < 0 && _size!.width - _right >= 0) {
|
||||||
|
_left = math.max(_left + dx, 0);
|
||||||
|
}
|
||||||
|
return (old + dx) - _left;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _moveBottomByDy(double dy) {
|
||||||
|
if (_bottomDrain.isAvailable) {
|
||||||
|
dy = _bottomDrain.consume(dy);
|
||||||
|
}
|
||||||
|
_bottomDrain.add(_addBottom(dy));
|
||||||
|
}
|
||||||
|
|
||||||
|
double _addBottom(double dy) {
|
||||||
|
final old = _bottom;
|
||||||
|
final upper = _size!.height - _top - _TouchDot.size * 2 - _threshold;
|
||||||
|
if (upper >= _bottom) {
|
||||||
|
_bottom = (_bottom - dy).clamp(0, upper);
|
||||||
|
} else if (dy > 0 && _size!.height - _top >= 0) {
|
||||||
|
_bottom = math.max(_bottom - dy, 0);
|
||||||
|
}
|
||||||
|
return _bottom - (old - dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _moveRightByDx(double dx) {
|
||||||
|
if (_rightDrain.isAvailable) {
|
||||||
|
dx = _rightDrain.consume(dx);
|
||||||
|
}
|
||||||
|
_rightDrain.add(_addRight(dx));
|
||||||
|
}
|
||||||
|
|
||||||
|
double _addRight(double dx) {
|
||||||
|
final old = _right;
|
||||||
|
final upper = _size!.width - _left - _TouchDot.size * 2 - _threshold;
|
||||||
|
if (upper >= _right) {
|
||||||
|
_right = (_right - dx).clamp(0, upper);
|
||||||
|
} else if (dx > 0 && _size!.width - _left >= 0) {
|
||||||
|
_right = math.max(_right - dx, 0);
|
||||||
|
}
|
||||||
|
return _right - (old - dx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _moveRectBy(Offset offset) {
|
||||||
|
if (offset.dy < 0) {
|
||||||
|
// up
|
||||||
|
final actual = math.min(_top, -offset.dy);
|
||||||
|
_top -= actual;
|
||||||
|
_bottom += actual;
|
||||||
|
} else {
|
||||||
|
// down
|
||||||
|
final actual = math.min(_bottom, offset.dy);
|
||||||
|
_top += actual;
|
||||||
|
_bottom -= actual;
|
||||||
|
}
|
||||||
|
if (offset.dx < 0) {
|
||||||
|
// left
|
||||||
|
final actual = math.min(_left, -offset.dx);
|
||||||
|
_left -= actual;
|
||||||
|
_right += actual;
|
||||||
|
} else {
|
||||||
|
// right
|
||||||
|
final actual = math.min(_right, offset.dx);
|
||||||
|
_left += actual;
|
||||||
|
_right -= actual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_CropArguments _getCropArgs() {
|
||||||
|
final topPercent = _top / _size!.height;
|
||||||
|
final leftPercent = _left / _size!.width;
|
||||||
|
final bottomPercent = (_size!.height - _bottom) / _size!.height;
|
||||||
|
final rightPercent = (_size!.width - _right) / _size!.width;
|
||||||
|
return _CropArguments(topPercent, leftPercent, bottomPercent, rightPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restoreCropArgs(_CropArguments args) {
|
||||||
|
_top = args.top * _size!.height;
|
||||||
|
_left = args.left * _size!.width;
|
||||||
|
_bottom = _size!.height - args.bottom * _size!.height;
|
||||||
|
_right = _size!.width - args.right * _size!.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset state after orientation change
|
||||||
|
void _reset() {
|
||||||
|
_log.info("[reset] Reset state");
|
||||||
|
if (_initialState != null) {
|
||||||
|
// this is needed to also reset the state of the observer
|
||||||
|
widget.onCropChanged?.call(_initialState!);
|
||||||
|
}
|
||||||
|
_isInitialRestored = false;
|
||||||
|
_size = null;
|
||||||
|
_offsetY = 0;
|
||||||
|
_top = 0;
|
||||||
|
_left = 0;
|
||||||
|
_bottom = 0;
|
||||||
|
_right = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_CropArguments? _initialState;
|
||||||
|
bool _isInitialRestored = false;
|
||||||
|
Size? _size;
|
||||||
|
double _offsetY = 0;
|
||||||
|
|
||||||
|
var _top = 0.0;
|
||||||
|
final _topDrain = _Drain();
|
||||||
|
var _left = 0.0;
|
||||||
|
final _leftDrain = _Drain();
|
||||||
|
var _bottom = 0.0;
|
||||||
|
final _bottomDrain = _Drain();
|
||||||
|
var _right = 0.0;
|
||||||
|
final _rightDrain = _Drain();
|
||||||
|
// set this to false when pointer moved outside of the area, making user to
|
||||||
|
// start a new pan session to move the rect
|
||||||
|
var _canMoveRect = true;
|
||||||
|
|
||||||
|
Orientation? _prevOrientation;
|
||||||
|
|
||||||
|
static const _threshold = 24;
|
||||||
|
static final _log =
|
||||||
|
Logger("widget.image_editor.crop_controller._CropControllerState");
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TouchDot extends StatelessWidget {
|
||||||
|
static const double size = 24;
|
||||||
|
|
||||||
|
const _TouchDot({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
color: Colors.white60,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CropClipper extends CustomClipper<Rect> {
|
||||||
|
const _CropClipper(this.left, this.top, this.right, this.bottom);
|
||||||
|
|
||||||
|
@override
|
||||||
|
getClip(Size size) => Rect.fromLTRB(left, top, right, bottom);
|
||||||
|
|
||||||
|
@override
|
||||||
|
shouldReclip(CustomClipper oldClipper) {
|
||||||
|
if (oldClipper is! _CropClipper) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return left != oldClipper.left ||
|
||||||
|
top != oldClipper.top ||
|
||||||
|
right != oldClipper.right ||
|
||||||
|
bottom != oldClipper.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double left;
|
||||||
|
final double top;
|
||||||
|
final double right;
|
||||||
|
final double bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store exceeding values and consume them if needed
|
||||||
|
class _Drain {
|
||||||
|
void add(double v) {
|
||||||
|
_drain += v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_drain = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume by [v], and return whatever that remain in [v]
|
||||||
|
double consume(double v) {
|
||||||
|
if (_drain.sign == v.sign) {
|
||||||
|
// add more to drain
|
||||||
|
_drain += v;
|
||||||
|
v = 0;
|
||||||
|
} else {
|
||||||
|
// consume from drain
|
||||||
|
_drain += v;
|
||||||
|
if (_drain.sign == v.sign) {
|
||||||
|
// consumed all, dy = remaining
|
||||||
|
v = _drain;
|
||||||
|
_drain = 0;
|
||||||
|
} else {
|
||||||
|
v = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isAvailable => _drain != 0;
|
||||||
|
|
||||||
|
double _drain = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CropArguments implements TransformArguments {
|
||||||
|
const _CropArguments(this.top, this.left, this.bottom, this.right);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => TransformCropFilter(top, left, bottom, right);
|
||||||
|
|
||||||
|
@override
|
||||||
|
getToolType() => TransformToolType.crop;
|
||||||
|
|
||||||
|
final double top;
|
||||||
|
final double left;
|
||||||
|
final double bottom;
|
||||||
|
final double right;
|
||||||
|
}
|
|
@ -5,13 +5,14 @@ import 'package:nc_photos/widget/image_editor/toolbar_button.dart';
|
||||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
|
||||||
enum TransformToolType {
|
enum TransformToolType {
|
||||||
|
crop,
|
||||||
orientation,
|
orientation,
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class TransformArguments {
|
abstract class TransformArguments {
|
||||||
ImageFilter toImageFilter();
|
ImageFilter? toImageFilter();
|
||||||
|
|
||||||
TransformToolType _getToolType();
|
TransformToolType getToolType();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransformToolbar extends StatefulWidget {
|
class TransformToolbar extends StatefulWidget {
|
||||||
|
@ -19,6 +20,8 @@ class TransformToolbar extends StatefulWidget {
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.initialState,
|
required this.initialState,
|
||||||
required this.onActiveFiltersChanged,
|
required this.onActiveFiltersChanged,
|
||||||
|
required this.isCropModeChanged,
|
||||||
|
required this.onCropToolDeactivated,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -26,6 +29,8 @@ class TransformToolbar extends StatefulWidget {
|
||||||
|
|
||||||
final List<TransformArguments> initialState;
|
final List<TransformArguments> initialState;
|
||||||
final ValueChanged<Iterable<TransformArguments>> onActiveFiltersChanged;
|
final ValueChanged<Iterable<TransformArguments>> onActiveFiltersChanged;
|
||||||
|
final ValueChanged<bool> isCropModeChanged;
|
||||||
|
final VoidCallback onCropToolDeactivated;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TransformToolbarState extends State<TransformToolbar> {
|
class _TransformToolbarState extends State<TransformToolbar> {
|
||||||
|
@ -33,7 +38,7 @@ class _TransformToolbarState extends State<TransformToolbar> {
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
for (final s in widget.initialState) {
|
for (final s in widget.initialState) {
|
||||||
_filters[s._getToolType()] = s;
|
_filters[s.getToolType()] = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +57,7 @@ class _TransformToolbarState extends State<TransformToolbar> {
|
||||||
child = _buildOrientationOption(context);
|
child = _buildOrientationOption(context);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TransformToolType.crop:
|
||||||
case null:
|
case null:
|
||||||
child = null;
|
child = null;
|
||||||
break;
|
break;
|
||||||
|
@ -73,6 +79,14 @@ class _TransformToolbarState extends State<TransformToolbar> {
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.crop_outlined,
|
||||||
|
label: L10n.global().imageEditTransformCrop,
|
||||||
|
onPressed: _onCropPressed,
|
||||||
|
isSelected: _selectedFilter == TransformToolType.crop,
|
||||||
|
activationOrder:
|
||||||
|
_filters.containsKey(TransformToolType.crop) ? -1 : null,
|
||||||
|
),
|
||||||
ToolbarButton(
|
ToolbarButton(
|
||||||
icon: Icons.rotate_90_degrees_ccw_outlined,
|
icon: Icons.rotate_90_degrees_ccw_outlined,
|
||||||
label: L10n.global().imageEditTransformOrientation,
|
label: L10n.global().imageEditTransformOrientation,
|
||||||
|
@ -165,15 +179,27 @@ class _TransformToolbarState extends State<TransformToolbar> {
|
||||||
_selectedFilter = null;
|
_selectedFilter = null;
|
||||||
_filters.remove(type);
|
_filters.remove(type);
|
||||||
});
|
});
|
||||||
|
if (type == TransformToolType.crop) {
|
||||||
|
widget.isCropModeChanged(false);
|
||||||
|
widget.onCropToolDeactivated();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (_selectedFilter == TransformToolType.crop) {
|
||||||
|
widget.isCropModeChanged(false);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedFilter = type;
|
_selectedFilter = type;
|
||||||
_filters[type] ??= defArgs;
|
_filters[type] ??= defArgs;
|
||||||
});
|
});
|
||||||
|
if (type == TransformToolType.crop) {
|
||||||
|
widget.isCropModeChanged(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_notifyFiltersChanged();
|
_notifyFiltersChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCropPressed() =>
|
||||||
|
_onFilterPressed(TransformToolType.crop, const _DummyCropArguments());
|
||||||
void _onOrientationPressed() => _onFilterPressed(
|
void _onOrientationPressed() => _onFilterPressed(
|
||||||
TransformToolType.orientation, const _OrientationArguments(0));
|
TransformToolType.orientation, const _OrientationArguments(0));
|
||||||
|
|
||||||
|
@ -192,6 +218,18 @@ class _TransformToolbarState extends State<TransformToolbar> {
|
||||||
TransformToolType? _selectedFilter;
|
TransformToolType? _selectedFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arguments for crop is handled by its controller, this is used to restore
|
||||||
|
// state in the toolbar only
|
||||||
|
class _DummyCropArguments implements TransformArguments {
|
||||||
|
const _DummyCropArguments();
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
getToolType() => TransformToolType.crop;
|
||||||
|
}
|
||||||
|
|
||||||
class _OrientationArguments implements TransformArguments {
|
class _OrientationArguments implements TransformArguments {
|
||||||
const _OrientationArguments(this.value);
|
const _OrientationArguments(this.value);
|
||||||
|
|
||||||
|
@ -199,7 +237,7 @@ class _OrientationArguments implements TransformArguments {
|
||||||
toImageFilter() => TransformOrientationFilter(value);
|
toImageFilter() => TransformOrientationFilter(value);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_getToolType() => TransformToolType.orientation;
|
getToolType() => TransformToolType.orientation;
|
||||||
|
|
||||||
final int value;
|
final int value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ add_library( # Sets the name of the library.
|
||||||
filter/brightness.cpp
|
filter/brightness.cpp
|
||||||
filter/color_levels.cpp
|
filter/color_levels.cpp
|
||||||
filter/contrast.cpp
|
filter/contrast.cpp
|
||||||
|
filter/crop.cpp
|
||||||
filter/curve.cpp
|
filter/curve.cpp
|
||||||
filter/hslhsv.cpp
|
filter/hslhsv.cpp
|
||||||
filter/orientation.cpp
|
filter/orientation.cpp
|
||||||
|
|
69
plugin/android/src/main/cpp/filter/crop.cpp
Normal file
69
plugin/android/src/main/cpp/filter/crop.cpp
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <exception>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../exception.h"
|
||||||
|
#include "../log.h"
|
||||||
|
#include "../util.h"
|
||||||
|
|
||||||
|
using namespace plugin;
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class Crop {
|
||||||
|
public:
|
||||||
|
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||||
|
const size_t height, const int top, const int left,
|
||||||
|
const int dstWidth, const int dstHeight);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr const char *TAG = "Crop";
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||||
|
Java_com_nkming_nc_1photos_plugin_image_1processor_Crop_applyNative(
|
||||||
|
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||||
|
jint top, jint left, jint dstWidth, jint dstHeight) {
|
||||||
|
try {
|
||||||
|
initOpenMp();
|
||||||
|
RaiiContainer<jbyte> cRgba8(
|
||||||
|
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||||
|
[&](jbyte *obj) {
|
||||||
|
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||||
|
});
|
||||||
|
const auto result =
|
||||||
|
Crop().apply(reinterpret_cast<uint8_t *>(cRgba8.get()), width, height,
|
||||||
|
top, left, dstWidth, dstHeight);
|
||||||
|
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> Crop::apply(const uint8_t *rgba8, const size_t width,
|
||||||
|
const size_t height, const int top, const int left,
|
||||||
|
const int dstWidth, const int dstHeight) {
|
||||||
|
LOGI(TAG, "[apply] top: %d, left: %d, width: %d, height: %d", top, left,
|
||||||
|
dstWidth, dstHeight);
|
||||||
|
vector<uint8_t> output(dstWidth * dstHeight * 4);
|
||||||
|
#pragma omp parallel for
|
||||||
|
for (size_t y = 0; y < dstHeight; ++y) {
|
||||||
|
const auto srcY = y + top;
|
||||||
|
memcpy(output.data() + (y * dstWidth * 4),
|
||||||
|
rgba8 + (srcY * width + left) * 4, dstWidth * 4);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
|
@ -238,6 +238,10 @@ interface ImageFilter {
|
||||||
"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)
|
"orientation" -> Orientation(json["degree"] as Int)
|
||||||
|
"crop" -> Crop(
|
||||||
|
json["top"] as Double, json["left"] as Double,
|
||||||
|
json["bottom"] as Double, json["right"] as Double
|
||||||
|
)
|
||||||
else -> throw IllegalArgumentException(
|
else -> throw IllegalArgumentException(
|
||||||
"Unknown type: ${json["type"]}"
|
"Unknown type: ${json["type"]}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.nkming.nc_photos.plugin.image_processor
|
||||||
|
|
||||||
|
import com.nkming.nc_photos.plugin.ImageFilter
|
||||||
|
import java.lang.Integer.max
|
||||||
|
|
||||||
|
class Crop(
|
||||||
|
val top: Double, val left: Double, val bottom: Double, val right: Double
|
||||||
|
) : ImageFilter {
|
||||||
|
override fun apply(rgba8: Rgba8Image): Rgba8Image {
|
||||||
|
// prevent w/h == 0
|
||||||
|
val width = max((rgba8.width * (right - left)).toInt(), 1)
|
||||||
|
val height = max((rgba8.height * (bottom - top)).toInt(), 1)
|
||||||
|
val top = (rgba8.height * top).toInt()
|
||||||
|
val left = (rgba8.width * left).toInt()
|
||||||
|
val data = applyNative(
|
||||||
|
rgba8.pixel, rgba8.width, rgba8.height, top, left, width, height
|
||||||
|
)
|
||||||
|
return Rgba8Image(data, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun applyNative(
|
||||||
|
rgba8: ByteArray, width: Int, height: Int, top: Int, left: Int,
|
||||||
|
dstWidth: Int, dstHeight: Int
|
||||||
|
): ByteArray
|
||||||
|
}
|
|
@ -44,6 +44,24 @@ class ColorTintFilter extends _SingleWeightFilter {
|
||||||
const ColorTintFilter(double weight) : super("tint", weight);
|
const ColorTintFilter(double weight) : super("tint", weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TransformCropFilter implements ImageFilter {
|
||||||
|
const TransformCropFilter(this.top, this.left, this.bottom, this.right);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toJson() => {
|
||||||
|
"type": "crop",
|
||||||
|
"top": top,
|
||||||
|
"left": left,
|
||||||
|
"bottom": bottom,
|
||||||
|
"right": right,
|
||||||
|
};
|
||||||
|
|
||||||
|
final double top;
|
||||||
|
final double left;
|
||||||
|
final double bottom;
|
||||||
|
final double right;
|
||||||
|
}
|
||||||
|
|
||||||
class TransformOrientationFilter implements ImageFilter {
|
class TransformOrientationFilter implements ImageFilter {
|
||||||
const TransformOrientationFilter(this.degree);
|
const TransformOrientationFilter(this.degree);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue