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
|
||||
# flutter clean
|
||||
# arb_translate
|
||||
flutter pub get
|
||||
# arb_translate
|
||||
# flutter gen-l10n
|
||||
# > to generate gherkin test
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# > to remove unused step definitions
|
||||
|
|
|
@ -56,8 +56,8 @@ Design notes:
|
|||
- Right pane: signatures drawer displaying saved signatures as cards.
|
||||
- able to drag and drop signature cards onto the PDF as placed signatures.
|
||||
- Each signature card shows a preview.
|
||||
- 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).
|
||||
- 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).
|
||||
- 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.
|
||||
- "draw" opens a simple drawing interface (draw canvas) to create a signature card.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "Grafik anpassen",
|
||||
"appTitle": "PDF-Signatur",
|
||||
"backgroundRemoval": "Hintergrund entfernen",
|
||||
"brightness": "Helligkeit",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"adjustGraphic": "Adjust graphic",
|
||||
"@adjustGraphic": {},
|
||||
"appTitle": "PDF Signature",
|
||||
"@appTitle": {},
|
||||
"backgroundRemoval": "Background removal",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "Ajustar gráfico",
|
||||
"appTitle": "Firma PDF",
|
||||
"backgroundRemoval": "Eliminar fondo",
|
||||
"brightness": "Brillo",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "Ajuster le graphique",
|
||||
"appTitle": "Signature PDF",
|
||||
"backgroundRemoval": "Suppression de l'arrière-plan",
|
||||
"brightness": "Luminosité",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "グラフィックを調整する",
|
||||
"appTitle": "PDF署名",
|
||||
"backgroundRemoval": "背景除去",
|
||||
"brightness": "明るさ",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "그래픽 조정",
|
||||
"appTitle": "PDF 서명",
|
||||
"backgroundRemoval": "배경 제거",
|
||||
"brightness": "밝기",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "Регулювати графіку",
|
||||
"appTitle": "Підпис PDF",
|
||||
"backgroundRemoval": "Видалення фону",
|
||||
"brightness": "Яскравість",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"@@locale": "zh",
|
||||
"adjustGraphic": "調整圖形",
|
||||
"appTitle": "PDF 簽名",
|
||||
"backgroundRemoval": "去除背景",
|
||||
"brightness": "亮度",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"adjustGraphic": "调整图形",
|
||||
"appTitle": "PDF 签名",
|
||||
"backgroundRemoval": "背景移除",
|
||||
"brightness": "亮度",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"@@locale": "zh_TW",
|
||||
"adjustGraphic": "調整圖形",
|
||||
"appTitle": "PDF 簽名",
|
||||
"backgroundRemoval": "去除背景",
|
||||
"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(
|
||||
bytes: bytes,
|
||||
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 '../../signature/view_model/signature_library.dart';
|
||||
import 'image_editor_dialog.dart';
|
||||
import '../../../common/menu_labels.dart';
|
||||
import '../../signature/widgets/rotated_signature_image.dart';
|
||||
|
||||
/// Renders a single signature overlay (either interactive or placed) on a page.
|
||||
|
@ -82,41 +81,52 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
final Color borderColor = isPlaced ? Colors.red : Colors.indigo;
|
||||
final double borderWidth = isPlaced ? (isSelected ? 3.0 : 2.0) : 2.0;
|
||||
|
||||
Widget content = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromRGBO(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
||||
),
|
||||
border: Border.all(color: borderColor, width: borderWidth),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_SignatureImage(
|
||||
interactive: interactive,
|
||||
placedIndex: placedIndex,
|
||||
pageNumber: pageNumber,
|
||||
sig: sig,
|
||||
),
|
||||
if (interactive)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
key: const Key('signature_handle'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanUpdate:
|
||||
(d) => onResizeSignature?.call(
|
||||
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
||||
),
|
||||
child: const Icon(Icons.open_in_full, size: 20),
|
||||
),
|
||||
// Instead of DecoratedBox, use a Stack to control layering
|
||||
Widget content = Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background layer (semi-transparent color)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Color.fromRGBO(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Signature image layer
|
||||
_SignatureImage(
|
||||
interactive: interactive,
|
||||
placedIndex: placedIndex,
|
||||
pageNumber: pageNumber,
|
||||
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)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
key: const Key('signature_handle'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanUpdate:
|
||||
(d) => onResizeSignature?.call(
|
||||
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
||||
),
|
||||
child: const Icon(Icons.open_in_full, size: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (interactive) {
|
||||
|
@ -128,8 +138,10 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
(d) => onDragSignature?.call(
|
||||
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
||||
),
|
||||
onSecondaryTapDown: (d) => _showActiveMenu(context, d.globalPosition),
|
||||
onLongPressStart: (d) => _showActiveMenu(context, d.globalPosition),
|
||||
onSecondaryTapDown:
|
||||
(d) => _showActiveMenu(context, d.globalPosition, ref, null),
|
||||
onLongPressStart:
|
||||
(d) => _showActiveMenu(context, d.globalPosition, ref, null),
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
|
@ -139,12 +151,12 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
onTap: () => onSelectPlaced?.call(placedIndex),
|
||||
onSecondaryTapDown: (d) {
|
||||
if (placedIndex != null) {
|
||||
_showPlacedMenu(context, ref, d.globalPosition);
|
||||
_showActiveMenu(context, d.globalPosition, ref, placedIndex);
|
||||
}
|
||||
},
|
||||
onLongPressStart: (d) {
|
||||
if (placedIndex != null) {
|
||||
_showPlacedMenu(context, ref, d.globalPosition);
|
||||
_showActiveMenu(context, d.globalPosition, ref, placedIndex);
|
||||
}
|
||||
},
|
||||
child: content,
|
||||
|
@ -153,7 +165,12 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
return content;
|
||||
}
|
||||
|
||||
void _showActiveMenu(BuildContext context, Offset globalPos) {
|
||||
void _showActiveMenu(
|
||||
BuildContext context,
|
||||
Offset globalPos,
|
||||
WidgetRef ref,
|
||||
int? placedIndex,
|
||||
) {
|
||||
showMenu<String>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
|
@ -163,74 +180,44 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
globalPos.dy,
|
||||
),
|
||||
items: [
|
||||
PopupMenuItem<String>(
|
||||
key: const Key('ctx_active_confirm'),
|
||||
value: 'confirm',
|
||||
child: Text(MenuLabels.confirm(context)),
|
||||
),
|
||||
// if not placed, show Adjust and Confirm option
|
||||
if (placedIndex == null) ...[
|
||||
PopupMenuItem<String>(
|
||||
key: const Key('ctx_active_confirm'),
|
||||
value: 'confirm',
|
||||
child: Text(AppLocalizations.of(context).confirm),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
key: const Key('ctx_active_adjust'),
|
||||
value: 'adjust',
|
||||
child: Text(AppLocalizations.of(context).adjustGraphic),
|
||||
),
|
||||
],
|
||||
PopupMenuItem<String>(
|
||||
key: const Key('ctx_active_delete'),
|
||||
value: 'delete',
|
||||
child: Text(MenuLabels.delete(context)),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
key: const Key('ctx_active_adjust'),
|
||||
value: 'adjust',
|
||||
child: Text(MenuLabels.adjustGraphic(context)),
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
),
|
||||
],
|
||||
).then((choice) {
|
||||
if (choice == 'confirm') {
|
||||
onConfirmSignature?.call();
|
||||
if (placedIndex == null) {
|
||||
onConfirmSignature?.call();
|
||||
}
|
||||
// For placed, confirm does nothing
|
||||
} else if (choice == 'delete') {
|
||||
onClearActiveOverlay?.call();
|
||||
if (placedIndex == null) {
|
||||
onClearActiveOverlay?.call();
|
||||
} else {
|
||||
ref
|
||||
.read(pdfProvider.notifier)
|
||||
.removePlacement(page: pageNumber, index: placedIndex);
|
||||
}
|
||||
} else if (choice == 'adjust') {
|
||||
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
|
||||
.read(pdfProvider.notifier)
|
||||
.removePlacement(page: pageNumber, index: placedIndex!);
|
||||
}
|
||||
break;
|
||||
case 'adjust':
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => const ImageEditorDialog(),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _SignatureImage extends ConsumerWidget {
|
||||
|
@ -291,12 +278,6 @@ class _SignatureImage extends ConsumerWidget {
|
|||
rotationDeg = placementList[placedIndex!].rotationDeg;
|
||||
}
|
||||
}
|
||||
return RotatedSignatureImage(
|
||||
bytes: bytes,
|
||||
rotationDeg: rotationDeg,
|
||||
enableAngleAwareScale: interactive,
|
||||
fit: BoxFit.contain,
|
||||
wrapInRepaintBoundary: true,
|
||||
);
|
||||
return RotatedSignatureImage(bytes: bytes, rotationDeg: rotationDeg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
/// A lightweight widget to render signature bytes with rotation and an
|
||||
/// angle-aware scale-to-fit so the rotated image stays within its bounds.
|
||||
|
@ -9,30 +10,18 @@ class RotatedSignatureImage extends StatefulWidget {
|
|||
super.key,
|
||||
required this.bytes,
|
||||
this.rotationDeg = 0.0,
|
||||
this.enableAngleAwareScale = true,
|
||||
this.fit = BoxFit.contain,
|
||||
this.gaplessPlayback = true,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.wrapInRepaintBoundary = true,
|
||||
this.alignment = Alignment.center,
|
||||
this.semanticLabel,
|
||||
this.intrinsicAspectRatio,
|
||||
});
|
||||
|
||||
final Uint8List bytes;
|
||||
final double rotationDeg;
|
||||
final bool enableAngleAwareScale;
|
||||
final BoxFit fit;
|
||||
final bool gaplessPlayback;
|
||||
final FilterQuality filterQuality;
|
||||
final bool wrapInRepaintBoundary;
|
||||
final AlignmentGeometry alignment;
|
||||
final BoxFit fit = BoxFit.contain;
|
||||
final bool gaplessPlayback = true;
|
||||
final Alignment alignment = Alignment.center;
|
||||
final bool wrapInRepaintBoundary = true;
|
||||
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
|
||||
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() {
|
||||
_unlisten();
|
||||
// Only derive AR if not provided
|
||||
if (widget.intrinsicAspectRatio != null) return;
|
||||
// Decode synchronously to get aspect ratio
|
||||
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));
|
||||
_stream = stream;
|
||||
_listener = ImageStreamListener((ImageInfo info, bool sync) {
|
||||
final w = info.image.width;
|
||||
final h = info.image.height;
|
||||
if (w > 0 && h > 0) {
|
||||
final ar = w / h;
|
||||
if (mounted && _derivedAspectRatio != ar) {
|
||||
setState(() => _derivedAspectRatio = ar);
|
||||
}
|
||||
_setAspectRatio(w / h);
|
||||
}
|
||||
});
|
||||
stream.addListener(_listener!);
|
||||
|
@ -106,24 +105,20 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
|||
);
|
||||
|
||||
if (angle != 0.0) {
|
||||
if (widget.enableAngleAwareScale) {
|
||||
final double c = math.cos(angle).abs();
|
||||
final double s = math.sin(angle).abs();
|
||||
final ar = widget.intrinsicAspectRatio ?? _derivedAspectRatio;
|
||||
double scaleToFit;
|
||||
if (ar != null && ar > 0) {
|
||||
scaleToFit = math.min(ar / (ar * c + s), 1.0 / (ar * s + c));
|
||||
} else {
|
||||
// Fallback: square approximation
|
||||
scaleToFit = 1.0 / (c + s).clamp(1.0, double.infinity);
|
||||
}
|
||||
img = Transform.scale(
|
||||
scale: scaleToFit,
|
||||
child: Transform.rotate(angle: angle, child: img),
|
||||
);
|
||||
final double c = math.cos(angle).abs();
|
||||
final double s = math.sin(angle).abs();
|
||||
final ar = _derivedAspectRatio;
|
||||
double scaleToFit;
|
||||
if (ar != null && ar > 0) {
|
||||
scaleToFit = math.min(ar / (ar * c + s), 1.0 / (ar * s + c));
|
||||
} else {
|
||||
img = Transform.rotate(angle: angle, child: img);
|
||||
// Fallback: square approximation
|
||||
scaleToFit = 1.0 / (c + s).clamp(1.0, double.infinity);
|
||||
}
|
||||
img = Transform.scale(
|
||||
scale: scaleToFit,
|
||||
child: Transform.rotate(angle: angle, child: img),
|
||||
);
|
||||
}
|
||||
|
||||
if (!widget.wrapInRepaintBoundary) return img;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../view_model/signature_library.dart';
|
||||
import 'signature_drag_data.dart';
|
||||
import '../../../common/menu_labels.dart';
|
||||
import 'rotated_signature_image.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
|
||||
class SignatureCard extends StatelessWidget {
|
||||
const SignatureCard({
|
||||
|
@ -30,9 +30,6 @@ class SignatureCard extends StatelessWidget {
|
|||
Widget img = RotatedSignatureImage(
|
||||
bytes: asset.bytes,
|
||||
rotationDeg: rotationDeg,
|
||||
enableAngleAwareScale: true,
|
||||
fit: BoxFit.contain,
|
||||
wrapInRepaintBoundary: true,
|
||||
);
|
||||
Widget base = SizedBox(
|
||||
width: 96,
|
||||
|
@ -92,12 +89,12 @@ class SignatureCard extends StatelessWidget {
|
|||
PopupMenuItem(
|
||||
key: const Key('mi_signature_adjust'),
|
||||
value: 'adjust',
|
||||
child: Text(MenuLabels.adjustGraphic(context)),
|
||||
child: Text(AppLocalizations.of(context).adjustGraphic),
|
||||
),
|
||||
PopupMenuItem(
|
||||
key: const Key('mi_signature_delete'),
|
||||
value: 'delete',
|
||||
child: Text(MenuLabels.delete(context)),
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -123,12 +120,12 @@ class SignatureCard extends StatelessWidget {
|
|||
PopupMenuItem(
|
||||
key: const Key('mi_signature_adjust'),
|
||||
value: 'adjust',
|
||||
child: Text(MenuLabels.adjustGraphic(context)),
|
||||
child: Text(AppLocalizations.of(context).adjustGraphic),
|
||||
),
|
||||
PopupMenuItem(
|
||||
key: const Key('mi_signature_delete'),
|
||||
value: 'delete',
|
||||
child: Text(MenuLabels.delete(context)),
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -163,9 +160,6 @@ class SignatureCard extends StatelessWidget {
|
|||
child: RotatedSignatureImage(
|
||||
bytes: asset.bytes,
|
||||
rotationDeg: rotationDeg,
|
||||
enableAngleAwareScale: true,
|
||||
fit: BoxFit.contain,
|
||||
wrapInRepaintBoundary: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -46,6 +46,11 @@ void main() {
|
|||
|
||||
// Verify the context menu shows "Adjust graphic"
|
||||
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);
|
||||
|
||||
// Do not proceed to open the dialog here; the goal is just to verify menu content.
|
||||
|
|
|
@ -26,14 +26,7 @@ void main() {
|
|||
child: SizedBox(
|
||||
width: 200,
|
||||
height: 150, // same aspect as image bounds (4:3)
|
||||
child: RotatedSignatureImage(
|
||||
bytes: bytes,
|
||||
rotationDeg: -90,
|
||||
enableAngleAwareScale: true,
|
||||
intrinsicAspectRatio: 4 / 3,
|
||||
fit: BoxFit.contain,
|
||||
wrapInRepaintBoundary: false, // make Transform visible
|
||||
),
|
||||
child: RotatedSignatureImage(bytes: bytes, rotationDeg: -90),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue