Merge branch 'image-editor' into dev

This commit is contained in:
Ming Ming 2022-07-23 01:29:06 +08:00
commit 2085b9197e
46 changed files with 3679 additions and 23 deletions

View file

@ -8,3 +8,4 @@ const enhanceZeroDceUrl = "https://bit.ly/3wKJcm9";
const enhanceDeepLabPortraitBlurUrl = "https://bit.ly/3wIuXy6";
const enhanceEsrganUrl = "https://bit.ly/3wO0NJP";
const enhanceStyleTransferUrl = "https://bit.ly/3agpTcF";
const editPhotosUrl = "https://bit.ly/3v82oKA";

View file

@ -82,6 +82,22 @@ extension IterableExtension<T> on Iterable<T> {
return toList();
}
}
/// The first index of [element] in this iterable
///
/// Searches the list from index start to the end of the list. The first time
/// an object o is encountered so that o == element, the index of o is
/// returned. Returns -1 if element is not found.
int indexOf(T element, [int start = 0]) {
var i = 0;
for (final e in this) {
if (e == element) {
return i;
}
++i;
}
return -1;
}
}
extension IterableFlattenExtension<T> on Iterable<Iterable<T>> {

View file

@ -1256,6 +1256,51 @@
"@doubleTapExitNotification": {
"description": "If double tap to exit is enabled in settings, shown when users tap the back button"
},
"imageEditDiscardDialogTitle": "Discard changes?",
"@imageEditDiscardDialogTitle": {
"description": "Warn before dismissing image editor (e.g., user pressing back button)"
},
"imageEditDiscardDialogContent": "Your changes are not saved",
"discardButtonLabel": "DISCARD",
"@discardButtonLabel": {
"description": "Discard the current unsaved content"
},
"saveTooltip": "Save",
"@saveTooltip": {
"description": "Save the current content"
},
"imageEditColorBrightness": "Brightness",
"@imageEditColorBrightness": {
"description": "Adjust the brightness of an image"
},
"imageEditColorContrast": "Contrast",
"@imageEditColorContrast": {
"description": "Adjust the contrast of an image"
},
"imageEditColorWhitePoint": "White point",
"@imageEditColorWhitePoint": {
"description": "Adjust the white point of an image. Learn more about this adjustment: https://www.photoreview.com.au/tips/editing/advanced-levels-adjustments"
},
"imageEditColorBlackPoint": "Black point",
"@imageEditColorBlackPoint": {
"description": "Adjust the black point of an image"
},
"imageEditColorSaturation": "Saturation",
"@imageEditColorSaturation": {
"description": "Adjust the color saturation of an image"
},
"imageEditColorWarmth": "Warmth",
"@imageEditColorWarmth": {
"description": "This roughly equals to adjusting the color temperature of an image. The end result is to shift the image colors such that it looks 'warmer' or 'cooler'"
},
"imageEditColorTint": "Tint",
"@imageEditColorTint": {
"description": "Shift colors from a green to a magenta tint"
},
"imageEditTitle": "Preview edits",
"@imageEditTitle": {
"description": "Title of the image editor"
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {

View file

@ -112,6 +112,18 @@
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle",
"errorAlbumDowngrade"
],
@ -242,6 +254,18 @@
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle",
"errorAlbumDowngrade"
],
@ -252,11 +276,50 @@
"settingsAccountLabelDescription",
"settingsDoubleTapExitTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"es": [
"rootPickerSkipConfirmationDialogContent2"
"rootPickerSkipConfirmationDialogContent2",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"fi": [
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"fr": [
@ -291,7 +354,19 @@
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"pl": [
@ -343,7 +418,19 @@
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"pt": [
@ -374,7 +461,19 @@
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"ru": [
@ -405,7 +504,19 @@
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"zh": [
@ -436,7 +547,19 @@
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
],
"zh_Hant": [
@ -467,6 +590,18 @@
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification"
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
"imageEditDiscardDialogContent",
"discardButtonLabel",
"saveTooltip",
"imageEditColorBrightness",
"imageEditColorContrast",
"imageEditColorWhitePoint",
"imageEditColorBlackPoint",
"imageEditColorSaturation",
"imageEditColorWarmth",
"imageEditColorTint",
"imageEditTitle"
]
}

View file

@ -0,0 +1,41 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
/// ImageProvider for raw RGBA pixels
class PixelImage extends ImageProvider<PixelImage> {
PixelImage(
this.rgba,
this.width,
this.height, {
this.scale = 1.0,
});
@override
obtainKey(ImageConfiguration configuration) =>
SynchronousFuture<PixelImage>(this);
@override
load(PixelImage key, DecoderCallback decode) =>
OneFrameImageStreamCompleter(_createImageInfo());
Future<ImageInfo> _createImageInfo() async {
final codec = await ImageDescriptor.raw(
await ImmutableBuffer.fromUint8List(rgba),
width: width,
height: height,
pixelFormat: PixelFormat.rgba8888,
).instantiateCodec();
final frame = await codec.getNextFrame();
return ImageInfo(image: frame.image, scale: scale);
}
final Uint8List rgba;
final int width;
final int height;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
}

View file

@ -110,8 +110,8 @@ class AppTheme extends StatelessWidget {
static Color getUnfocusedIconColor(BuildContext context) {
return Theme.of(context).brightness == Brightness.light
? Colors.black54
: Colors.white70;
? unfocusedIconColorLight
: unfocusedIconColorDark;
}
static ThemeData buildLightThemeData() {
@ -181,6 +181,9 @@ class AppTheme extends StatelessWidget {
static const primaryTextColorLight = Colors.black87;
static final primaryTextColorDark = Colors.white.withOpacity(.87);
static const unfocusedIconColorLight = Colors.black54;
static const unfocusedIconColorDark = Colors.white70;
static const widthLimitedContentMaxWidth = 550.0;
/// Make a TextButton look like a default FlatButton. See

View file

@ -355,8 +355,10 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
}
void _reqQuery() {
_bloc.add(const ScanLocalDirBlocQuery(
["Download/Photos (for Nextcloud)/Enhanced Photos"]));
_bloc.add(const ScanLocalDirBlocQuery([
"Download/Photos (for Nextcloud)/Enhanced Photos",
"Download/Photos (for Nextcloud)/Edited Photos",
]));
}
final _bloc = ScanLocalDirBloc();

View file

@ -0,0 +1,649 @@
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/double_extension.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/pixel_image_provider.dart';
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/stateful_slider.dart';
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
class ImageEditorArguments {
const ImageEditorArguments(this.account, this.file);
final Account account;
final File file;
}
class ImageEditor extends StatefulWidget {
static const routeName = "/image-editor";
static Route buildRoute(ImageEditorArguments args) => MaterialPageRoute(
builder: (context) => ImageEditor.fromArgs(args),
);
const ImageEditor({
Key? key,
required this.account,
required this.file,
}) : super(key: key);
ImageEditor.fromArgs(ImageEditorArguments args, {Key? key})
: this(
key: key,
account: args.account,
file: args.file,
);
@override
createState() => _ImageEditorState();
final Account account;
final File file;
}
class _ImageEditorState extends State<ImageEditor> {
@override
initState() {
super.initState();
_initImage();
_ensurePermission();
}
Future<void> _ensurePermission() async {
if (!await const PermissionHandler().ensureStorageWritePermission()) {
if (mounted) {
Navigator.of(context).pop();
}
}
}
@override
build(BuildContext context) => AppTheme(
child: Scaffold(
body: Builder(
builder: _buildContent,
),
),
);
Future<void> _initImage() async {
final fileInfo = await LargeImageCacheManager.inst
.getFileFromCache(api_util.getFilePreviewUrl(
widget.account,
widget.file,
width: k.photoLargeSize,
height: k.photoLargeSize,
a: true,
));
_src = await ImageLoader.loadUri(
"file://${fileInfo!.file.path}",
480,
360,
ImageLoaderResizeMethod.fit,
isAllowSwapSide: true,
);
if (mounted) {
setState(() {
_isDoneInit = true;
});
}
}
Widget _buildContent(BuildContext context) {
return WillPopScope(
onWillPop: () async {
_onBackButton(context);
return false;
},
child: ColoredBox(
color: Colors.black,
child: Column(
children: [
_buildAppBar(context),
Expanded(
child: _isDoneInit
? Image(
image: (_dst ?? _src).run((obj) =>
PixelImage(obj.pixel, obj.width, obj.height)),
fit: BoxFit.contain,
gaplessPlayback: true,
)
: Container(),
),
_buildFilterOption(context),
_buildFilterBar(context),
],
),
),
);
}
Widget _buildAppBar(BuildContext context) => AppBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
foregroundColor: Colors.white.withOpacity(.87),
leading: BackButton(onPressed: () => _onBackButton(context)),
title: Text(L10n.global().imageEditTitle),
actions: [
if (_filters.isNotEmpty)
IconButton(
icon: const Icon(Icons.save_outlined),
tooltip: L10n.global().saveTooltip,
onPressed: () => _onSavePressed(context),
),
IconButton(
icon: const Icon(Icons.help_outline),
tooltip: L10n.global().helpTooltip,
onPressed: () {
launch(help_util.editPhotosUrl);
},
),
],
);
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),
_FilterButton(
icon: Icons.brightness_medium,
label: L10n.global().imageEditColorBrightness,
onPressed: _onBrightnessPressed,
isSelected: _selectedFilter == _ColorFilterType.brightness,
activationOrder:
_filters.keys.indexOf(_ColorFilterType.brightness),
),
_FilterButton(
icon: Icons.contrast,
label: L10n.global().imageEditColorContrast,
onPressed: _onContrastPressed,
isSelected: _selectedFilter == _ColorFilterType.contrast,
activationOrder:
_filters.keys.indexOf(_ColorFilterType.contrast),
),
_FilterButton(
icon: Icons.circle,
label: L10n.global().imageEditColorWhitePoint,
onPressed: _onWhitePointPressed,
isSelected: _selectedFilter == _ColorFilterType.whitePoint,
activationOrder:
_filters.keys.indexOf(_ColorFilterType.whitePoint),
),
_FilterButton(
icon: Icons.circle_outlined,
label: L10n.global().imageEditColorBlackPoint,
onPressed: _onBlackPointPressed,
isSelected: _selectedFilter == _ColorFilterType.blackPoint,
activationOrder:
_filters.keys.indexOf(_ColorFilterType.blackPoint),
),
_FilterButton(
icon: Icons.invert_colors,
label: L10n.global().imageEditColorSaturation,
onPressed: _onSaturationPressed,
isSelected: _selectedFilter == _ColorFilterType.saturation,
activationOrder:
_filters.keys.indexOf(_ColorFilterType.saturation),
),
_FilterButton(
icon: Icons.thermostat,
label: L10n.global().imageEditColorWarmth,
onPressed: _onWarmthPressed,
isSelected: _selectedFilter == _ColorFilterType.warmth,
activationOrder: _filters.keys.indexOf(_ColorFilterType.warmth),
),
_FilterButton(
icon: Icons.colorize,
label: L10n.global().imageEditColorTint,
onPressed: _onTintPressed,
isSelected: _selectedFilter == _ColorFilterType.tint,
activationOrder: _filters.keys.indexOf(_ColorFilterType.tint),
),
const SizedBox(width: 16),
],
),
),
),
);
}
Widget _buildFilterOption(BuildContext context) {
Widget? child;
switch (_selectedFilter) {
case _ColorFilterType.brightness:
child = _buildBrightnessOption(context);
break;
case _ColorFilterType.contrast:
child = _buildContrastOption(context);
break;
case _ColorFilterType.whitePoint:
child = _buildWhitePointOption(context);
break;
case _ColorFilterType.blackPoint:
child = _buildBlackPointOption(context);
break;
case _ColorFilterType.saturation:
child = _buildSaturationOption(context);
break;
case _ColorFilterType.warmth:
child = _buildWarmthOption(context);
break;
case _ColorFilterType.tint:
child = _buildTintOption(context);
break;
case null:
child = null;
break;
}
return Container(
height: 96,
alignment: Alignment.center,
child: child,
);
}
Widget _buildSliderOption(
BuildContext context, {
required Key key,
required double min,
required double max,
required double initialValue,
ValueChanged<double>? onChangeEnd,
}) {
return AppTheme.dark(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Stack(
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(min.toStringAsFixedTruncated(1)),
),
if (min < 0 && max > 0)
const Align(
alignment: AlignmentDirectional.center,
child: Text("0"),
),
Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(max.toStringAsFixedTruncated(1)),
),
],
),
),
StatefulSlider(
key: key,
initialValue: initialValue.toDouble(),
min: min.toDouble(),
max: max.toDouble(),
onChangeEnd: onChangeEnd,
),
],
),
),
);
}
Widget _buildBrightnessOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.brightness.name),
min: -100,
max: 100,
initialValue:
(_filters[_ColorFilterType.brightness] as _BrightnessArguments)
.value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.brightness] as _BrightnessArguments)
.value = value;
_applyFilters();
},
);
Widget _buildContrastOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.contrast.name),
min: -100,
max: 100,
initialValue:
(_filters[_ColorFilterType.contrast] as _ContrastArguments).value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.contrast] as _ContrastArguments).value =
value;
_applyFilters();
},
);
Widget _buildWhitePointOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.whitePoint.name),
min: -100,
max: 100,
initialValue:
(_filters[_ColorFilterType.whitePoint] as _WhitePointArguments)
.value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.whitePoint] as _WhitePointArguments)
.value = value;
_applyFilters();
},
);
Widget _buildBlackPointOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.blackPoint.name),
min: -100,
max: 100,
initialValue:
(_filters[_ColorFilterType.blackPoint] as _BlackPointArguments)
.value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.blackPoint] as _BlackPointArguments)
.value = value;
_applyFilters();
},
);
Widget _buildSaturationOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.saturation.name),
min: -100,
max: 100,
initialValue:
(_filters[_ColorFilterType.saturation] as _SaturationArguments)
.value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.saturation] as _SaturationArguments)
.value = value;
_applyFilters();
},
);
Widget _buildWarmthOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.warmth.name),
min: -100,
max: 100,
initialValue:
(_filters[_ColorFilterType.warmth] as _WarmthArguments).value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.warmth] as _WarmthArguments).value = value;
_applyFilters();
},
);
Widget _buildTintOption(BuildContext context) => _buildSliderOption(
context,
key: Key(_ColorFilterType.tint.name),
min: -100,
max: 100,
initialValue: (_filters[_ColorFilterType.tint] as _TintArguments).value,
onChangeEnd: (value) {
(_filters[_ColorFilterType.tint] as _TintArguments).value = value;
_applyFilters();
},
);
Future<void> _onBackButton(BuildContext context) async {
if (_filters.isEmpty) {
Navigator.of(context).pop();
return;
}
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(L10n.global().imageEditDiscardDialogTitle),
content: Text(L10n.global().imageEditDiscardDialogContent),
actions: [
TextButton(
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
onPressed: () {
Navigator.of(context).pop(false);
},
),
TextButton(
child: Text(L10n.global().discardButtonLabel),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
),
);
if (result == true) {
Navigator.of(context).pop();
}
}
Future<void> _onSavePressed(BuildContext context) async {
await ImageProcessor.colorFilter(
"${widget.account.url}/${widget.file.path}",
widget.file.filename,
4096,
3072,
_buildFilterList(),
headers: {
"Authorization": Api.getAuthorizationHeaderValue(widget.account),
},
);
Navigator.of(context).pop();
}
void _onFilterPressed(_ColorFilterType type, _FilterArguments defArgs) {
if (_selectedFilter == type) {
// deactivate filter
setState(() {
_selectedFilter = null;
_filters.remove(type);
});
} else {
setState(() {
_selectedFilter = type;
_filters[type] ??= defArgs;
});
}
_applyFilters();
}
void _onBrightnessPressed() =>
_onFilterPressed(_ColorFilterType.brightness, _BrightnessArguments(0));
void _onContrastPressed() =>
_onFilterPressed(_ColorFilterType.contrast, _ContrastArguments(0));
void _onWhitePointPressed() =>
_onFilterPressed(_ColorFilterType.whitePoint, _WhitePointArguments(0));
void _onBlackPointPressed() =>
_onFilterPressed(_ColorFilterType.blackPoint, _BlackPointArguments(0));
void _onSaturationPressed() =>
_onFilterPressed(_ColorFilterType.saturation, _SaturationArguments(0));
void _onWarmthPressed() =>
_onFilterPressed(_ColorFilterType.warmth, _WarmthArguments(0));
void _onTintPressed() =>
_onFilterPressed(_ColorFilterType.tint, _TintArguments(0));
List<ImageFilter> _buildFilterList() {
return _filters.entries.map((e) {
switch (e.key) {
case _ColorFilterType.brightness:
return (e.value as _BrightnessArguments)
.run((arg) => ColorBrightnessFilter(arg.value / 100));
case _ColorFilterType.contrast:
return (e.value as _ContrastArguments)
.run((arg) => ColorContrastFilter(arg.value / 100));
case _ColorFilterType.whitePoint:
return (e.value as _WhitePointArguments)
.run((arg) => ColorWhitePointFilter(arg.value / 100));
case _ColorFilterType.blackPoint:
return (e.value as _BlackPointArguments)
.run((arg) => ColorBlackPointFilter(arg.value / 100));
case _ColorFilterType.saturation:
return (e.value as _SaturationArguments)
.run((arg) => ColorSaturationFilter(arg.value / 100));
case _ColorFilterType.warmth:
return (e.value as _WarmthArguments)
.run((arg) => ColorWarmthFilter(arg.value / 100));
case _ColorFilterType.tint:
return (e.value as _TintArguments)
.run((arg) => ColorTintFilter(arg.value / 100));
}
}).toList();
}
Future<void> _applyFilters() async {
final result = await ImageProcessor.filterPreview(_src, _buildFilterList());
setState(() {
_dst = result;
});
}
bool _isDoneInit = false;
late final Rgba8Image _src;
Rgba8Image? _dst;
final _filters = <_ColorFilterType, _FilterArguments>{};
_ColorFilterType? _selectedFilter;
}
enum _ColorFilterType {
brightness,
contrast,
whitePoint,
blackPoint,
saturation,
warmth,
tint,
}
abstract class _FilterArguments {}
class _FilterDoubleArguments implements _FilterArguments {
_FilterDoubleArguments(this.value);
double value;
}
typedef _BrightnessArguments = _FilterDoubleArguments;
typedef _ContrastArguments = _FilterDoubleArguments;
typedef _WhitePointArguments = _FilterDoubleArguments;
typedef _BlackPointArguments = _FilterDoubleArguments;
typedef _SaturationArguments = _FilterDoubleArguments;
typedef _WarmthArguments = _FilterDoubleArguments;
typedef _TintArguments = _FilterDoubleArguments;
class _FilterButton extends StatelessWidget {
const _FilterButton({
Key? key,
required this.icon,
required this.label,
required this.onPressed,
this.isSelected = false,
this.activationOrder = -1,
}) : super(key: key);
@override
build(BuildContext context) {
final color = !isSelected && isActivated
? AppTheme.primarySwatchDark[900]!.withOpacity(0.4)
: AppTheme.primarySwatchDark[500]!.withOpacity(0.7);
return InkWell(
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 64,
height: 64,
child: Stack(
fit: StackFit.expand,
children: [
AnimatedOpacity(
opacity: isSelected || isActivated ? 1 : 0,
duration: k.animationDurationNormal,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: color,
),
),
),
Center(
child: Icon(
icon,
size: 32,
color: AppTheme.unfocusedIconColorDark,
),
),
if (isActivated)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Align(
alignment: Alignment.topCenter,
child: Text(
(activationOrder + 1).toString(),
style: const TextStyle(
fontSize: 12,
color: AppTheme.unfocusedIconColorDark,
),
),
),
),
],
),
),
const SizedBox(height: 8),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
color: AppTheme.unfocusedIconColorDark,
),
),
],
),
),
);
}
bool get isActivated => activationOrder >= 0;
final IconData icon;
final String label;
final VoidCallback? onPressed;
final bool isSelected;
final int activationOrder;
}

View file

@ -19,6 +19,7 @@ import 'package:nc_photos/widget/dynamic_album_browser.dart';
import 'package:nc_photos/widget/enhanced_photo_browser.dart';
import 'package:nc_photos/widget/favorite_browser.dart';
import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/image_editor.dart';
import 'package:nc_photos/widget/local_file_viewer.dart';
import 'package:nc_photos/widget/people_browser.dart';
import 'package:nc_photos/widget/person_browser.dart';
@ -163,6 +164,7 @@ class _MyAppState extends State<MyApp>
route ??= _handleEnhancedPhotoBrowserRoute(settings);
route ??= _handleLocalFileViewerRoute(settings);
route ??= _handleEnhancementSettingsRoute(settings);
route ??= _handleImageEditorRoute(settings);
return route;
}
@ -539,6 +541,19 @@ class _MyAppState extends State<MyApp>
return null;
}
Route<dynamic>? _handleImageEditorRoute(RouteSettings settings) {
try {
if (settings.name == ImageEditor.routeName &&
settings.arguments != null) {
final args = settings.arguments as ImageEditorArguments;
return ImageEditor.buildRoute(args);
}
} catch (e) {
_log.severe("[_handleImageEditorRoute] Failed while handling route", e);
}
return null;
}
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final _navigatorKey = GlobalKey<NavigatorState>();

View file

@ -26,6 +26,7 @@ import 'package:nc_photos/widget/disposable.dart';
import 'package:nc_photos/widget/handler/enhance_handler.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
import 'package:nc_photos/widget/image_editor.dart';
import 'package:nc_photos/widget/image_viewer.dart';
import 'package:nc_photos/widget/slideshow_dialog.dart';
import 'package:nc_photos/widget/slideshow_viewer.dart';
@ -212,7 +213,15 @@ class _ViewerState extends State<Viewer>
onPressed: () => _onSharePressed(context),
),
if (features.isSupportEnhancement &&
EnhanceHandler.isSupportedFormat(file))
EnhanceHandler.isSupportedFormat(file)) ...[
IconButton(
icon: Icon(
Icons.tune_outlined,
color: Colors.white.withOpacity(.87),
),
tooltip: L10n.global().editTooltip,
onPressed: () => _onEditPressed(context),
),
IconButton(
icon: Icon(
Icons.auto_fix_high_outlined,
@ -221,6 +230,7 @@ class _ViewerState extends State<Viewer>
tooltip: L10n.global().enhanceTooltip,
onPressed: () => _onEnhancePressed(context),
),
],
IconButton(
icon: Icon(
Icons.download_outlined,
@ -570,6 +580,18 @@ class _ViewerState extends State<Viewer>
).shareFiles(widget.account, [file]);
}
void _onEditPressed(BuildContext context) {
final file = widget.streamFiles[_viewerController.currentPage];
if (!file_util.isSupportedImageFormat(file)) {
_log.shout("[_onEditPressed] Video file not supported");
return;
}
_log.info("[_onEditPressed] Edit file: ${file.path}");
Navigator.of(context).pushNamed(ImageEditor.routeName,
arguments: ImageEditorArguments(widget.account, file));
}
void _onEnhancePressed(BuildContext context) {
final file = widget.streamFiles[_viewerController.currentPage];
if (!file_util.isSupportedImageFormat(file)) {

View file

@ -33,6 +33,16 @@ add_library( # Sets the name of the library.
SHARED
# Provides a relative path to your source file(s).
filter/brightness.cpp
filter/color_levels.cpp
filter/contrast.cpp
filter/curve.cpp
filter/hslhsv.cpp
filter/saturation.cpp
filter/tint.cpp
filter/warmth.cpp
filter/yuv.cpp
lib/spline/spline.cpp
arbitrary_style_transfer.cpp
deep_lap_3.cpp
esrgan.cpp

View file

@ -0,0 +1,77 @@
#include <cmath>
#include <cstdint>
#include <cstring>
#include <exception>
#include <jni.h>
#include <vector>
#include "../exception.h"
#include "../log.h"
#include "../math_util.h"
#include "../util.h"
#include "./hslhsv.h"
using namespace plugin;
using namespace std;
namespace {
class Brightness {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static constexpr const char *TAG = "Brightness";
};
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_Brightness_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat weight) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = Brightness().apply(
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, weight);
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> Brightness::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
const float mul = 1 + weight / 2;
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
auto hsv = filter::rgb8ToHsv(rgba8 + p);
hsv[2] = clamp(0.f, hsv[2] * mul, 1.f);
const auto &newRgb = filter::hsvToRgb8(hsv.data());
memcpy(output.data() + p, newRgb.data(), 3);
output[p + 3] = rgba8[p + 3];
}
return output;
}
} // namespace

View file

@ -0,0 +1,178 @@
#include <cstdint>
#include <exception>
#include <jni.h>
#include <vector>
#include "../exception.h"
#include "../log.h"
#include "../math_util.h"
#include "../util.h"
using namespace plugin;
using namespace std;
namespace {
constexpr float INPUT_AMPLITUDE = .4f;
constexpr uint8_t OUTPUT_AMPLITUDE = 100;
class WhitePoint {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static uint8_t applyInputLevel(const uint8_t p, const float weight) {
const auto pf = p / 255.f;
const auto max = 1 - weight * INPUT_AMPLITUDE;
return clamp<int>(0, clamp(0.f, pf, max) / max * 255.f, 255);
}
static uint8_t applyOutputLevel(const uint8_t p, const float weight) {
return clamp<int>(0, p / 255.f * (255 - weight * OUTPUT_AMPLITUDE), 255);
}
static std::vector<uint8_t> buildLut(const float weight);
static constexpr const char *TAG = "WhitePoint";
};
class BlackPoint {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static inline uint8_t applyInputLevel(const uint8_t p, const float weight) {
const auto pf = p / 255.f;
const auto min = weight * INPUT_AMPLITUDE;
return clamp<int>(0, (clamp(min, pf, 1.f) - min) / (1 - min) * 255.f, 255);
}
static inline uint8_t applyOutputLevel(const uint8_t p, const float weight) {
const auto x = weight * OUTPUT_AMPLITUDE;
return clamp<int>(0, p / 255.f * (255 - x) + x, 255);
}
static std::vector<uint8_t> buildLut(const float weight);
static constexpr const char *TAG = "BlackPoint";
};
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_WhitePoint_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat weight) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = WhitePoint().apply(
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, weight);
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;
}
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_BlackPoint_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat weight) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = BlackPoint().apply(
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, weight);
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> WhitePoint::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
const auto lut = buildLut(weight);
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
output[p + 0] = lut[rgba8[p + 0]];
output[p + 1] = lut[rgba8[p + 1]];
output[p + 2] = lut[rgba8[p + 2]];
output[p + 3] = rgba8[p + 3];
}
return output;
}
vector<uint8_t> WhitePoint::buildLut(const float weight) {
vector<uint8_t> product(256);
const float weightAbs = std::abs(weight);
const auto fn =
weight > 0 ? &WhitePoint::applyInputLevel : &WhitePoint::applyOutputLevel;
#pragma omp parallel for
for (size_t i = 0; i < 256; ++i) {
product[i] = fn(i, weightAbs);
}
return product;
}
vector<uint8_t> BlackPoint::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
const auto lut = buildLut(weight);
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
output[p + 0] = lut[rgba8[p + 0]];
output[p + 1] = lut[rgba8[p + 1]];
output[p + 2] = lut[rgba8[p + 2]];
output[p + 3] = rgba8[p + 3];
}
return output;
}
vector<uint8_t> BlackPoint::buildLut(const float weight) {
vector<uint8_t> product(256);
const float weightAbs = std::abs(weight);
const auto fn =
weight > 0 ? &BlackPoint::applyInputLevel : &BlackPoint::applyOutputLevel;
#pragma omp parallel for
for (size_t i = 0; i < 256; ++i) {
product[i] = fn(i, weightAbs);
}
return product;
}
} // namespace

View file

@ -0,0 +1,93 @@
#include <cmath>
#include <cstdint>
#include <cstring>
#include <exception>
#include <jni.h>
#include <memory>
#include <vector>
#include "../exception.h"
#include "../log.h"
#include "../math_util.h"
#include "../util.h"
#include "./hslhsv.h"
using namespace plugin;
using namespace std;
namespace {
class Contrast {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static constexpr const char *TAG = "Contrast";
};
inline uint8_t applySingle(const uint8_t p, const float mul) {
return clamp(0, static_cast<int>((p - 127) * mul + 127), 0xFF);
}
std::vector<uint8_t> buildLut(const float mul);
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_Contrast_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat weight) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = Contrast().apply(
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, weight);
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> Contrast::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
const float mul = weight >= 0 ? weight + 1 : (weight + 1) * .4f + .6f;
const auto lut = buildLut(mul);
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
output[p + 0] = lut[rgba8[p + 0]];
output[p + 1] = lut[rgba8[p + 1]];
output[p + 2] = lut[rgba8[p + 2]];
output[p + 3] = rgba8[p + 3];
}
return output;
}
vector<uint8_t> buildLut(const float mul) {
vector<uint8_t> product(256);
#pragma omp parallel for
for (size_t i = 0; i < 256; ++i) {
product[i] = applySingle(i, mul);
}
return product;
}
} // namespace

View file

@ -0,0 +1,56 @@
#include <algorithm>
#include <cstdint>
#include <vector>
#include "../lib/spline/spline.h"
#include "./curve.h"
using namespace std;
namespace {
std::vector<uint8_t> buildLut(const vector<uint8_t> &from,
const vector<uint8_t> &to);
vector<double> transformPoints(const vector<uint8_t> &pts);
} // namespace
namespace plugin {
namespace filter {
Curve::Curve(const vector<uint8_t> &from, const vector<uint8_t> &to)
: lut(buildLut(from, to)) {}
} // namespace filter
} // namespace plugin
namespace {
std::vector<uint8_t> buildLut(const vector<uint8_t> &from,
const vector<uint8_t> &to) {
assert(from.size() >= 2);
assert(from[0] == 0);
assert(from[from.size() - 1] == 255);
assert(to.size() >= 2);
assert(to[0] == 0);
assert(to[to.size() - 1] == 255);
tk::spline spline(transformPoints(from), transformPoints(to),
tk::spline::cspline_hermite);
std::vector<uint8_t> lut;
lut.reserve(256);
for (int i = 0; i <= 0xFF; ++i) {
lut.push_back(std::min(std::max(0, static_cast<int>(spline(i))), 0xFF));
}
return lut;
}
vector<double> transformPoints(const vector<uint8_t> &pts) {
vector<double> product;
product.reserve(pts.size());
for (const auto pt : pts) {
product.push_back(pt);
}
return product;
}
} // namespace

View file

@ -0,0 +1,26 @@
#include <cstdint>
#include <vector>
namespace plugin {
namespace filter {
class Curve {
public:
/**
* Construct a curve that fit values from @a from to @a to
* @param from Control points, must be sorted, must begins with 0 and ends
* with 255
* @param to
*/
Curve(const std::vector<uint8_t> &from, const std::vector<uint8_t> &to);
Curve(const Curve &) = default;
Curve(Curve &&) = default;
uint8_t fit(const uint8_t from) const { return lut[from]; }
private:
std::vector<uint8_t> lut;
};
} // namespace filter
} // namespace plugin

View file

@ -0,0 +1,170 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include "../math_util.h"
#include "./hslhsv.h"
using namespace std;
namespace plugin {
namespace filter {
array<float, 3> rgb8ToHsl(const uint8_t *rgb8) {
const auto max = std::max(std::max(rgb8[0], rgb8[1]), rgb8[2]);
const auto min = std::min(std::min(rgb8[0], rgb8[1]), rgb8[2]);
const auto chroma = max - min;
float rgbF[] = {rgb8[0] / 255.f, rgb8[1] / 255.f, rgb8[2] / 255.f};
const auto maxF = max / 255.f;
const auto minF = min / 255.f;
const auto chromaF = maxF - minF;
const auto l = (maxF + minF) / 2;
float h;
if (chroma == 0) {
h = 0;
} else if (max == rgb8[0]) {
h = fmodf(60 * (0 + (rgbF[1] - rgbF[2]) / chromaF) + 360, 360.f);
} else if (max == rgb8[1]) {
h = 60 * (2 + (rgbF[2] - rgbF[0]) / chromaF);
} else if (max == rgb8[2]) {
h = 60 * (4 + (rgbF[0] - rgbF[1]) / chromaF);
}
float s;
if (std::abs(l - 0) < 1e-3 || std::abs(l - 1) < 1e-3) {
s = 0;
} else {
s = chromaF / (1 - std::abs(2 * maxF - chromaF - 1));
}
return {h, s, l};
}
array<float, 3> rgb8ToHsv(const uint8_t *rgb8) {
const auto max = std::max(std::max(rgb8[0], rgb8[1]), rgb8[2]);
const auto min = std::min(std::min(rgb8[0], rgb8[1]), rgb8[2]);
const auto chroma = max - min;
float rgbF[] = {rgb8[0] / 255.f, rgb8[1] / 255.f, rgb8[2] / 255.f};
const auto maxF = max / 255.f;
const auto minF = min / 255.f;
const auto chromaF = maxF - minF;
float h;
if (chroma == 0) {
h = 0;
} else if (max == rgb8[0]) {
h = fmodf(60 * (0 + (rgbF[1] - rgbF[2]) / chromaF) + 360, 360.f);
} else if (max == rgb8[1]) {
h = 60 * (2 + (rgbF[2] - rgbF[0]) / chromaF);
} else if (max == rgb8[2]) {
h = 60 * (4 + (rgbF[0] - rgbF[1]) / chromaF);
}
float s;
if (max == 0) {
s = 0;
} else {
s = chromaF / maxF;
}
return {h, s, maxF};
}
array<uint8_t, 3> hslToRgb8(const float *hsl) {
const auto chroma = hsl[1] * (1 - std::abs(2 * hsl[2] - 1));
const auto h2 = hsl[0] / 60;
const auto x = chroma * (1 - std::abs(fmodf(h2, 2) - 1));
float r2, g2, b2;
if (0 <= h2 && h2 < 1) {
r2 = chroma;
g2 = x;
b2 = 0;
} else if (1 <= h2 && h2 < 2) {
r2 = x;
g2 = chroma;
b2 = 0;
} else if (2 <= h2 && h2 < 3) {
r2 = 0;
g2 = chroma;
b2 = x;
} else if (3 <= h2 && h2 < 4) {
r2 = 0;
g2 = x;
b2 = chroma;
} else if (4 <= h2 && h2 < 5) {
r2 = x;
g2 = 0;
b2 = chroma;
} else {
// 5 <= h2 && h2 < 6
r2 = chroma;
g2 = 0;
b2 = x;
}
const auto m = hsl[2] - chroma / 2;
return {static_cast<uint8_t>((r2 + m) * 255),
static_cast<uint8_t>((g2 + m) * 255),
static_cast<uint8_t>((b2 + m) * 255)};
}
array<uint8_t, 3> hsvToRgb8(const float *hsv) {
const auto chroma = hsv[2] * hsv[1];
const auto h2 = hsv[0] / 60;
const auto x = chroma * (1 - std::abs(fmodf(h2, 2) - 1));
float r2, g2, b2;
if (0 <= h2 && h2 < 1) {
r2 = chroma;
g2 = x;
b2 = 0;
} else if (1 <= h2 && h2 < 2) {
r2 = x;
g2 = chroma;
b2 = 0;
} else if (2 <= h2 && h2 < 3) {
r2 = 0;
g2 = chroma;
b2 = x;
} else if (3 <= h2 && h2 < 4) {
r2 = 0;
g2 = x;
b2 = chroma;
} else if (4 <= h2 && h2 < 5) {
r2 = x;
g2 = 0;
b2 = chroma;
} else {
// 5 <= h2 && h2 < 6
r2 = chroma;
g2 = 0;
b2 = x;
}
const auto m = hsv[2] - chroma;
return {static_cast<uint8_t>((r2 + m) * 255),
static_cast<uint8_t>((g2 + m) * 255),
static_cast<uint8_t>((b2 + m) * 255)};
}
std::array<float, 3> hslToHsv(const float *hsl) {
const auto v = hsl[2] + hsl[1] * std::min(hsl[2], 1 - hsl[2]);
float s;
if (std::abs(v - 0) < 1e-3) {
s = 0;
} else {
s = 2 * (1 - hsl[2] / v);
}
return {hsl[0], s, v};
}
std::array<float, 3> hsvToHsl(const float *hsv) {
const auto l = hsv[2] * (1 - hsv[1] / 2);
float s;
if (std::abs(l - 0) < 1e-3 || std::abs(l - 1) < 1e-3) {
s = 0;
} else {
s = (hsv[2] - l) / std::min(l, 1 - l);
}
return {hsv[0], s, l};
}
} // namespace filter
} // namespace plugin

View file

@ -0,0 +1,19 @@
#pragma once
#include <array>
#include <cstdint>
namespace plugin {
namespace filter {
std::array<float, 3> rgb8ToHsl(const uint8_t *rgb8);
std::array<float, 3> rgb8ToHsv(const uint8_t *rgb8);
std::array<uint8_t, 3> hslToRgb8(const float *hsl);
std::array<uint8_t, 3> hsvToRgb8(const float *hsv);
std::array<float, 3> hslToHsv(const float *hsl);
std::array<float, 3> hsvToHsl(const float *hsv);
} // namespace filter
} // namespace plugin

View file

@ -0,0 +1,77 @@
#include <cmath>
#include <cstdint>
#include <cstring>
#include <exception>
#include <jni.h>
#include <memory>
#include <vector>
#include "../exception.h"
#include "../log.h"
#include "../math_util.h"
#include "../util.h"
#include "./hslhsv.h"
using namespace plugin;
using namespace std;
namespace {
class Saturation {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static constexpr const char *TAG = "Saturation";
};
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_Saturation_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat value) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = Saturation().apply(
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, value);
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> Saturation::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
auto hsl = filter::rgb8ToHsl(rgba8 + p);
hsl[1] = clamp(0.f, hsl[1] * (1 + weight), 1.f);
const auto &newRgb = filter::hslToRgb8(hsl.data());
memcpy(output.data() + p, newRgb.data(), 3);
output[p + 3] = rgba8[p + 3];
}
return output;
}
} // namespace

View file

@ -0,0 +1,77 @@
#include <cstdint>
#include <cstring>
#include <exception>
#include <jni.h>
#include <vector>
#include "../exception.h"
#include "../log.h"
#include "../math_util.h"
#include "../util.h"
#include "./yuv.h"
using namespace plugin;
using namespace std;
namespace {
class Tint {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static constexpr const char *TAG = "Tint";
};
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_Tint_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat weight) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = Tint().apply(reinterpret_cast<uint8_t *>(cRgba8.get()),
width, height, weight);
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> Tint::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
auto yuv = filter::rgb8ToYuv(rgba8 + p);
// +-0.1
yuv[1] = clamp(0.f, yuv[1] + 0.1f * weight, 1.f);
yuv[2] = clamp(0.f, yuv[2] + 0.1f * weight, 1.f);
const auto &newRgb = filter::yuvToRgb8(yuv.data());
memcpy(output.data() + p, newRgb.data(), 3);
output[p + 3] = rgba8[p + 3];
}
return output;
}
} // namespace

View file

@ -0,0 +1,118 @@
#include <cmath>
#include <cstdint>
#include <exception>
#include <jni.h>
#include <memory>
#include <vector>
#include "../exception.h"
#include "../log.h"
#include "../util.h"
#include "./curve.h"
using namespace plugin;
using namespace std;
namespace {
class Warmth {
public:
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight);
private:
static filter::Curve getRCurve(const float weight);
static unique_ptr<filter::Curve> getGCurve(const float weight);
static filter::Curve getBCurve(const float weight);
static constexpr const char *TAG = "Warmth";
};
inline uint8_t weighted(const uint8_t from, const uint8_t to,
const float weight) {
return (to - from) * weight + from;
}
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_nkming_nc_1photos_plugin_image_1processor_Warmth_applyNative(
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
jfloat weight) {
try {
initOpenMp();
RaiiContainer<jbyte> cRgba8(
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
[&](jbyte *obj) {
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
});
const auto result = Warmth().apply(
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, weight);
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> Warmth::apply(const uint8_t *rgba8, const size_t width,
const size_t height, const float weight) {
LOGI(TAG, "[apply] weight: %.2f", weight);
if (weight == 0) {
// shortcut
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
}
const auto rCurve = getRCurve(weight);
const auto gCurve = getGCurve(weight);
const auto bCurve = getBCurve(weight);
vector<uint8_t> output(width * height * 4);
#pragma omp parallel for
for (size_t i = 0; i < width * height; ++i) {
const auto p = i * 4;
output[p + 0] = rCurve.fit(rgba8[p + 0]);
output[p + 1] = gCurve ? gCurve->fit(rgba8[p + 1]) : rgba8[p + 1];
output[p + 2] = bCurve.fit(rgba8[p + 2]);
output[p + 3] = rgba8[p + 3];
}
return output;
}
filter::Curve Warmth::getRCurve(const float weight) {
if (weight >= 0) {
return filter::Curve({0, 78, 195, 255}, {0, weighted(78, 100, weight),
weighted(195, 220, weight), 255});
} else {
return filter::Curve({0, 95, 220, 255},
{0, weighted(95, 60, std::abs(weight)),
weighted(220, 185, std::abs(weight)), 255});
}
}
unique_ptr<filter::Curve> Warmth::getGCurve(const float weight) {
if (weight >= 0) {
return make_unique<filter::Curve>(
vector<uint8_t>{0, 135, 255},
vector<uint8_t>{0, weighted(135, 125, weight), 255});
} else {
return nullptr;
}
}
filter::Curve Warmth::getBCurve(const float weight) {
if (weight >= 0) {
return filter::Curve({0, 95, 220, 255}, {0, weighted(95, 60, weight),
weighted(220, 185, weight), 255});
} else {
return filter::Curve({0, 78, 195, 255},
{0, weighted(78, 100, std::abs(weight)),
weighted(195, 220, std::abs(weight)), 255});
}
}
} // namespace

View file

@ -0,0 +1,40 @@
#include <array>
#include <cstdint>
#include "../math_util.h"
#include "./yuv.h"
using namespace std;
namespace plugin {
namespace filter {
array<float, 3> rgb8ToYuv(const uint8_t *rgb8) {
const float rgbF[] = {rgb8[0] / 255.f, rgb8[1] / 255.f, rgb8[2] / 255.f};
return {
clamp(0.f, rgbF[0] * .299f + rgbF[1] * .587f + rgbF[2] * .114f, 1.f),
clamp(0.f,
rgbF[0] * -.168736f + rgbF[1] * -.331264f + rgbF[2] * .5f + .5f,
1.f),
clamp(0.f,
rgbF[0] * .5f + rgbF[1] * -.418688f + rgbF[2] * -.081312f + .5f,
1.f),
};
}
array<uint8_t, 3> yuvToRgb8(const float *yuv) {
const float yuv_[] = {yuv[0], yuv[1] - .5f, yuv[2] - .5f};
const float rgbF[] = {
yuv_[0] + yuv_[2] * 1.4f,
yuv_[0] + yuv_[1] * -.343f + yuv_[2] * -.711f,
yuv_[0] + yuv_[1] * 1.765f,
};
return {
static_cast<uint8_t>(clamp<int>(0, rgbF[0] * 255, 255)),
static_cast<uint8_t>(clamp<int>(0, rgbF[1] * 255, 255)),
static_cast<uint8_t>(clamp<int>(0, rgbF[2] * 255, 255)),
};
}
} // namespace filter
} // namespace plugin

View file

@ -0,0 +1,26 @@
#pragma once
#include <array>
#include <cstdint>
namespace plugin {
namespace filter {
/**
* Map a RGB color to full range YCbCr
*
* @param rgb8
* @return Returned values are in the range of 0 to 1
*/
std::array<float, 3> rgb8ToYuv(const uint8_t *rgb8);
/**
* Map a full range YCbCr color to RGB
*
* @param yuv
* @return
*/
std::array<uint8_t, 3> yuvToRgb8(const float *yuv);
} // namespace filter
} // namespace plugin

View file

@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View file

@ -0,0 +1,763 @@
#include "./spline.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdio>
#include <sstream>
#include <string>
#include <vector>
namespace {
// band matrix solver
class band_matrix {
private:
std::vector<std::vector<double>> m_upper; // upper band
std::vector<std::vector<double>> m_lower; // lower band
public:
band_matrix(){}; // constructor
band_matrix(int dim, int n_u, int n_l); // constructor
~band_matrix(){}; // destructor
void resize(int dim, int n_u, int n_l); // init with dim,n_u,n_l
int dim() const; // matrix dimension
int num_upper() const { return (int)m_upper.size() - 1; }
int num_lower() const { return (int)m_lower.size() - 1; }
// access operator
double &operator()(int i, int j); // write
double operator()(int i, int j) const; // read
// we can store an additional diagonal (in m_lower)
double &saved_diag(int i);
double saved_diag(int i) const;
void lu_decompose();
std::vector<double> r_solve(const std::vector<double> &b) const;
std::vector<double> l_solve(const std::vector<double> &b) const;
std::vector<double> lu_solve(const std::vector<double> &b,
bool is_lu_decomposed = false);
};
double get_eps();
std::vector<double> solve_cubic(double a, double b, double c, double d,
int newton_iter = 0);
} // namespace
namespace tk {
void spline::set_boundary(spline::bd_type left, double left_value,
spline::bd_type right, double right_value) {
assert(m_x.size() == 0); // set_points() must not have happened yet
m_left = left;
m_right = right;
m_left_value = left_value;
m_right_value = right_value;
}
void spline::set_coeffs_from_b() {
assert(m_x.size() == m_y.size());
assert(m_x.size() == m_b.size());
assert(m_x.size() > 2);
size_t n = m_b.size();
if (m_c.size() != n)
m_c.resize(n);
if (m_d.size() != n)
m_d.resize(n);
for (size_t i = 0; i < n - 1; i++) {
const double h = m_x[i + 1] - m_x[i];
// from continuity and differentiability condition
m_c[i] =
(3.0 * (m_y[i + 1] - m_y[i]) / h - (2.0 * m_b[i] + m_b[i + 1])) / h;
// from differentiability condition
m_d[i] = ((m_b[i + 1] - m_b[i]) / (3.0 * h) - 2.0 / 3.0 * m_c[i]) / h;
}
// for left extrapolation coefficients
m_c0 = (m_left == first_deriv) ? 0.0 : m_c[0];
}
void spline::set_points(const std::vector<double> &x,
const std::vector<double> &y, spline_type type) {
assert(x.size() == y.size());
assert(x.size() >= 3);
// not-a-knot with 3 points has many solutions
if (m_left == not_a_knot || m_right == not_a_knot)
assert(x.size() >= 4);
m_type = type;
m_made_monotonic = false;
m_x = x;
m_y = y;
int n = (int)x.size();
// check strict monotonicity of input vector x
for (int i = 0; i < n - 1; i++) {
assert(m_x[i] < m_x[i + 1]);
}
if (type == linear) {
// linear interpolation
m_d.resize(n);
m_c.resize(n);
m_b.resize(n);
for (int i = 0; i < n - 1; i++) {
m_d[i] = 0.0;
m_c[i] = 0.0;
m_b[i] = (m_y[i + 1] - m_y[i]) / (m_x[i + 1] - m_x[i]);
}
// ignore boundary conditions, set slope equal to the last segment
m_b[n - 1] = m_b[n - 2];
m_c[n - 1] = 0.0;
m_d[n - 1] = 0.0;
} else if (type == cspline) {
// classical cubic splines which are C^2 (twice cont differentiable)
// this requires solving an equation system
// setting up the matrix and right hand side of the equation system
// for the parameters b[]
int n_upper = (m_left == spline::not_a_knot) ? 2 : 1;
int n_lower = (m_right == spline::not_a_knot) ? 2 : 1;
band_matrix A(n, n_upper, n_lower);
std::vector<double> rhs(n);
for (int i = 1; i < n - 1; i++) {
A(i, i - 1) = 1.0 / 3.0 * (x[i] - x[i - 1]);
A(i, i) = 2.0 / 3.0 * (x[i + 1] - x[i - 1]);
A(i, i + 1) = 1.0 / 3.0 * (x[i + 1] - x[i]);
rhs[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) -
(y[i] - y[i - 1]) / (x[i] - x[i - 1]);
}
// boundary conditions
if (m_left == spline::second_deriv) {
// 2*c[0] = f''
A(0, 0) = 2.0;
A(0, 1) = 0.0;
rhs[0] = m_left_value;
} else if (m_left == spline::first_deriv) {
// b[0] = f', needs to be re-expressed in terms of c:
// (2c[0]+c[1])(x[1]-x[0]) = 3 ((y[1]-y[0])/(x[1]-x[0]) - f')
A(0, 0) = 2.0 * (x[1] - x[0]);
A(0, 1) = 1.0 * (x[1] - x[0]);
rhs[0] = 3.0 * ((y[1] - y[0]) / (x[1] - x[0]) - m_left_value);
} else if (m_left == spline::not_a_knot) {
// f'''(x[1]) exists, i.e. d[0]=d[1], or re-expressed in c:
// -h1*c[0] + (h0+h1)*c[1] - h0*c[2] = 0
A(0, 0) = -(x[2] - x[1]);
A(0, 1) = x[2] - x[0];
A(0, 2) = -(x[1] - x[0]);
rhs[0] = 0.0;
} else {
assert(false);
}
if (m_right == spline::second_deriv) {
// 2*c[n-1] = f''
A(n - 1, n - 1) = 2.0;
A(n - 1, n - 2) = 0.0;
rhs[n - 1] = m_right_value;
} else if (m_right == spline::first_deriv) {
// b[n-1] = f', needs to be re-expressed in terms of c:
// (c[n-2]+2c[n-1])(x[n-1]-x[n-2])
// = 3 (f' - (y[n-1]-y[n-2])/(x[n-1]-x[n-2]))
A(n - 1, n - 1) = 2.0 * (x[n - 1] - x[n - 2]);
A(n - 1, n - 2) = 1.0 * (x[n - 1] - x[n - 2]);
rhs[n - 1] =
3.0 * (m_right_value - (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]));
} else if (m_right == spline::not_a_knot) {
// f'''(x[n-2]) exists, i.e. d[n-3]=d[n-2], or re-expressed in c:
// -h_{n-2}*c[n-3] + (h_{n-3}+h_{n-2})*c[n-2] - h_{n-3}*c[n-1] = 0
A(n - 1, n - 3) = -(x[n - 1] - x[n - 2]);
A(n - 1, n - 2) = x[n - 1] - x[n - 3];
A(n - 1, n - 1) = -(x[n - 2] - x[n - 3]);
rhs[0] = 0.0;
} else {
assert(false);
}
// solve the equation system to obtain the parameters c[]
m_c = A.lu_solve(rhs);
// calculate parameters b[] and d[] based on c[]
m_d.resize(n);
m_b.resize(n);
for (int i = 0; i < n - 1; i++) {
m_d[i] = 1.0 / 3.0 * (m_c[i + 1] - m_c[i]) / (x[i + 1] - x[i]);
m_b[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) -
1.0 / 3.0 * (2.0 * m_c[i] + m_c[i + 1]) * (x[i + 1] - x[i]);
}
// for the right extrapolation coefficients (zero cubic term)
// f_{n-1}(x) = y_{n-1} + b*(x-x_{n-1}) + c*(x-x_{n-1})^2
double h = x[n - 1] - x[n - 2];
// m_c[n-1] is determined by the boundary condition
m_d[n - 1] = 0.0;
m_b[n - 1] = 3.0 * m_d[n - 2] * h * h + 2.0 * m_c[n - 2] * h +
m_b[n - 2]; // = f'_{n-2}(x_{n-1})
if (m_right == first_deriv)
m_c[n - 1] = 0.0; // force linear extrapolation
} else if (type == cspline_hermite) {
// hermite cubic splines which are C^1 (cont. differentiable)
// and derivatives are specified on each grid point
// (here we use 3-point finite differences)
m_b.resize(n);
m_c.resize(n);
m_d.resize(n);
// set b to match 1st order derivative finite difference
for (int i = 1; i < n - 1; i++) {
const double h = m_x[i + 1] - m_x[i];
const double hl = m_x[i] - m_x[i - 1];
m_b[i] = -h / (hl * (hl + h)) * m_y[i - 1] +
(h - hl) / (hl * h) * m_y[i] + hl / (h * (hl + h)) * m_y[i + 1];
}
// boundary conditions determine b[0] and b[n-1]
if (m_left == first_deriv) {
m_b[0] = m_left_value;
} else if (m_left == second_deriv) {
const double h = m_x[1] - m_x[0];
m_b[0] = 0.5 *
(-m_b[1] - 0.5 * m_left_value * h + 3.0 * (m_y[1] - m_y[0]) / h);
} else if (m_left == not_a_knot) {
// f''' continuous at x[1]
const double h0 = m_x[1] - m_x[0];
const double h1 = m_x[2] - m_x[1];
m_b[0] = -m_b[1] + 2.0 * (m_y[1] - m_y[0]) / h0 +
h0 * h0 / (h1 * h1) *
(m_b[1] + m_b[2] - 2.0 * (m_y[2] - m_y[1]) / h1);
} else {
assert(false);
}
if (m_right == first_deriv) {
m_b[n - 1] = m_right_value;
m_c[n - 1] = 0.0;
} else if (m_right == second_deriv) {
const double h = m_x[n - 1] - m_x[n - 2];
m_b[n - 1] = 0.5 * (-m_b[n - 2] + 0.5 * m_right_value * h +
3.0 * (m_y[n - 1] - m_y[n - 2]) / h);
m_c[n - 1] = 0.5 * m_right_value;
} else if (m_right == not_a_knot) {
// f''' continuous at x[n-2]
const double h0 = m_x[n - 2] - m_x[n - 3];
const double h1 = m_x[n - 1] - m_x[n - 2];
m_b[n - 1] =
-m_b[n - 2] + 2.0 * (m_y[n - 1] - m_y[n - 2]) / h1 +
h1 * h1 / (h0 * h0) *
(m_b[n - 3] + m_b[n - 2] - 2.0 * (m_y[n - 2] - m_y[n - 3]) / h0);
// f'' continuous at x[n-1]: c[n-1] = 3*d[n-2]*h[n-2] + c[n-1]
m_c[n - 1] = (m_b[n - 2] + 2.0 * m_b[n - 1]) / h1 -
3.0 * (m_y[n - 1] - m_y[n - 2]) / (h1 * h1);
} else {
assert(false);
}
m_d[n - 1] = 0.0;
// parameters c and d are determined by continuity and differentiability
set_coeffs_from_b();
} else {
assert(false);
}
// for left extrapolation coefficients
m_c0 = (m_left == first_deriv) ? 0.0 : m_c[0];
}
bool spline::make_monotonic() {
assert(m_x.size() == m_y.size());
assert(m_x.size() == m_b.size());
assert(m_x.size() > 2);
bool modified = false;
const int n = (int)m_x.size();
// make sure: input data monotonic increasing --> b_i>=0
// input data monotonic decreasing --> b_i<=0
for (int i = 0; i < n; i++) {
int im1 = std::max(i - 1, 0);
int ip1 = std::min(i + 1, n - 1);
if (((m_y[im1] <= m_y[i]) && (m_y[i] <= m_y[ip1]) && m_b[i] < 0.0) ||
((m_y[im1] >= m_y[i]) && (m_y[i] >= m_y[ip1]) && m_b[i] > 0.0)) {
modified = true;
m_b[i] = 0.0;
}
}
// if input data is monotonic (b[i], b[i+1], avg have all the same sign)
// ensure a sufficient criteria for monotonicity is satisfied:
// sqrt(b[i]^2+b[i+1]^2) <= 3 |avg|, with avg=(y[i+1]-y[i])/h,
for (int i = 0; i < n - 1; i++) {
double h = m_x[i + 1] - m_x[i];
double avg = (m_y[i + 1] - m_y[i]) / h;
if (avg == 0.0 && (m_b[i] != 0.0 || m_b[i + 1] != 0.0)) {
modified = true;
m_b[i] = 0.0;
m_b[i + 1] = 0.0;
} else if ((m_b[i] >= 0.0 && m_b[i + 1] >= 0.0 && avg > 0.0) ||
(m_b[i] <= 0.0 && m_b[i + 1] <= 0.0 && avg < 0.0)) {
// input data is monotonic
double r =
sqrt(m_b[i] * m_b[i] + m_b[i + 1] * m_b[i + 1]) / std::fabs(avg);
if (r > 3.0) {
// sufficient criteria for monotonicity: r<=3
// adjust b[i] and b[i+1]
modified = true;
m_b[i] *= (3.0 / r);
m_b[i + 1] *= (3.0 / r);
}
}
}
if (modified == true) {
set_coeffs_from_b();
m_made_monotonic = true;
}
return modified;
}
// return the closest idx so that m_x[idx] <= x (return 0 if x<m_x[0])
size_t spline::find_closest(double x) const {
std::vector<double>::const_iterator it;
it = std::upper_bound(m_x.begin(), m_x.end(), x); // *it > x
size_t idx = std::max(int(it - m_x.begin()) - 1, 0); // m_x[idx] <= x
return idx;
}
double spline::operator()(double x) const {
// polynomial evaluation using Horner's scheme
// TODO: consider more numerically accurate algorithms, e.g.:
// - Clenshaw
// - Even-Odd method by A.C.R. Newbery
// - Compensated Horner Scheme
size_t n = m_x.size();
size_t idx = find_closest(x);
double h = x - m_x[idx];
double interpol;
if (x < m_x[0]) {
// extrapolation to the left
interpol = (m_c0 * h + m_b[0]) * h + m_y[0];
} else if (x > m_x[n - 1]) {
// extrapolation to the right
interpol = (m_c[n - 1] * h + m_b[n - 1]) * h + m_y[n - 1];
} else {
// interpolation
interpol = ((m_d[idx] * h + m_c[idx]) * h + m_b[idx]) * h + m_y[idx];
}
return interpol;
}
double spline::deriv(int order, double x) const {
assert(order > 0);
size_t n = m_x.size();
size_t idx = find_closest(x);
double h = x - m_x[idx];
double interpol;
if (x < m_x[0]) {
// extrapolation to the left
switch (order) {
case 1:
interpol = 2.0 * m_c0 * h + m_b[0];
break;
case 2:
interpol = 2.0 * m_c0;
break;
default:
interpol = 0.0;
break;
}
} else if (x > m_x[n - 1]) {
// extrapolation to the right
switch (order) {
case 1:
interpol = 2.0 * m_c[n - 1] * h + m_b[n - 1];
break;
case 2:
interpol = 2.0 * m_c[n - 1];
break;
default:
interpol = 0.0;
break;
}
} else {
// interpolation
switch (order) {
case 1:
interpol = (3.0 * m_d[idx] * h + 2.0 * m_c[idx]) * h + m_b[idx];
break;
case 2:
interpol = 6.0 * m_d[idx] * h + 2.0 * m_c[idx];
break;
case 3:
interpol = 6.0 * m_d[idx];
break;
default:
interpol = 0.0;
break;
}
}
return interpol;
}
std::vector<double> spline::solve(double y, bool ignore_extrapolation) const {
std::vector<double> x; // roots for the entire spline
std::vector<double> root; // roots for each piecewise cubic
const size_t n = m_x.size();
// left extrapolation
if (ignore_extrapolation == false) {
root = solve_cubic(m_y[0] - y, m_b[0], m_c0, 0.0, 1);
for (size_t j = 0; j < root.size(); j++) {
if (root[j] < 0.0) {
x.push_back(m_x[0] + root[j]);
}
}
}
// brute force check if piecewise cubic has roots in their resp. segment
// TODO: make more efficient
for (size_t i = 0; i < n - 1; i++) {
root = solve_cubic(m_y[i] - y, m_b[i], m_c[i], m_d[i], 1);
for (size_t j = 0; j < root.size(); j++) {
double h = (i > 0) ? (m_x[i] - m_x[i - 1]) : 0.0;
double eps = get_eps() * 512.0 * std::min(h, 1.0);
if ((-eps <= root[j]) && (root[j] < m_x[i + 1] - m_x[i])) {
double new_root = m_x[i] + root[j];
if (x.size() > 0 && x.back() + eps > new_root) {
x.back() = new_root; // avoid spurious duplicate roots
} else {
x.push_back(new_root);
}
}
}
}
// right extrapolation
if (ignore_extrapolation == false) {
root = solve_cubic(m_y[n - 1] - y, m_b[n - 1], m_c[n - 1], 0.0, 1);
for (size_t j = 0; j < root.size(); j++) {
if (0.0 <= root[j]) {
x.push_back(m_x[n - 1] + root[j]);
}
}
}
return x;
};
std::string spline::info() const {
std::stringstream ss;
ss << "type " << m_type << ", left boundary deriv " << m_left << " = ";
ss << m_left_value << ", right boundary deriv " << m_right << " = ";
ss << m_right_value << std::endl;
if (m_made_monotonic) {
ss << "(spline has been adjusted for piece-wise monotonicity)";
}
return ss.str();
}
} // namespace tk
namespace {
// band_matrix implementation
// -------------------------
band_matrix::band_matrix(int dim, int n_u, int n_l) { resize(dim, n_u, n_l); }
void band_matrix::resize(int dim, int n_u, int n_l) {
assert(dim > 0);
assert(n_u >= 0);
assert(n_l >= 0);
m_upper.resize(n_u + 1);
m_lower.resize(n_l + 1);
for (size_t i = 0; i < m_upper.size(); i++) {
m_upper[i].resize(dim);
}
for (size_t i = 0; i < m_lower.size(); i++) {
m_lower[i].resize(dim);
}
}
int band_matrix::dim() const {
if (m_upper.size() > 0) {
return m_upper[0].size();
} else {
return 0;
}
}
// defines the new operator (), so that we can access the elements
// by A(i,j), index going from i=0,...,dim()-1
double &band_matrix::operator()(int i, int j) {
int k = j - i; // what band is the entry
assert((i >= 0) && (i < dim()) && (j >= 0) && (j < dim()));
assert((-num_lower() <= k) && (k <= num_upper()));
// k=0 -> diagonal, k<0 lower left part, k>0 upper right part
if (k >= 0)
return m_upper[k][i];
else
return m_lower[-k][i];
}
double band_matrix::operator()(int i, int j) const {
int k = j - i; // what band is the entry
assert((i >= 0) && (i < dim()) && (j >= 0) && (j < dim()));
assert((-num_lower() <= k) && (k <= num_upper()));
// k=0 -> diagonal, k<0 lower left part, k>0 upper right part
if (k >= 0)
return m_upper[k][i];
else
return m_lower[-k][i];
}
// second diag (used in LU decomposition), saved in m_lower
double band_matrix::saved_diag(int i) const {
assert((i >= 0) && (i < dim()));
return m_lower[0][i];
}
double &band_matrix::saved_diag(int i) {
assert((i >= 0) && (i < dim()));
return m_lower[0][i];
}
// LR-Decomposition of a band matrix
void band_matrix::lu_decompose() {
int i_max, j_max;
int j_min;
double x;
// preconditioning
// normalize column i so that a_ii=1
for (int i = 0; i < this->dim(); i++) {
assert(this->operator()(i, i) != 0.0);
this->saved_diag(i) = 1.0 / this->operator()(i, i);
j_min = std::max(0, i - this->num_lower());
j_max = std::min(this->dim() - 1, i + this->num_upper());
for (int j = j_min; j <= j_max; j++) {
this->operator()(i, j) *= this->saved_diag(i);
}
this->operator()(i, i) = 1.0; // prevents rounding errors
}
// Gauss LR-Decomposition
for (int k = 0; k < this->dim(); k++) {
i_max = std::min(this->dim() - 1,
k + this->num_lower()); // num_lower not a mistake!
for (int i = k + 1; i <= i_max; i++) {
assert(this->operator()(k, k) != 0.0);
x = -this->operator()(i, k) / this->operator()(k, k);
this->operator()(i, k) = -x; // assembly part of L
j_max = std::min(this->dim() - 1, k + this->num_upper());
for (int j = k + 1; j <= j_max; j++) {
// assembly part of R
this->operator()(i, j) =
this->operator()(i, j) + x * this->operator()(k, j);
}
}
}
}
// solves Ly=b
std::vector<double> band_matrix::l_solve(const std::vector<double> &b) const {
assert(this->dim() == (int)b.size());
std::vector<double> x(this->dim());
int j_start;
double sum;
for (int i = 0; i < this->dim(); i++) {
sum = 0;
j_start = std::max(0, i - this->num_lower());
for (int j = j_start; j < i; j++)
sum += this->operator()(i, j) * x[j];
x[i] = (b[i] * this->saved_diag(i)) - sum;
}
return x;
}
// solves Rx=y
std::vector<double> band_matrix::r_solve(const std::vector<double> &b) const {
assert(this->dim() == (int)b.size());
std::vector<double> x(this->dim());
int j_stop;
double sum;
for (int i = this->dim() - 1; i >= 0; i--) {
sum = 0;
j_stop = std::min(this->dim() - 1, i + this->num_upper());
for (int j = i + 1; j <= j_stop; j++)
sum += this->operator()(i, j) * x[j];
x[i] = (b[i] - sum) / this->operator()(i, i);
}
return x;
}
std::vector<double> band_matrix::lu_solve(const std::vector<double> &b,
bool is_lu_decomposed) {
assert(this->dim() == (int)b.size());
std::vector<double> x, y;
if (is_lu_decomposed == false) {
this->lu_decompose();
}
y = this->l_solve(b);
x = this->r_solve(y);
return x;
}
// machine precision of a double, i.e. the successor of 1 is 1+eps
double get_eps() {
// return std::numeric_limits<double>::epsilon(); // __DBL_EPSILON__
return 2.2204460492503131e-16; // 2^-52
}
// solutions for a + b*x = 0
std::vector<double> solve_linear(double a, double b) {
std::vector<double> x; // roots
if (b == 0.0) {
if (a == 0.0) {
// 0*x = 0
x.resize(1);
x[0] = 0.0; // any x solves it but we need to pick one
return x;
} else {
// 0*x + ... = 0, no solution
return x;
}
} else {
x.resize(1);
x[0] = -a / b;
return x;
}
}
// solutions for a + b*x + c*x^2 = 0
std::vector<double> solve_quadratic(double a, double b, double c,
int newton_iter = 0) {
if (c == 0.0) {
return solve_linear(a, b);
}
// rescale so that we solve x^2 + 2p x + q = (x+p)^2 + q - p^2 = 0
double p = 0.5 * b / c;
double q = a / c;
double discr = p * p - q;
const double eps = 0.5 * get_eps();
double discr_err = (6.0 * (p * p) + 3.0 * fabs(q) + fabs(discr)) * eps;
std::vector<double> x; // roots
if (fabs(discr) <= discr_err) {
// discriminant is zero --> one root
x.resize(1);
x[0] = -p;
} else if (discr < 0) {
// no root
} else {
// two roots
x.resize(2);
x[0] = -p - sqrt(discr);
x[1] = -p + sqrt(discr);
}
// improve solution via newton steps
for (size_t i = 0; i < x.size(); i++) {
for (int k = 0; k < newton_iter; k++) {
double f = (c * x[i] + b) * x[i] + a;
double f1 = 2.0 * c * x[i] + b;
// only adjust if slope is large enough
if (fabs(f1) > 1e-8) {
x[i] -= f / f1;
}
}
}
return x;
}
// solutions for the cubic equation: a + b*x +c*x^2 + d*x^3 = 0
// this is a naive implementation of the analytic solution without
// optimisation for speed or numerical accuracy
// newton_iter: number of newton iterations to improve analytical solution
// see also
// gsl: gsl_poly_solve_cubic() in solve_cubic.c
// octave: roots.m - via eigenvalues of the Frobenius companion matrix
std::vector<double> solve_cubic(double a, double b, double c, double d,
int newton_iter) {
if (d == 0.0) {
return solve_quadratic(a, b, c, newton_iter);
}
// convert to normalised form: a + bx + cx^2 + x^3 = 0
if (d != 1.0) {
a /= d;
b /= d;
c /= d;
}
// convert to depressed cubic: z^3 - 3pz - 2q = 0
// via substitution: z = x + c/3
std::vector<double> z; // roots of the depressed cubic
double p = -(1.0 / 3.0) * b + (1.0 / 9.0) * (c * c);
double r = 2.0 * (c * c) - 9.0 * b;
double q = -0.5 * a - (1.0 / 54.0) * (c * r);
double discr = p * p * p - q * q; // discriminant
// calculating numerical round-off errors with assumptions:
// - each operation is precise but each intermediate result x
// when stored has max error of x*eps
// - only multiplication with a power of 2 introduces no new error
// - a,b,c,d and some fractions (e.g. 1/3) have rounding errors eps
// - p_err << |p|, q_err << |q|, ... (this is violated in rare cases)
// would be more elegant to use boost::numeric::interval<double>
const double eps = get_eps();
double p_err =
eps * ((3.0 / 3.0) * fabs(b) + (4.0 / 9.0) * (c * c) + fabs(p));
double r_err = eps * (6.0 * (c * c) + 18.0 * fabs(b) + fabs(r));
double q_err = 0.5 * fabs(a) * eps +
(1.0 / 54.0) * fabs(c) * (r_err + fabs(r) * 3.0 * eps) +
fabs(q) * eps;
double discr_err = (p * p) * (3.0 * p_err + fabs(p) * 2.0 * eps) +
fabs(q) * (2.0 * q_err + fabs(q) * eps) +
fabs(discr) * eps;
// depending on the discriminant we get different solutions
if (fabs(discr) <= discr_err) {
// discriminant zero: one or two real roots
if (fabs(p) <= p_err) {
// p and q are zero: single root
z.resize(1);
z[0] = 0.0; // triple root
} else {
z.resize(2);
z[0] = 2.0 * q / p; // single root
z[1] = -0.5 * z[0]; // double root
}
} else if (discr > 0) {
// three real roots: via trigonometric solution
z.resize(3);
double ac = (1.0 / 3.0) * acos(q / (p * sqrt(p)));
double sq = 2.0 * sqrt(p);
z[0] = sq * cos(ac);
z[1] = sq * cos(ac - 2.0 * M_PI / 3.0);
z[2] = sq * cos(ac - 4.0 * M_PI / 3.0);
} else if (discr < 0.0) {
// single real root: via Cardano's fromula
z.resize(1);
double sgnq = (q >= 0 ? 1 : -1);
double basis = fabs(q) + sqrt(-discr);
double C = sgnq * pow(basis, 1.0 / 3.0); // c++11 has std::cbrt()
z[0] = C + p / C;
}
for (size_t i = 0; i < z.size(); i++) {
// convert depressed cubic roots to original cubic: x = z - c/3
z[i] -= (1.0 / 3.0) * c;
// improve solution via newton steps
for (int k = 0; k < newton_iter; k++) {
double f = ((z[i] + c) * z[i] + b) * z[i] + a;
double f1 = (3.0 * z[i] + 2.0 * c) * z[i] + b;
// only adjust if slope is large enough
if (fabs(f1) > 1e-8) {
z[i] -= f / f1;
}
}
}
// ensure if a=0 we get exactly x=0 as root
// TODO: remove this fudge
if (a == 0.0) {
assert(z.size() > 0); // cubic should always have at least one root
double xmin = fabs(z[0]);
size_t imin = 0;
for (size_t i = 1; i < z.size(); i++) {
if (xmin > fabs(z[i])) {
xmin = fabs(z[i]);
imin = i;
}
}
z[imin] = 0.0; // replace the smallest absolute value with 0
}
std::sort(z.begin(), z.end());
return z;
}
} // namespace

View file

@ -0,0 +1,123 @@
/*
* spline.h
*
* simple cubic spline interpolation library without external
* dependencies
*
* ---------------------------------------------------------------------
* Copyright (C) 2011, 2014, 2016, 2021 Tino Kluge (ttk448 at gmail.com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*
*/
#pragma once
#include <cassert>
#include <string>
#include <vector>
namespace tk {
// spline interpolation
class spline {
public:
// spline types
enum spline_type {
linear = 10, // linear interpolation
cspline = 30, // cubic splines (classical C^2)
cspline_hermite = 31 // cubic hermite splines (local, only C^1)
};
// boundary condition type for the spline end-points
enum bd_type { first_deriv = 1, second_deriv = 2, not_a_knot = 3 };
protected:
std::vector<double> m_x, m_y; // x,y coordinates of points
// interpolation parameters
// f(x) = a_i + b_i*(x-x_i) + c_i*(x-x_i)^2 + d_i*(x-x_i)^3
// where a_i = y_i, or else it won't go through grid points
std::vector<double> m_b, m_c, m_d; // spline coefficients
double m_c0; // for left extrapolation
spline_type m_type;
bd_type m_left, m_right;
double m_left_value, m_right_value;
bool m_made_monotonic;
void set_coeffs_from_b(); // calculate c_i, d_i from b_i
size_t find_closest(double x) const; // closest idx so that m_x[idx]<=x
public:
// default constructor: set boundary condition to be zero curvature
// at both ends, i.e. natural splines
spline()
: m_type(cspline), m_left(second_deriv), m_right(second_deriv),
m_left_value(0.0), m_right_value(0.0), m_made_monotonic(false) {
;
}
spline(const std::vector<double> &X, const std::vector<double> &Y,
spline_type type = cspline, bool make_monotonic = false,
bd_type left = second_deriv, double left_value = 0.0,
bd_type right = second_deriv, double right_value = 0.0)
: m_type(type), m_left(left), m_right(right), m_left_value(left_value),
m_right_value(right_value),
m_made_monotonic(false) // false correct here: make_monotonic() sets it
{
this->set_points(X, Y, m_type);
if (make_monotonic) {
this->make_monotonic();
}
}
// modify boundary conditions: if called it must be before set_points()
void set_boundary(bd_type left, double left_value, bd_type right,
double right_value);
// set all data points (cubic_spline=false means linear interpolation)
void set_points(const std::vector<double> &x, const std::vector<double> &y,
spline_type type = cspline);
// adjust coefficients so that the spline becomes piecewise monotonic
// where possible
// this is done by adjusting slopes at grid points by a non-negative
// factor and this will break C^2
// this can also break boundary conditions if adjustments need to
// be made at the boundary points
// returns false if no adjustments have been made, true otherwise
bool make_monotonic();
// evaluates the spline at point x
double operator()(double x) const;
double deriv(int order, double x) const;
// solves for all x so that: spline(x) = y
std::vector<double> solve(double y, bool ignore_extrapolation = true) const;
// returns the input data points
std::vector<double> get_x() const { return m_x; }
std::vector<double> get_y() const { return m_y; }
double get_x_min() const {
assert(!m_x.empty());
return m_x.front();
}
double get_x_max() const {
assert(!m_x.empty());
return m_x.back();
}
// spline info string, i.e. spline type, boundary conditions etc.
std::string info() const;
};
} // namespace tkPLINE_H */

View file

@ -0,0 +1,65 @@
package com.nkming.nc_photos.plugin
import android.content.Context
import android.net.Uri
import com.nkming.nc_photos.plugin.image_processor.Rgba8Image
import com.nkming.nc_photos.plugin.image_processor.TfLiteHelper
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ImageLoaderChannelHandler(context: Context) :
MethodChannel.MethodCallHandler {
companion object {
const val METHOD_CHANNEL = "${K.LIB_ID}/image_loader_method"
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"loadUri" -> {
try {
loadUri(
call.argument("fileUri")!!,
call.argument("maxWidth")!!,
call.argument("maxHeight")!!,
call.argument("resizeMethod")!!,
call.argument("isAllowSwapSide")!!,
call.argument("shouldUpscale")!!,
result
)
} catch (e: Throwable) {
result.error("systemException", e.toString(), null)
}
}
else -> result.notImplemented()
}
}
/**
* Load and resize an image pointed by a uri
*
* @param fileUri
* @param maxWidth
* @param maxHeight
* @param resizeMethod
* @param isAllowSwapSide
* @param shouldUpscale
* @param result
*/
private fun loadUri(
fileUri: String, maxWidth: Int, maxHeight: Int, resizeMethod: Int,
isAllowSwapSide: Boolean, shouldUpscale: Boolean,
result: MethodChannel.Result
) {
val image = BitmapUtil.loadImage(
context, Uri.parse(fileUri), maxWidth, maxHeight,
BitmapResizeMethod.values()[resizeMethod], isAllowSwapSide,
shouldUpscale
).use {
Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height)
}
result.success(image.toJson())
}
private val context = context
}

View file

@ -4,9 +4,11 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.nkming.nc_photos.plugin.image_processor.*
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.Serializable
class ImageProcessorChannelHandler(context: Context) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
@ -86,6 +88,36 @@ class ImageProcessorChannelHandler(context: Context) :
}
}
"colorFilter" -> {
try {
colorFilter(
call.argument("fileUrl")!!,
call.argument("headers"),
call.argument("filename")!!,
call.argument("maxWidth")!!,
call.argument("maxHeight")!!,
call.argument("filters")!!,
result
)
} catch (e: Throwable) {
logE(TAG, "Uncaught exception", e)
result.error("systemException", e.toString(), null)
}
}
"filterPreview" -> {
try {
filterPreview(
call.argument("rgba8")!!,
call.argument("filters")!!,
result
)
} catch (e: Throwable) {
logE(TAG, "Uncaught exception", e)
result.error("systemException", e.toString(), null)
}
}
else -> result.notImplemented()
}
}
@ -142,6 +174,34 @@ class ImageProcessorChannelHandler(context: Context) :
}
)
private fun colorFilter(
fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, filters: List<Map<String, Any>>,
result: MethodChannel.Result
) {
// convert to serializable
val l = arrayListOf<Serializable>()
filters.mapTo(l, { HashMap(it) })
method(
fileUrl, headers, filename, maxWidth, maxHeight,
ImageProcessorService.METHOD_COLOR_FILTER, result,
onIntent = {
it.putExtra(ImageProcessorService.EXTRA_FILTERS, l)
}
)
}
private fun filterPreview(
rgba8: Map<String, Any>, filters: List<Map<String, Any>>,
result: MethodChannel.Result
) {
var img = Rgba8Image.fromJson(rgba8)
for (f in filters.map(ColorFilter::fromJson)) {
img = f.apply(img)
}
result.success(img.toJson())
}
private fun method(
fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, method: String,
@ -165,3 +225,24 @@ class ImageProcessorChannelHandler(context: Context) :
private val context = context
private var eventSink: EventChannel.EventSink? = null
}
interface ColorFilter {
companion object {
fun fromJson(json: Map<String, Any>): ColorFilter {
return when (json["type"]) {
"brightness" -> Brightness((json["weight"] as Double).toFloat())
"contrast" -> Contrast((json["weight"] as Double).toFloat())
"whitePoint" -> WhitePoint((json["weight"] as Double).toFloat())
"blackPoint" -> BlackPoint((json["weight"] as Double).toFloat())
"saturation" -> Saturation((json["weight"] as Double).toFloat())
"warmth" -> Warmth((json["weight"] as Double).toFloat())
"tint" -> Tint((json["weight"] as Double).toFloat())
else -> throw IllegalArgumentException(
"Unknown type: ${json["type"]}"
)
}
}
}
fun apply(rgba8: Rgba8Image): Rgba8Image
}

View file

@ -17,11 +17,9 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.exifinterface.media.ExifInterface
import com.nkming.nc_photos.plugin.image_processor.ArbitraryStyleTransfer
import com.nkming.nc_photos.plugin.image_processor.DeepLab3Portrait
import com.nkming.nc_photos.plugin.image_processor.Esrgan
import com.nkming.nc_photos.plugin.image_processor.ZeroDce
import com.nkming.nc_photos.plugin.image_processor.*
import java.io.File
import java.io.Serializable
import java.net.HttpURLConnection
import java.net.URL
@ -32,6 +30,7 @@ class ImageProcessorService : Service() {
const val METHOD_DEEP_LAP_PORTRAIT = "DeepLab3Portrait"
const val METHOD_ESRGAN = "Esrgan"
const val METHOD_ARBITRARY_STYLE_TRANSFER = "ArbitraryStyleTransfer"
const val METHOD_COLOR_FILTER = "ColorFilter"
const val EXTRA_FILE_URL = "fileUrl"
const val EXTRA_HEADERS = "headers"
const val EXTRA_FILENAME = "filename"
@ -41,6 +40,17 @@ class ImageProcessorService : Service() {
const val EXTRA_ITERATION = "iteration"
const val EXTRA_STYLE_URI = "styleUri"
const val EXTRA_WEIGHT = "weight"
const val EXTRA_FILTERS = "filters"
val ENHANCE_METHODS = listOf(
METHOD_ZERO_DCE,
METHOD_DEEP_LAP_PORTRAIT,
METHOD_ESRGAN,
METHOD_ARBITRARY_STYLE_TRANSFER,
)
val EDIT_METHODS = listOf(
METHOD_COLOR_FILTER,
)
private const val ACTION_CANCEL = "cancel"
@ -116,6 +126,7 @@ class ImageProcessorService : Service() {
METHOD_ARBITRARY_STYLE_TRANSFER -> onArbitraryStyleTransfer(
startId, intent.extras!!
)
METHOD_COLOR_FILTER -> onColorFilter(startId, intent.extras!!)
else -> {
logE(TAG, "Unknown method: $method")
// we can't call stopSelf here as it'll stop the service even if
@ -159,6 +170,18 @@ class ImageProcessorService : Service() {
)
}
private fun onColorFilter(startId: Int, extras: Bundle) {
val filters = extras.getSerializable(EXTRA_FILTERS)!!
.asType<ArrayList<Serializable>>()
.map { it.asType<HashMap<String, Any>>() }
return onMethod(
startId, extras, METHOD_COLOR_FILTER,
args = mapOf(
"filters" to filters,
)
)
}
/**
* Handle methods without arguments
*
@ -231,7 +254,7 @@ class ImageProcessorService : Service() {
)
return NotificationCompat.Builder(this, CHANNEL_ID).run {
setSmallIcon(R.drawable.outline_image_white_24)
setContentTitle("Successfully enhanced image")
setContentTitle("Successfully processed image")
setContentText("Tap to view the result")
setContentIntent(pi)
setAutoCancel(true)
@ -244,7 +267,7 @@ class ImageProcessorService : Service() {
): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID).run {
setSmallIcon(R.drawable.outline_error_outline_white_24)
setContentTitle("Failed enhancing image")
setContentTitle("Failed processing image")
setContentText(exception.message)
build()
}
@ -260,7 +283,7 @@ class ImageProcessorService : Service() {
)
return NotificationCompat.Builder(this, CHANNEL_ID).run {
setSmallIcon(R.drawable.outline_auto_fix_high_white_24)
setContentTitle("Preparing to restart photo enhancement")
setContentTitle("Preparing to restart photo processing")
addAction(
0, getString(android.R.string.cancel), cancelPendingIntent
)
@ -552,13 +575,28 @@ private open class ImageProcessorCommandTask(context: Context) :
cmd.args["weight"] as Float
).infer(fileUri)
ImageProcessorService.METHOD_COLOR_FILTER -> {
@Suppress("Unchecked_cast")
ColorFilterProcessor(
context, cmd.maxWidth, cmd.maxHeight,
cmd.args["filters"] as List<Map<String, Any>>,
).apply(fileUri)
}
else -> throw IllegalArgumentException(
"Unknown method: ${cmd.method}"
)
}
})
handleCancel()
saveBitmap(output, cmd.filename, file)
saveBitmap(
output, cmd.filename, file,
if (cmd.method in ImageProcessorService.EDIT_METHODS) {
"Edited Photos"
} else {
"Enhanced Photos"
}
)
} finally {
file.delete()
}
@ -598,7 +636,7 @@ private open class ImageProcessorCommandTask(context: Context) :
}
private fun saveBitmap(
bitmap: Bitmap, filename: String, srcFile: File
bitmap: Bitmap, filename: String, srcFile: File, subDir: String
): Uri {
logI(TAG, "[saveBitmap] $filename")
val outFile = File.createTempFile("out", null, getTempDir(context))
@ -619,7 +657,7 @@ private open class ImageProcessorCommandTask(context: Context) :
// move file to user accessible storage
val uri = MediaStoreUtil.copyFileToDownload(
context, Uri.fromFile(outFile), filename,
"Photos (for Nextcloud)/Enhanced Photos"
"Photos (for Nextcloud)/$subDir"
)
outFile.delete()
return uri

View file

@ -113,6 +113,14 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware,
PreferenceChannelHandler.METHOD_CHANNEL
)
preferenceMethodChannel.setMethodCallHandler(preferenceChannelHandler)
val imageLoaderChannelHandler =
ImageLoaderChannelHandler(flutterPluginBinding.applicationContext)
imageLoaderMethodChannel = MethodChannel(
flutterPluginBinding.binaryMessenger,
ImageLoaderChannelHandler.METHOD_CHANNEL
)
imageLoaderMethodChannel.setMethodCallHandler(imageLoaderChannelHandler)
}
override fun onDetachedFromEngine(
@ -131,6 +139,7 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware,
permissionMethodChannel.setMethodCallHandler(null)
logcatMethodChannel.setMethodCallHandler(null)
preferenceMethodChannel.setMethodCallHandler(null)
imageLoaderMethodChannel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
@ -219,6 +228,7 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware,
private lateinit var permissionMethodChannel: MethodChannel
private lateinit var logcatMethodChannel: MethodChannel
private lateinit var preferenceMethodChannel: MethodChannel
private lateinit var imageLoaderMethodChannel: MethodChannel
private lateinit var lockChannelHandler: LockChannelHandler
private lateinit var mediaStoreChannelHandler: MediaStoreChannelHandler

View file

@ -3,6 +3,7 @@ package com.nkming.nc_photos.plugin
import android.app.PendingIntent
import android.os.Build
import android.os.Bundle
import java.io.Serializable
import java.net.HttpURLConnection
fun getPendingIntentFlagImmutable(): Int {
@ -40,3 +41,6 @@ inline fun <T> measureTime(tag: String, message: String, block: () -> T): T {
}
fun Bundle.getIntOrNull(key: String) = get(key) as? Int
@Suppress("Unchecked_cast")
fun <T> Serializable.asType() = this as T

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class BlackPoint(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class Brightness(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -0,0 +1,37 @@
package com.nkming.nc_photos.plugin.image_processor
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import com.nkming.nc_photos.plugin.BitmapResizeMethod
import com.nkming.nc_photos.plugin.BitmapUtil
import com.nkming.nc_photos.plugin.ColorFilter
import com.nkming.nc_photos.plugin.use
class ColorFilterProcessor(
context: Context, maxWidth: Int, maxHeight: Int,
filters: List<Map<String, Any>>
) {
companion object {
const val TAG = "ColorFilterProcessor"
}
fun apply(imageUri: Uri): Bitmap {
var img = BitmapUtil.loadImage(
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false
).use {
Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height)
}
for (f in filters.map(ColorFilter::fromJson)) {
img = f.apply(img)
}
return img.toBitmap()
}
private val context = context
private val maxWidth = maxWidth
private val maxHeight = maxHeight
private val filters = filters
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class Contrast(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class Cool(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -0,0 +1,33 @@
package com.nkming.nc_photos.plugin.image_processor
import android.graphics.Bitmap
import java.nio.ByteBuffer
/**
* Container of pixel data stored in RGBA format
*/
class Rgba8Image(val pixel: ByteArray, val width: Int, val height: Int) {
companion object {
fun fromJson(json: Map<String, Any>) = Rgba8Image(
json["pixel"] as ByteArray, json["width"] as Int,
json["height"] as Int
)
}
fun toJson() = mapOf<String, Any>(
"pixel" to pixel,
"width" to width,
"height" to height,
)
fun toBitmap(): Bitmap {
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
.apply {
copyPixelsFromBuffer(ByteBuffer.wrap(pixel))
}
}
init {
assert(pixel.size == width * height * 4)
}
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class Saturation(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -1,6 +1,7 @@
package com.nkming.nc_photos.plugin.image_processor
import android.graphics.Bitmap
import java.nio.ByteBuffer
import java.nio.IntBuffer
interface TfLiteHelper {
@ -25,6 +26,18 @@ interface TfLiteHelper {
return rgb8
}
/**
* Convert an ARGB_8888 Android bitmap to a RGBA byte array
*
* @param bitmap
* @return
*/
fun bitmapToRgba8Array(bitmap: Bitmap): ByteArray {
val buffer = ByteBuffer.allocate(bitmap.width * bitmap.height * 4)
bitmap.copyPixelsToBuffer(buffer)
return buffer.array()
}
/**
* Convert a RGB8 byte array to an ARGB_8888 Android bitmap
*

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class Tint(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class Warmth(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin.image_processor
import com.nkming.nc_photos.plugin.ColorFilter
class WhitePoint(val weight: Float) : ColorFilter {
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
rgba8.width, rgba8.height
)
private external fun applyNative(
rgba8: ByteArray, width: Int, height: Int, weight: Float
): ByteArray
}

View file

@ -2,6 +2,8 @@ library nc_photos_plugin;
export 'src/content_uri.dart';
export 'src/exception.dart';
export 'src/image.dart';
export 'src/image_loader.dart';
export 'src/image_processor.dart';
export 'src/lock.dart';
export 'src/logcat.dart';

22
plugin/lib/src/image.dart Normal file
View file

@ -0,0 +1,22 @@
import 'dart:typed_data';
/// Container of pixel data stored in RGBA format
class Rgba8Image {
const Rgba8Image(this.pixel, this.width, this.height);
factory Rgba8Image.fromJson(Map<String, dynamic> json) => Rgba8Image(
json["pixel"],
json["width"],
json["height"],
);
Map<String, dynamic> toJson() => {
"pixel": pixel,
"width": width,
"height": height,
};
final Uint8List pixel;
final int width;
final int height;
}

View file

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:nc_photos_plugin/src/image.dart';
import 'package:nc_photos_plugin/src/k.dart' as k;
enum ImageLoaderResizeMethod {
fit,
fill,
}
class ImageLoader {
static Future<Rgba8Image> loadUri(
String fileUri,
int maxWidth,
int maxHeight,
ImageLoaderResizeMethod resizeMethod, {
bool isAllowSwapSide = false,
bool shouldUpscale = false,
}) async {
final result =
await _methodChannel.invokeMethod<Map>("loadUri", <String, dynamic>{
"fileUri": fileUri,
"maxWidth": maxWidth,
"maxHeight": maxHeight,
"resizeMethod": resizeMethod.index,
"isAllowSwapSide": isAllowSwapSide,
"shouldUpscale": shouldUpscale,
});
return Rgba8Image.fromJson(result!.cast<String, dynamic>());
}
static const _methodChannel = MethodChannel("${k.libId}/image_loader_method");
}

View file

@ -1,8 +1,49 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:nc_photos_plugin/src/image.dart';
import 'package:nc_photos_plugin/src/k.dart' as k;
abstract class ImageFilter {
Map<String, dynamic> toJson();
}
class ColorBrightnessFilter extends _SingleWeightFilter {
const ColorBrightnessFilter(double weight) : super("brightness", weight);
}
class ColorContrastFilter extends _SingleWeightFilter {
const ColorContrastFilter(double weight) : super("contrast", weight);
}
class ColorWhitePointFilter extends _SingleWeightFilter {
const ColorWhitePointFilter(double weight) : super("whitePoint", weight);
}
class ColorHighlightFilter extends _SingleWeightFilter {
const ColorHighlightFilter(double weight) : super("highlight", weight);
}
class ColorShadowFilter extends _SingleWeightFilter {
const ColorShadowFilter(double weight) : super("shadow", weight);
}
class ColorBlackPointFilter extends _SingleWeightFilter {
const ColorBlackPointFilter(double weight) : super("blackPoint", weight);
}
class ColorSaturationFilter extends _SingleWeightFilter {
const ColorSaturationFilter(double weight) : super("saturation", weight);
}
class ColorWarmthFilter extends _SingleWeightFilter {
const ColorWarmthFilter(double weight) : super("warmth", weight);
}
class ColorTintFilter extends _SingleWeightFilter {
const ColorTintFilter(double weight) : super("tint", weight);
}
class ImageProcessor {
static Future<void> zeroDce(
String fileUrl,
@ -72,6 +113,48 @@ class ImageProcessor {
"weight": weight,
});
static Future<void> colorFilter(
String fileUrl,
String filename,
int maxWidth,
int maxHeight,
List<ImageFilter> filters, {
Map<String, String>? headers,
}) =>
_methodChannel.invokeMethod("colorFilter", <String, dynamic>{
"fileUrl": fileUrl,
"headers": headers,
"filename": filename,
"maxWidth": maxWidth,
"maxHeight": maxHeight,
"filters": filters.map((f) => f.toJson()).toList(),
});
static Future<Rgba8Image> filterPreview(
Rgba8Image img,
List<ImageFilter> filters,
) async {
final result = await _methodChannel
.invokeMethod<Map>("filterPreview", <String, dynamic>{
"rgba8": img.toJson(),
"filters": filters.map((f) => f.toJson()).toList(),
});
return Rgba8Image.fromJson(result!.cast<String, dynamic>());
}
static const _methodChannel =
MethodChannel("${k.libId}/image_processor_method");
}
class _SingleWeightFilter implements ImageFilter {
const _SingleWeightFilter(this.type, this.weight);
@override
toJson() => {
"type": type,
"weight": weight,
};
final String type;
final double weight;
}