fix: rotation and scale of placed signature on PDF are not sync
This commit is contained in:
parent
4f149656bd
commit
fba880e1be
|
@ -10,8 +10,9 @@ checkout [`docs/FRs.md`](docs/FRs.md)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# flutter clean
|
# flutter clean
|
||||||
# arb_translate
|
|
||||||
flutter pub get
|
flutter pub get
|
||||||
|
# arb_translate
|
||||||
|
# flutter gen-l10n
|
||||||
# > to generate gherkin test
|
# > to generate gherkin test
|
||||||
flutter pub run build_runner build --delete-conflicting-outputs
|
flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
# > to remove unused step definitions
|
# > to remove unused step definitions
|
||||||
|
|
|
@ -56,8 +56,8 @@ Design notes:
|
||||||
- Right pane: signatures drawer displaying saved signatures as cards.
|
- Right pane: signatures drawer displaying saved signatures as cards.
|
||||||
- able to drag and drop signature cards onto the PDF as placed signatures.
|
- able to drag and drop signature cards onto the PDF as placed signatures.
|
||||||
- Each signature card shows a preview.
|
- Each signature card shows a preview.
|
||||||
- long tap/right-click will show menu with options to delete, adjust graphic of image.
|
- long tap/right-click will show menu with options to delete, Adjust graphic of image.
|
||||||
- "adjust graphic" opens a simple image editor, which can remove backgrounds, Rotate (rotation handle).
|
- "Adjust graphic" opens a simple image editor, which can remove backgrounds, Rotate (rotation handle).
|
||||||
- There is an empty card with "new signature" prompt and 2 buttons: "from file" and "draw".
|
- There is an empty card with "new signature" prompt and 2 buttons: "from file" and "draw".
|
||||||
- "from file" opens a file picker to select an image as a signature card.
|
- "from file" opens a file picker to select an image as a signature card.
|
||||||
- "draw" opens a simple drawing interface (draw canvas) to create a signature card.
|
- "draw" opens a simple drawing interface (draw canvas) to create a signature card.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "Grafik anpassen",
|
||||||
"appTitle": "PDF-Signatur",
|
"appTitle": "PDF-Signatur",
|
||||||
"backgroundRemoval": "Hintergrund entfernen",
|
"backgroundRemoval": "Hintergrund entfernen",
|
||||||
"brightness": "Helligkeit",
|
"brightness": "Helligkeit",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
|
"adjustGraphic": "Adjust graphic",
|
||||||
|
"@adjustGraphic": {},
|
||||||
"appTitle": "PDF Signature",
|
"appTitle": "PDF Signature",
|
||||||
"@appTitle": {},
|
"@appTitle": {},
|
||||||
"backgroundRemoval": "Background removal",
|
"backgroundRemoval": "Background removal",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "Ajustar gráfico",
|
||||||
"appTitle": "Firma PDF",
|
"appTitle": "Firma PDF",
|
||||||
"backgroundRemoval": "Eliminar fondo",
|
"backgroundRemoval": "Eliminar fondo",
|
||||||
"brightness": "Brillo",
|
"brightness": "Brillo",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "Ajuster le graphique",
|
||||||
"appTitle": "Signature PDF",
|
"appTitle": "Signature PDF",
|
||||||
"backgroundRemoval": "Suppression de l'arrière-plan",
|
"backgroundRemoval": "Suppression de l'arrière-plan",
|
||||||
"brightness": "Luminosité",
|
"brightness": "Luminosité",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "グラフィックを調整する",
|
||||||
"appTitle": "PDF署名",
|
"appTitle": "PDF署名",
|
||||||
"backgroundRemoval": "背景除去",
|
"backgroundRemoval": "背景除去",
|
||||||
"brightness": "明るさ",
|
"brightness": "明るさ",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "그래픽 조정",
|
||||||
"appTitle": "PDF 서명",
|
"appTitle": "PDF 서명",
|
||||||
"backgroundRemoval": "배경 제거",
|
"backgroundRemoval": "배경 제거",
|
||||||
"brightness": "밝기",
|
"brightness": "밝기",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "Регулювати графіку",
|
||||||
"appTitle": "Підпис PDF",
|
"appTitle": "Підпис PDF",
|
||||||
"backgroundRemoval": "Видалення фону",
|
"backgroundRemoval": "Видалення фону",
|
||||||
"brightness": "Яскравість",
|
"brightness": "Яскравість",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"@@locale": "zh",
|
"@@locale": "zh",
|
||||||
|
"adjustGraphic": "調整圖形",
|
||||||
"appTitle": "PDF 簽名",
|
"appTitle": "PDF 簽名",
|
||||||
"backgroundRemoval": "去除背景",
|
"backgroundRemoval": "去除背景",
|
||||||
"brightness": "亮度",
|
"brightness": "亮度",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"adjustGraphic": "调整图形",
|
||||||
"appTitle": "PDF 签名",
|
"appTitle": "PDF 签名",
|
||||||
"backgroundRemoval": "背景移除",
|
"backgroundRemoval": "背景移除",
|
||||||
"brightness": "亮度",
|
"brightness": "亮度",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"@@locale": "zh_TW",
|
"@@locale": "zh_TW",
|
||||||
|
"adjustGraphic": "調整圖形",
|
||||||
"appTitle": "PDF 簽名",
|
"appTitle": "PDF 簽名",
|
||||||
"backgroundRemoval": "去除背景",
|
"backgroundRemoval": "去除背景",
|
||||||
"brightness": "亮度",
|
"brightness": "亮度",
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Centralized accessors for context menu labels to avoid duplication.
|
|
||||||
class MenuLabels {
|
|
||||||
static String confirm(BuildContext context) =>
|
|
||||||
AppLocalizations.of(context).confirm;
|
|
||||||
|
|
||||||
static String delete(BuildContext context) =>
|
|
||||||
AppLocalizations.of(context).delete;
|
|
||||||
|
|
||||||
// Not yet localized in l10n; keep here for single source of truth.
|
|
||||||
static String adjustGraphic(BuildContext context) => 'Adjust graphic';
|
|
||||||
}
|
|
|
@ -51,9 +51,6 @@ class ImageEditorDialog extends ConsumerWidget {
|
||||||
return RotatedSignatureImage(
|
return RotatedSignatureImage(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
rotationDeg: sig.rotation,
|
rotationDeg: sig.rotation,
|
||||||
enableAngleAwareScale: true,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
wrapInRepaintBoundary: true,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -9,7 +9,6 @@ import '../../signature/view_model/signature_controller.dart';
|
||||||
import '../view_model/pdf_controller.dart';
|
import '../view_model/pdf_controller.dart';
|
||||||
import '../../signature/view_model/signature_library.dart';
|
import '../../signature/view_model/signature_library.dart';
|
||||||
import 'image_editor_dialog.dart';
|
import 'image_editor_dialog.dart';
|
||||||
import '../../../common/menu_labels.dart';
|
|
||||||
import '../../signature/widgets/rotated_signature_image.dart';
|
import '../../signature/widgets/rotated_signature_image.dart';
|
||||||
|
|
||||||
/// Renders a single signature overlay (either interactive or placed) on a page.
|
/// Renders a single signature overlay (either interactive or placed) on a page.
|
||||||
|
@ -82,25 +81,37 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
final Color borderColor = isPlaced ? Colors.red : Colors.indigo;
|
final Color borderColor = isPlaced ? Colors.red : Colors.indigo;
|
||||||
final double borderWidth = isPlaced ? (isSelected ? 3.0 : 2.0) : 2.0;
|
final double borderWidth = isPlaced ? (isSelected ? 3.0 : 2.0) : 2.0;
|
||||||
|
|
||||||
Widget content = DecoratedBox(
|
// Instead of DecoratedBox, use a Stack to control layering
|
||||||
decoration: BoxDecoration(
|
Widget content = Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// Background layer (semi-transparent color)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
color: Color.fromRGBO(
|
color: Color.fromRGBO(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
||||||
),
|
),
|
||||||
border: Border.all(color: borderColor, width: borderWidth),
|
|
||||||
),
|
),
|
||||||
child: Stack(
|
),
|
||||||
alignment: Alignment.center,
|
// Signature image layer
|
||||||
children: [
|
|
||||||
_SignatureImage(
|
_SignatureImage(
|
||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
placedIndex: placedIndex,
|
placedIndex: placedIndex,
|
||||||
pageNumber: pageNumber,
|
pageNumber: pageNumber,
|
||||||
sig: sig,
|
sig: sig,
|
||||||
),
|
),
|
||||||
|
// Border layer (on top, using Positioned.fill with a transparent background)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: borderColor, width: borderWidth),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Resize handle (only for interactive mode, on top of everything)
|
||||||
if (interactive)
|
if (interactive)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
|
@ -116,7 +127,6 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
|
@ -128,8 +138,10 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
(d) => onDragSignature?.call(
|
(d) => onDragSignature?.call(
|
||||||
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
||||||
),
|
),
|
||||||
onSecondaryTapDown: (d) => _showActiveMenu(context, d.globalPosition),
|
onSecondaryTapDown:
|
||||||
onLongPressStart: (d) => _showActiveMenu(context, d.globalPosition),
|
(d) => _showActiveMenu(context, d.globalPosition, ref, null),
|
||||||
|
onLongPressStart:
|
||||||
|
(d) => _showActiveMenu(context, d.globalPosition, ref, null),
|
||||||
child: content,
|
child: content,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -139,12 +151,12 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
onTap: () => onSelectPlaced?.call(placedIndex),
|
onTap: () => onSelectPlaced?.call(placedIndex),
|
||||||
onSecondaryTapDown: (d) {
|
onSecondaryTapDown: (d) {
|
||||||
if (placedIndex != null) {
|
if (placedIndex != null) {
|
||||||
_showPlacedMenu(context, ref, d.globalPosition);
|
_showActiveMenu(context, d.globalPosition, ref, placedIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPressStart: (d) {
|
onLongPressStart: (d) {
|
||||||
if (placedIndex != null) {
|
if (placedIndex != null) {
|
||||||
_showPlacedMenu(context, ref, d.globalPosition);
|
_showActiveMenu(context, d.globalPosition, ref, placedIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: content,
|
child: content,
|
||||||
|
@ -153,7 +165,12 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showActiveMenu(BuildContext context, Offset globalPos) {
|
void _showActiveMenu(
|
||||||
|
BuildContext context,
|
||||||
|
Offset globalPos,
|
||||||
|
WidgetRef ref,
|
||||||
|
int? placedIndex,
|
||||||
|
) {
|
||||||
showMenu<String>(
|
showMenu<String>(
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(
|
position: RelativeRect.fromLTRB(
|
||||||
|
@ -163,71 +180,41 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
globalPos.dy,
|
globalPos.dy,
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
|
// if not placed, show Adjust and Confirm option
|
||||||
|
if (placedIndex == null) ...[
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
key: const Key('ctx_active_confirm'),
|
key: const Key('ctx_active_confirm'),
|
||||||
value: 'confirm',
|
value: 'confirm',
|
||||||
child: Text(MenuLabels.confirm(context)),
|
child: Text(AppLocalizations.of(context).confirm),
|
||||||
),
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
key: const Key('ctx_active_delete'),
|
|
||||||
value: 'delete',
|
|
||||||
child: Text(MenuLabels.delete(context)),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
key: const Key('ctx_active_adjust'),
|
key: const Key('ctx_active_adjust'),
|
||||||
value: 'adjust',
|
value: 'adjust',
|
||||||
child: Text(MenuLabels.adjustGraphic(context)),
|
child: Text(AppLocalizations.of(context).adjustGraphic),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
key: const Key('ctx_active_delete'),
|
||||||
|
value: 'delete',
|
||||||
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).then((choice) {
|
).then((choice) {
|
||||||
if (choice == 'confirm') {
|
if (choice == 'confirm') {
|
||||||
|
if (placedIndex == null) {
|
||||||
onConfirmSignature?.call();
|
onConfirmSignature?.call();
|
||||||
|
}
|
||||||
|
// For placed, confirm does nothing
|
||||||
} else if (choice == 'delete') {
|
} else if (choice == 'delete') {
|
||||||
|
if (placedIndex == null) {
|
||||||
onClearActiveOverlay?.call();
|
onClearActiveOverlay?.call();
|
||||||
} else if (choice == 'adjust') {
|
} else {
|
||||||
showDialog(context: context, builder: (_) => const ImageEditorDialog());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showPlacedMenu(BuildContext context, WidgetRef ref, Offset globalPos) {
|
|
||||||
showMenu<String>(
|
|
||||||
context: context,
|
|
||||||
position: RelativeRect.fromLTRB(
|
|
||||||
globalPos.dx,
|
|
||||||
globalPos.dy,
|
|
||||||
globalPos.dx,
|
|
||||||
globalPos.dy,
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
key: const Key('ctx_placed_delete'),
|
|
||||||
value: 'delete',
|
|
||||||
child: Text(MenuLabels.delete(context)),
|
|
||||||
),
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
key: const Key('ctx_placed_adjust'),
|
|
||||||
value: 'adjust',
|
|
||||||
child: Text(MenuLabels.adjustGraphic(context)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).then((choice) {
|
|
||||||
switch (choice) {
|
|
||||||
case 'delete':
|
|
||||||
if (placedIndex != null) {
|
|
||||||
ref
|
ref
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.removePlacement(page: pageNumber, index: placedIndex!);
|
.removePlacement(page: pageNumber, index: placedIndex);
|
||||||
}
|
}
|
||||||
break;
|
} else if (choice == 'adjust') {
|
||||||
case 'adjust':
|
showDialog(context: context, builder: (_) => const ImageEditorDialog());
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) => const ImageEditorDialog(),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -291,12 +278,6 @@ class _SignatureImage extends ConsumerWidget {
|
||||||
rotationDeg = placementList[placedIndex!].rotationDeg;
|
rotationDeg = placementList[placedIndex!].rotationDeg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RotatedSignatureImage(
|
return RotatedSignatureImage(bytes: bytes, rotationDeg: rotationDeg);
|
||||||
bytes: bytes,
|
|
||||||
rotationDeg: rotationDeg,
|
|
||||||
enableAngleAwareScale: interactive,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
wrapInRepaintBoundary: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
/// A lightweight widget to render signature bytes with rotation and an
|
/// A lightweight widget to render signature bytes with rotation and an
|
||||||
/// angle-aware scale-to-fit so the rotated image stays within its bounds.
|
/// angle-aware scale-to-fit so the rotated image stays within its bounds.
|
||||||
|
@ -9,30 +10,18 @@ class RotatedSignatureImage extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.bytes,
|
required this.bytes,
|
||||||
this.rotationDeg = 0.0,
|
this.rotationDeg = 0.0,
|
||||||
this.enableAngleAwareScale = true,
|
|
||||||
this.fit = BoxFit.contain,
|
|
||||||
this.gaplessPlayback = true,
|
|
||||||
this.filterQuality = FilterQuality.low,
|
this.filterQuality = FilterQuality.low,
|
||||||
this.wrapInRepaintBoundary = true,
|
|
||||||
this.alignment = Alignment.center,
|
|
||||||
this.semanticLabel,
|
this.semanticLabel,
|
||||||
this.intrinsicAspectRatio,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
final double rotationDeg;
|
final double rotationDeg;
|
||||||
final bool enableAngleAwareScale;
|
|
||||||
final BoxFit fit;
|
|
||||||
final bool gaplessPlayback;
|
|
||||||
final FilterQuality filterQuality;
|
final FilterQuality filterQuality;
|
||||||
final bool wrapInRepaintBoundary;
|
final BoxFit fit = BoxFit.contain;
|
||||||
final AlignmentGeometry alignment;
|
final bool gaplessPlayback = true;
|
||||||
|
final Alignment alignment = Alignment.center;
|
||||||
|
final bool wrapInRepaintBoundary = true;
|
||||||
final String? semanticLabel;
|
final String? semanticLabel;
|
||||||
// Optional: intrinsic aspect ratio (width / height). If provided, we compute
|
|
||||||
// an angle-aware scale for non-square images to ensure the rotated rectangle
|
|
||||||
// (W,H) fits back into its (W,H) bounds. If null, we attempt to derive it
|
|
||||||
// from the image stream; only fall back to the square heuristic if unknown.
|
|
||||||
final double? intrinsicAspectRatio;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RotatedSignatureImage> createState() => _RotatedSignatureImageState();
|
State<RotatedSignatureImage> createState() => _RotatedSignatureImageState();
|
||||||
|
@ -60,20 +49,30 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setAspectRatio(double ar) {
|
||||||
|
if (mounted && _derivedAspectRatio != ar) {
|
||||||
|
setState(() => _derivedAspectRatio = ar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _resolveImage() {
|
void _resolveImage() {
|
||||||
_unlisten();
|
_unlisten();
|
||||||
// Only derive AR if not provided
|
// Decode synchronously to get aspect ratio
|
||||||
if (widget.intrinsicAspectRatio != null) return;
|
final decoded = img.decodePng(widget.bytes);
|
||||||
|
if (decoded != null) {
|
||||||
|
final w = decoded.width;
|
||||||
|
final h = decoded.height;
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
_setAspectRatio(w / h);
|
||||||
|
}
|
||||||
|
}
|
||||||
final stream = _provider.resolve(createLocalImageConfiguration(context));
|
final stream = _provider.resolve(createLocalImageConfiguration(context));
|
||||||
_stream = stream;
|
_stream = stream;
|
||||||
_listener = ImageStreamListener((ImageInfo info, bool sync) {
|
_listener = ImageStreamListener((ImageInfo info, bool sync) {
|
||||||
final w = info.image.width;
|
final w = info.image.width;
|
||||||
final h = info.image.height;
|
final h = info.image.height;
|
||||||
if (w > 0 && h > 0) {
|
if (w > 0 && h > 0) {
|
||||||
final ar = w / h;
|
_setAspectRatio(w / h);
|
||||||
if (mounted && _derivedAspectRatio != ar) {
|
|
||||||
setState(() => _derivedAspectRatio = ar);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stream.addListener(_listener!);
|
stream.addListener(_listener!);
|
||||||
|
@ -106,10 +105,9 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (angle != 0.0) {
|
if (angle != 0.0) {
|
||||||
if (widget.enableAngleAwareScale) {
|
|
||||||
final double c = math.cos(angle).abs();
|
final double c = math.cos(angle).abs();
|
||||||
final double s = math.sin(angle).abs();
|
final double s = math.sin(angle).abs();
|
||||||
final ar = widget.intrinsicAspectRatio ?? _derivedAspectRatio;
|
final ar = _derivedAspectRatio;
|
||||||
double scaleToFit;
|
double scaleToFit;
|
||||||
if (ar != null && ar > 0) {
|
if (ar != null && ar > 0) {
|
||||||
scaleToFit = math.min(ar / (ar * c + s), 1.0 / (ar * s + c));
|
scaleToFit = math.min(ar / (ar * c + s), 1.0 / (ar * s + c));
|
||||||
|
@ -121,9 +119,6 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
scale: scaleToFit,
|
scale: scaleToFit,
|
||||||
child: Transform.rotate(angle: angle, child: img),
|
child: Transform.rotate(angle: angle, child: img),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
img = Transform.rotate(angle: angle, child: img);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!widget.wrapInRepaintBoundary) return img;
|
if (!widget.wrapInRepaintBoundary) return img;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../view_model/signature_library.dart';
|
import '../view_model/signature_library.dart';
|
||||||
import 'signature_drag_data.dart';
|
import 'signature_drag_data.dart';
|
||||||
import '../../../common/menu_labels.dart';
|
|
||||||
import 'rotated_signature_image.dart';
|
import 'rotated_signature_image.dart';
|
||||||
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SignatureCard extends StatelessWidget {
|
class SignatureCard extends StatelessWidget {
|
||||||
const SignatureCard({
|
const SignatureCard({
|
||||||
|
@ -30,9 +30,6 @@ class SignatureCard extends StatelessWidget {
|
||||||
Widget img = RotatedSignatureImage(
|
Widget img = RotatedSignatureImage(
|
||||||
bytes: asset.bytes,
|
bytes: asset.bytes,
|
||||||
rotationDeg: rotationDeg,
|
rotationDeg: rotationDeg,
|
||||||
enableAngleAwareScale: true,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
wrapInRepaintBoundary: true,
|
|
||||||
);
|
);
|
||||||
Widget base = SizedBox(
|
Widget base = SizedBox(
|
||||||
width: 96,
|
width: 96,
|
||||||
|
@ -92,12 +89,12 @@ class SignatureCard extends StatelessWidget {
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
key: const Key('mi_signature_adjust'),
|
key: const Key('mi_signature_adjust'),
|
||||||
value: 'adjust',
|
value: 'adjust',
|
||||||
child: Text(MenuLabels.adjustGraphic(context)),
|
child: Text(AppLocalizations.of(context).adjustGraphic),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
key: const Key('mi_signature_delete'),
|
key: const Key('mi_signature_delete'),
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
child: Text(MenuLabels.delete(context)),
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -123,12 +120,12 @@ class SignatureCard extends StatelessWidget {
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
key: const Key('mi_signature_adjust'),
|
key: const Key('mi_signature_adjust'),
|
||||||
value: 'adjust',
|
value: 'adjust',
|
||||||
child: Text(MenuLabels.adjustGraphic(context)),
|
child: Text(AppLocalizations.of(context).adjustGraphic),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
key: const Key('mi_signature_delete'),
|
key: const Key('mi_signature_delete'),
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
child: Text(MenuLabels.delete(context)),
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -163,9 +160,6 @@ class SignatureCard extends StatelessWidget {
|
||||||
child: RotatedSignatureImage(
|
child: RotatedSignatureImage(
|
||||||
bytes: asset.bytes,
|
bytes: asset.bytes,
|
||||||
rotationDeg: rotationDeg,
|
rotationDeg: rotationDeg,
|
||||||
enableAngleAwareScale: true,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
wrapInRepaintBoundary: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -46,6 +46,11 @@ void main() {
|
||||||
|
|
||||||
// Verify the context menu shows "Adjust graphic"
|
// Verify the context menu shows "Adjust graphic"
|
||||||
expect(find.byKey(const Key('mi_signature_adjust')), findsOneWidget);
|
expect(find.byKey(const Key('mi_signature_adjust')), findsOneWidget);
|
||||||
|
|
||||||
|
// before confirm, adjust must be visible
|
||||||
|
expect(find.text('Adjust graphic'), findsOneWidget);
|
||||||
|
|
||||||
|
// after confirm, adjust must be visible
|
||||||
expect(find.text('Adjust graphic'), findsOneWidget);
|
expect(find.text('Adjust graphic'), findsOneWidget);
|
||||||
|
|
||||||
// Do not proceed to open the dialog here; the goal is just to verify menu content.
|
// Do not proceed to open the dialog here; the goal is just to verify menu content.
|
||||||
|
|
|
@ -26,14 +26,7 @@ void main() {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 150, // same aspect as image bounds (4:3)
|
height: 150, // same aspect as image bounds (4:3)
|
||||||
child: RotatedSignatureImage(
|
child: RotatedSignatureImage(bytes: bytes, rotationDeg: -90),
|
||||||
bytes: bytes,
|
|
||||||
rotationDeg: -90,
|
|
||||||
enableAngleAwareScale: true,
|
|
||||||
intrinsicAspectRatio: 4 / 3,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
wrapInRepaintBoundary: false, // make Transform visible
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue