feat: crop save (save visible area at original resolution) v1.4.0
- Save visible area at original resolution as JPEG q90 (title bar button or Ctrl+S) - Output name: original name + _HHmmss suffix; no overwrite, warning shown - Active color effects baked in; rotation saves bounding box - New: crop_save_service, l10n strings TR/EN, docs/plan-crop-save.md
This commit is contained in:
1 parent
73c1ff51d6
commit
168e087c8b
16 files changed
+399
-14
No files matched your search
@@ -3,9 +3,10 @@ import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class CustomTitleBar extends StatelessWidget {
|
||||
final VoidCallback? onSettings;
|
||||
final VoidCallback? onSaveCrop;
|
||||
final bool visible;
|
||||
|
||||
const CustomTitleBar({super.key, this.onSettings, this.visible = true});
|
||||
const CustomTitleBar({super.key, this.onSettings, this.onSaveCrop, this.visible = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -43,6 +44,11 @@ class CustomTitleBar extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (onSaveCrop != null)
|
||||
_BarButton(
|
||||
icon: Icons.save_outlined,
|
||||
onTap: onSaveCrop!,
|
||||
),
|
||||
_BarButton(
|
||||
icon: Icons.horizontal_rule,
|
||||
onTap: () => windowManager.minimize(),
|
||||
|
||||
@@ -10,10 +10,10 @@ class ImageCanvas extends StatefulWidget {
|
||||
final String filePath;
|
||||
const ImageCanvas({super.key, required this.filePath});
|
||||
@override
|
||||
State<ImageCanvas> createState() => _ImageCanvasState();
|
||||
State<ImageCanvas> createState() => ImageCanvasState();
|
||||
}
|
||||
|
||||
class _ImageCanvasState extends State<ImageCanvas> {
|
||||
class ImageCanvasState extends State<ImageCanvas> {
|
||||
bool _isFilled = true;
|
||||
final GlobalKey _imageKey = GlobalKey();
|
||||
double _tx = 0, _ty = 0, _sc = 1.0;
|
||||
@@ -314,6 +314,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
static const double _bLum = 0.0722;
|
||||
|
||||
ColorFilter? _cachedFilter;
|
||||
List<double>? _cachedMatrix;
|
||||
double _cachedSat = -1;
|
||||
double _cachedCon = -1;
|
||||
double _cachedBri = double.nan;
|
||||
@@ -380,11 +381,60 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
];
|
||||
|
||||
// M_final = M_contrast * M_brightness * M_saturation
|
||||
_cachedFilter = ColorFilter.matrix(
|
||||
_multiply4x5(_multiply4x5(contrastMatrix, brightnessMatrix), saturationMatrix));
|
||||
final matrix =
|
||||
_multiply4x5(_multiply4x5(contrastMatrix, brightnessMatrix), saturationMatrix);
|
||||
_cachedMatrix = matrix;
|
||||
_cachedFilter = ColorFilter.matrix(matrix);
|
||||
return _cachedFilter!;
|
||||
}
|
||||
|
||||
/// Aktif renk matrisi (4x5 row-major); identity ise null.
|
||||
/// CropSaveService'e aynen taşınır (aynı alan/semantik: 0-255).
|
||||
List<double>? get activeColorMatrix {
|
||||
if (_saturation == 1.0 && _contrast == 1.0 && _brightness == 0.0) return null;
|
||||
_buildCombinedFilter();
|
||||
return _cachedMatrix;
|
||||
}
|
||||
|
||||
/// Viewport'un görünen bölgesini kaynak (tam çözünürlük) uzayına eşler.
|
||||
/// [w]x[h] = gerçek dosya boyutu. Degrade durumda null.
|
||||
Rect? cropSourceRect(int w, int h) {
|
||||
if (_vpSize.width <= 0 || _vpSize.height <= 0 || w <= 0 || h <= 0) return null;
|
||||
final cx = _vpSize.width / 2, cy = _vpSize.height / 2;
|
||||
final rad = -_angle * math.pi / 180;
|
||||
final cos = math.cos(rad), sin = math.sin(rad);
|
||||
|
||||
Offset toSrc(Offset p) {
|
||||
final double qx = (p.dx - _tx) / _sc, qy = (p.dy - _ty) / _sc;
|
||||
final double rx = (_angle == 0) ? qx : (cos * (qx - cx) - sin * (qy - cy) + cx);
|
||||
final double ry = (_angle == 0) ? qy : (sin * (qx - cx) + cos * (qy - cy) + cy);
|
||||
final double k = _isFilled
|
||||
? math.max(_vpSize.width / w, _vpSize.height / h)
|
||||
: math.min(_vpSize.width / w, _vpSize.height / h);
|
||||
final double ox = (_vpSize.width - w * k) / 2, oy = (_vpSize.height - h * k) / 2;
|
||||
return Offset((rx - ox) / k, (ry - oy) / k);
|
||||
}
|
||||
|
||||
final corners = [
|
||||
toSrc(Offset.zero),
|
||||
toSrc(Offset(_vpSize.width, 0)),
|
||||
toSrc(Offset(_vpSize.width, _vpSize.height)),
|
||||
toSrc(Offset(0, _vpSize.height)),
|
||||
];
|
||||
double xmin = double.infinity, xmax = -double.infinity;
|
||||
double ymin = double.infinity, ymax = -double.infinity;
|
||||
for (final c in corners) {
|
||||
xmin = math.min(xmin, c.dx); xmax = math.max(xmax, c.dx);
|
||||
ymin = math.min(ymin, c.dy); ymax = math.max(ymax, c.dy);
|
||||
}
|
||||
return Rect.fromLTRB(
|
||||
xmin.clamp(0.0, w.toDouble()),
|
||||
ymin.clamp(0.0, h.toDouble()),
|
||||
xmax.clamp(0.0, w.toDouble()),
|
||||
ymax.clamp(0.0, h.toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
FileImage? _cachedFileImage;
|
||||
ImageProvider<Object>? _cachedProvider;
|
||||
String? _cachedPath;
|
||||
|
||||
Reference in new issue
Block a user