Compare commits

...

2 Commits

100 changed files with 721 additions and 492 deletions

View File

@ -32,7 +32,8 @@ flutter run -d <device_id>
### build ### build
For Windows #### Windows
```bash ```bash
flutter build windows flutter build windows
# create windows installer # create windows installer

View File

@ -7,7 +7,9 @@ import 'package:image/image.dart' as img;
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_library.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart'; import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
import 'data/services/preferences_providers.dart'; import 'data/services/preferences_providers.dart';
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart'; import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';

View File

@ -171,7 +171,14 @@ class ExportService {
pw.Positioned( pw.Positioned(
left: left, left: left,
top: top, top: top,
child: pw.Image(imgObj, width: w, height: h), child: pw.SizedBox(
width: w,
height: h,
child: pw.FittedBox(
fit: pw.BoxFit.contain,
child: pw.Image(imgObj),
),
),
), ),
); );
} }
@ -187,7 +194,14 @@ class ExportService {
pw.Positioned( pw.Positioned(
left: left, left: left,
top: top, top: top,
child: pw.Image(sigImgObj, width: w, height: h), child: pw.SizedBox(
width: w,
height: h,
child: pw.FittedBox(
fit: pw.BoxFit.contain,
child: pw.Image(sigImgObj),
),
),
), ),
); );
} }
@ -294,7 +308,14 @@ class ExportService {
pw.Positioned( pw.Positioned(
left: left, left: left,
top: top, top: top,
child: pw.Image(imgObj, width: w, height: h), child: pw.SizedBox(
width: w,
height: h,
child: pw.FittedBox(
fit: pw.BoxFit.contain,
child: pw.Image(imgObj),
),
),
), ),
); );
} }
@ -310,7 +331,14 @@ class ExportService {
pw.Positioned( pw.Positioned(
left: left, left: left,
top: top, top: top,
child: pw.Image(sigImgObj, width: w, height: h), child: pw.SizedBox(
width: w,
height: h,
child: pw.FittedBox(
fit: pw.BoxFit.contain,
child: pw.Image(sigImgObj),
),
),
), ),
); );
} }

View File

@ -0,0 +1,172 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../data/model/model.dart';
class PdfController extends StateNotifier<PdfState> {
PdfController() : super(PdfState.initial());
static const int samplePageCount = 5;
@visibleForTesting
void openSample() {
state = state.copyWith(
loaded: true,
pageCount: samplePageCount,
currentPage: 1,
pickedPdfPath: null,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
}
void openPicked({
required String path,
int pageCount = samplePageCount,
Uint8List? bytes,
}) {
state = state.copyWith(
loaded: true,
pageCount: pageCount,
currentPage: 1,
pickedPdfPath: path,
pickedPdfBytes: bytes,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
}
void jumpTo(int page) {
if (!state.loaded) return;
final clamped = page.clamp(1, state.pageCount);
state = state.copyWith(currentPage: clamped, selectedPlacementIndex: null);
}
// Set or clear the page that will receive the signature overlay.
void setSignedPage(int? page) {
if (!state.loaded) return;
if (page == null) {
state = state.copyWith(signedPage: null, selectedPlacementIndex: null);
} else {
final clamped = page.clamp(1, state.pageCount);
state = state.copyWith(signedPage: clamped, selectedPlacementIndex: null);
}
}
void setPageCount(int count) {
if (!state.loaded) return;
state = state.copyWith(pageCount: count.clamp(1, 9999));
}
// Multiple-signature helpers (rects are stored in normalized fractions 0..1
// relative to the page size: left/top/width/height are all 0..1)
void addPlacement({
required int page,
required Rect rect,
String image = 'default.png',
}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final map = Map<int, List<Rect>>.from(state.placementsByPage);
final list = List<Rect>.from(map[p] ?? const []);
list.add(rect);
map[p] = list;
// Sync image mapping list
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
final imgList = List<String>.from(imgMap[p] ?? const []);
imgList.add(image);
imgMap[p] = imgList;
state = state.copyWith(
placementsByPage: map,
placementImageByPage: imgMap,
selectedPlacementIndex: null,
);
}
void removePlacement({required int page, required int index}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final map = Map<int, List<Rect>>.from(state.placementsByPage);
final list = List<Rect>.from(map[p] ?? const []);
if (index >= 0 && index < list.length) {
list.removeAt(index);
// Sync image mapping
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
final imgList = List<String>.from(imgMap[p] ?? const []);
if (index >= 0 && index < imgList.length) {
imgList.removeAt(index);
}
if (list.isEmpty) {
map.remove(p);
imgMap.remove(p);
} else {
map[p] = list;
imgMap[p] = imgList;
}
state = state.copyWith(
placementsByPage: map,
placementImageByPage: imgMap,
selectedPlacementIndex: null,
);
}
}
// Update the rect of an existing placement on a page.
void updatePlacementRect({
required int page,
required int index,
required Rect rect,
}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final map = Map<int, List<Rect>>.from(state.placementsByPage);
final list = List<Rect>.from(map[p] ?? const []);
if (index >= 0 && index < list.length) {
list[index] = rect;
map[p] = list;
state = state.copyWith(placementsByPage: map);
}
}
List<Rect> placementsOn(int page) {
return List<Rect>.from(state.placementsByPage[page] ?? const []);
}
void selectPlacement(int? index) {
if (!state.loaded) return;
// Only allow valid index on current page; otherwise clear
if (index == null) {
state = state.copyWith(selectedPlacementIndex: null);
return;
}
final list = state.placementsByPage[state.currentPage] ?? const [];
if (index >= 0 && index < list.length) {
state = state.copyWith(selectedPlacementIndex: index);
} else {
state = state.copyWith(selectedPlacementIndex: null);
}
}
void deleteSelectedPlacement() {
final idx = state.selectedPlacementIndex;
if (idx == null) return;
removePlacement(page: state.currentPage, index: idx);
}
// NOTE: Programmatic reassignment of images has been removed.
// Convenience to get image name for a placement
String? imageOfPlacement({required int page, required int index}) {
final list = state.placementImageByPage[page] ?? const [];
if (index < 0 || index >= list.length) return null;
return list[index];
}
}
final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
(ref) => PdfController(),
);

View File

@ -7,211 +7,8 @@ import 'package:image/image.dart' as img;
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart'; import '../../../../data/model/model.dart';
import 'pdf_controller.dart';
class PdfController extends StateNotifier<PdfState> { import 'signature_library.dart';
PdfController() : super(PdfState.initial());
static const int samplePageCount = 5;
@visibleForTesting
void openSample() {
state = state.copyWith(
loaded: true,
pageCount: samplePageCount,
currentPage: 1,
pickedPdfPath: null,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
}
void openPicked({
required String path,
int pageCount = samplePageCount,
Uint8List? bytes,
}) {
state = state.copyWith(
loaded: true,
pageCount: pageCount,
currentPage: 1,
pickedPdfPath: path,
pickedPdfBytes: bytes,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
}
void jumpTo(int page) {
if (!state.loaded) return;
final clamped = page.clamp(1, state.pageCount);
state = state.copyWith(currentPage: clamped, selectedPlacementIndex: null);
}
// Set or clear the page that will receive the signature overlay.
void setSignedPage(int? page) {
if (!state.loaded) return;
if (page == null) {
state = state.copyWith(signedPage: null, selectedPlacementIndex: null);
} else {
final clamped = page.clamp(1, state.pageCount);
state = state.copyWith(signedPage: clamped, selectedPlacementIndex: null);
}
}
void setPageCount(int count) {
if (!state.loaded) return;
state = state.copyWith(pageCount: count.clamp(1, 9999));
}
// Multiple-signature helpers (rects are stored in normalized fractions 0..1
// relative to the page size: left/top/width/height are all 0..1)
void addPlacement({
required int page,
required Rect rect,
String image = 'default.png',
}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final map = Map<int, List<Rect>>.from(state.placementsByPage);
final list = List<Rect>.from(map[p] ?? const []);
list.add(rect);
map[p] = list;
// Sync image mapping list
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
final imgList = List<String>.from(imgMap[p] ?? const []);
imgList.add(image);
imgMap[p] = imgList;
state = state.copyWith(
placementsByPage: map,
placementImageByPage: imgMap,
selectedPlacementIndex: null,
);
}
void removePlacement({required int page, required int index}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final map = Map<int, List<Rect>>.from(state.placementsByPage);
final list = List<Rect>.from(map[p] ?? const []);
if (index >= 0 && index < list.length) {
list.removeAt(index);
// Sync image mapping
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
final imgList = List<String>.from(imgMap[p] ?? const []);
if (index >= 0 && index < imgList.length) {
imgList.removeAt(index);
}
if (list.isEmpty) {
map.remove(p);
imgMap.remove(p);
} else {
map[p] = list;
imgMap[p] = imgList;
}
state = state.copyWith(
placementsByPage: map,
placementImageByPage: imgMap,
selectedPlacementIndex: null,
);
}
}
// Update the rect of an existing placement on a page.
void updatePlacementRect({
required int page,
required int index,
required Rect rect,
}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final map = Map<int, List<Rect>>.from(state.placementsByPage);
final list = List<Rect>.from(map[p] ?? const []);
if (index >= 0 && index < list.length) {
list[index] = rect;
map[p] = list;
state = state.copyWith(placementsByPage: map);
}
}
List<Rect> placementsOn(int page) {
return List<Rect>.from(state.placementsByPage[page] ?? const []);
}
void selectPlacement(int? index) {
if (!state.loaded) return;
// Only allow valid index on current page; otherwise clear
if (index == null) {
state = state.copyWith(selectedPlacementIndex: null);
return;
}
final list = state.placementsByPage[state.currentPage] ?? const [];
if (index >= 0 && index < list.length) {
state = state.copyWith(selectedPlacementIndex: index);
} else {
state = state.copyWith(selectedPlacementIndex: null);
}
}
void deleteSelectedPlacement() {
final idx = state.selectedPlacementIndex;
if (idx == null) return;
removePlacement(page: state.currentPage, index: idx);
}
// NOTE: Programmatic reassignment of images has been removed.
// Convenience to get image name for a placement
String? imageOfPlacement({required int page, required int index}) {
final list = state.placementImageByPage[page] ?? const [];
if (index < 0 || index >= list.length) return null;
return list[index];
}
}
final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
(ref) => PdfController(),
);
/// A simple library of signature images available to the user in the sidebar.
class SignatureAsset {
final String id; // unique id
final Uint8List bytes;
final String? name; // optional display name (e.g., filename)
const SignatureAsset({required this.id, required this.bytes, this.name});
}
class SignatureLibraryController extends StateNotifier<List<SignatureAsset>> {
SignatureLibraryController() : super(const []);
String add(Uint8List bytes, {String? name}) {
// Always add a new asset (allow duplicates). This lets users create multiple cards
// even when loading the same image repeatedly for different adjustments/usages.
if (bytes.isEmpty) return '';
final id = DateTime.now().microsecondsSinceEpoch.toString();
state = List.of(state)
..add(SignatureAsset(id: id, bytes: bytes, name: name));
return id;
}
void remove(String id) {
state = state.where((a) => a.id != id).toList(growable: false);
}
SignatureAsset? byId(String id) {
for (final a in state) {
if (a.id == id) return a;
}
return null;
}
}
final signatureLibraryProvider =
StateNotifierProvider<SignatureLibraryController, List<SignatureAsset>>(
(ref) => SignatureLibraryController(),
);
class SignatureController extends StateNotifier<SignatureState> { class SignatureController extends StateNotifier<SignatureState> {
SignatureController() : super(SignatureState.initial()); SignatureController() : super(SignatureState.initial());
@ -385,21 +182,31 @@ class SignatureController extends StateNotifier<SignatureState> {
// Bind the processed image at placement time (so placed preview matches adjustments). // Bind the processed image at placement time (so placed preview matches adjustments).
// If processed bytes exist, always create a new asset for this placement. // If processed bytes exist, always create a new asset for this placement.
String id = ''; String id = '';
final processed = ref.read(processedSignatureImageProvider); // Compose final bytes for placement: apply adjustments (processed) then rotation.
if (processed != null && processed.isNotEmpty) { Uint8List? srcBytes = ref.read(processedSignatureImageProvider);
srcBytes ??= state.imageBytes;
// If still null, fall back to asset reference only.
if (srcBytes != null && srcBytes.isNotEmpty) {
final rot = state.rotation % 360;
Uint8List finalBytes = srcBytes;
if (rot != 0) {
try {
final decoded = img.decodeImage(srcBytes);
if (decoded != null) {
var out = img.copyRotate(
decoded,
angle: rot,
interpolation: img.Interpolation.linear,
);
finalBytes = Uint8List.fromList(img.encodePng(out, level: 6));
}
} catch (_) {}
}
id = ref id = ref
.read(signatureLibraryProvider.notifier) .read(signatureLibraryProvider.notifier)
.add(processed, name: 'image'); .add(finalBytes, name: 'image');
} else { } else {
// Fallback to current image source id = state.assetId ?? 'default.png';
final bytes = state.imageBytes;
if (bytes != null && bytes.isNotEmpty) {
id = ref
.read(signatureLibraryProvider.notifier)
.add(bytes, name: 'image');
} else {
id = state.assetId ?? 'default.png';
}
} }
// Store as UI-space rect (consistent with export and rendering paths) // Store as UI-space rect (consistent with export and rendering paths)
ref ref
@ -426,20 +233,29 @@ class SignatureController extends StateNotifier<SignatureState> {
final pdf = container.read(pdfProvider); final pdf = container.read(pdfProvider);
if (!pdf.loaded) return null; if (!pdf.loaded) return null;
String id = ''; String id = '';
final processed = container.read(processedSignatureImageProvider); Uint8List? srcBytes = container.read(processedSignatureImageProvider);
if (processed != null && processed.isNotEmpty) { srcBytes ??= state.imageBytes;
if (srcBytes != null && srcBytes.isNotEmpty) {
final rot = state.rotation % 360;
Uint8List finalBytes = srcBytes;
if (rot != 0) {
try {
final decoded = img.decodeImage(srcBytes);
if (decoded != null) {
var out = img.copyRotate(
decoded,
angle: rot,
interpolation: img.Interpolation.linear,
);
finalBytes = Uint8List.fromList(img.encodePng(out, level: 6));
}
} catch (_) {}
}
id = container id = container
.read(signatureLibraryProvider.notifier) .read(signatureLibraryProvider.notifier)
.add(processed, name: 'image'); .add(finalBytes, name: 'image');
} else { } else {
final bytes = state.imageBytes; id = state.assetId ?? 'default.png';
if (bytes != null && bytes.isNotEmpty) {
id = container
.read(signatureLibraryProvider.notifier)
.add(bytes, name: 'image');
} else {
id = state.assetId ?? 'default.png';
}
} }
container container
.read(pdfProvider.notifier) .read(pdfProvider.notifier)
@ -473,19 +289,33 @@ final signatureProvider =
/// current adjustment settings (contrast/brightness) and background removal. /// current adjustment settings (contrast/brightness) and background removal.
/// Returns null if no image is loaded. The output is a PNG to preserve alpha. /// Returns null if no image is loaded. The output is a PNG to preserve alpha.
final processedSignatureImageProvider = Provider<Uint8List?>((ref) { final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
final s = ref.watch(signatureProvider); // Watch only the fields that affect pixel processing to avoid recompute on rotation.
final String? assetId = ref.watch(signatureProvider.select((s) => s.assetId));
final Uint8List? directBytes = ref.watch(
signatureProvider.select((s) => s.imageBytes),
);
final double contrast = ref.watch(
signatureProvider.select((s) => s.contrast),
);
final double brightness = ref.watch(
signatureProvider.select((s) => s.brightness),
);
final bool bgRemoval = ref.watch(
signatureProvider.select((s) => s.bgRemoval),
);
// If active overlay is based on a library asset, pull its bytes // If active overlay is based on a library asset, pull its bytes
Uint8List? bytes; Uint8List? bytes;
if (s.assetId != null) { if (assetId != null) {
final lib = ref.watch(signatureLibraryProvider); final lib = ref.watch(signatureLibraryProvider);
for (final a in lib) { for (final a in lib) {
if (a.id == s.assetId) { if (a.id == assetId) {
bytes = a.bytes; bytes = a.bytes;
break; break;
} }
} }
} else { } else {
bytes = s.imageBytes; bytes = directBytes;
} }
if (bytes == null || bytes.isEmpty) return null; if (bytes == null || bytes.isEmpty) return null;
@ -501,9 +331,7 @@ final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
} }
// Parameters // Parameters
final double contrast = s.contrast; // [0..2], 1 = neutral // Rotation is not applied here (UI uses Transform; export applies once).
final double brightness = s.brightness; // [-1..1], 0 = neutral
final double rotationDeg = s.rotation; // degrees
const int thrLow = 220; // begin soft transparency from this avg luminance const int thrLow = 220; // begin soft transparency from this avg luminance
const int thrHigh = 245; // fully transparent from this avg luminance const int thrHigh = 245; // fully transparent from this avg luminance
@ -529,7 +357,7 @@ final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
// Near-white background removal (compute average luminance) // Near-white background removal (compute average luminance)
final int avg = ((r + g + b) / 3).round(); final int avg = ((r + g + b) / 3).round();
int remAlpha = 255; // 255 = fully opaque, 0 = transparent int remAlpha = 255; // 255 = fully opaque, 0 = transparent
if (s.bgRemoval) { if (bgRemoval) {
if (avg >= thrHigh) { if (avg >= thrHigh) {
remAlpha = 0; remAlpha = 0;
} else if (avg >= thrLow) { } else if (avg >= thrLow) {
@ -548,15 +376,9 @@ final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
} }
} }
// Apply rotation if any (around center) using bilinear interpolation and keep size // NOTE: Do not rotate here to keep UI responsive while dragging the slider.
if (rotationDeg % 360 != 0) { // Rotation is applied in the UI using Transform.rotate for preview and
// The image package rotates counter-clockwise; positive degrees rotate CCW // performed once on confirm/export to avoid per-frame recomputation.
out = img.copyRotate(
out,
angle: rotationDeg,
interpolation: img.Interpolation.linear,
);
}
// Encode as PNG to preserve transparency // Encode as PNG to preserve transparency
final png = img.encodePng(out, level: 6); final png = img.encodePng(out, level: 6);

View File

@ -0,0 +1,40 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// A simple library of signature images available to the user in the sidebar.
class SignatureAsset {
final String id; // unique id
final Uint8List bytes;
final String? name; // optional display name (e.g., filename)
const SignatureAsset({required this.id, required this.bytes, this.name});
}
class SignatureLibraryController extends StateNotifier<List<SignatureAsset>> {
SignatureLibraryController() : super(const []);
String add(Uint8List bytes, {String? name}) {
// Always add a new asset (allow duplicates). This lets users create multiple cards
// even when loading the same image repeatedly for different adjustments/usages.
if (bytes.isEmpty) return '';
final id = DateTime.now().microsecondsSinceEpoch.toString();
state = List.of(state)
..add(SignatureAsset(id: id, bytes: bytes, name: name));
return id;
}
void remove(String id) {
state = state.where((a) => a.id != id).toList(growable: false);
}
SignatureAsset? byId(String id) {
for (final a in state) {
if (a.id == id) return a;
}
return null;
}
}
final signatureLibraryProvider =
StateNotifierProvider<SignatureLibraryController, List<SignatureAsset>>(
(ref) => SignatureLibraryController(),
);

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart'; import '../../../../data/model/model.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_controller.dart';
class AdjustmentsPanel extends ConsumerWidget { class AdjustmentsPanel extends ConsumerWidget {
const AdjustmentsPanel({super.key, required this.sig}); const AdjustmentsPanel({super.key, required this.sig});

View File

@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_controller.dart';
import 'adjustments_panel.dart'; import 'adjustments_panel.dart';
import 'rotated_signature_image.dart';
class ImageEditorDialog extends ConsumerWidget { class ImageEditorDialog extends ConsumerWidget {
const ImageEditorDialog({super.key}); const ImageEditorDialog({super.key});
@ -47,7 +48,13 @@ class ImageEditorDialog extends ConsumerWidget {
if (bytes == null) { if (bytes == null) {
return Text(l.noSignatureLoaded); return Text(l.noSignatureLoaded);
} }
return Image.memory(bytes, fit: BoxFit.contain); return RotatedSignatureImage(
bytes: bytes,
rotationDeg: sig.rotation,
enableAngleAwareScale: true,
fit: BoxFit.contain,
wrapInRepaintBoundary: true,
);
}, },
), ),
), ),

View File

@ -4,7 +4,8 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
import '../../../../data/services/export_providers.dart'; import '../../../../data/services/export_providers.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import 'signature_drag_data.dart'; import 'signature_drag_data.dart';
import 'pdf_mock_continuous_list.dart'; import 'pdf_mock_continuous_list.dart';
import 'pdf_page_overlays.dart'; import 'pdf_page_overlays.dart';

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import 'signature_overlay.dart'; import 'signature_overlay.dart';
/// Builds all overlays for a given page: placed signatures and the active one. /// Builds all overlays for a given page: placed signatures and the active one.

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
import '../../../../data/services/export_providers.dart'; import '../../../../data/services/export_providers.dart';
import '../view_model/view_model.dart'; import '../view_model/pdf_controller.dart';
class PdfPagesOverview extends ConsumerWidget { class PdfPagesOverview extends ConsumerWidget {
const PdfPagesOverview({super.key}); const PdfPagesOverview({super.key});

View File

@ -9,7 +9,10 @@ import 'package:pdfrx/pdfrx.dart';
import 'package:multi_split_view/multi_split_view.dart'; import 'package:multi_split_view/multi_split_view.dart';
import '../../../../data/services/export_providers.dart'; import '../../../../data/services/export_providers.dart';
import '../view_model/view_model.dart'; import 'package:image/image.dart' as img;
import '../view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import '../view_model/signature_library.dart';
import 'draw_canvas.dart'; import 'draw_canvas.dart';
import 'pdf_toolbar.dart'; import 'pdf_toolbar.dart';
import 'pdf_page_area.dart'; import 'pdf_page_area.dart';
@ -137,16 +140,39 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
final useMock = ref.read(useMockViewerProvider); final useMock = ref.read(useMockViewerProvider);
bool ok = false; bool ok = false;
String? savedPath; String? savedPath;
// Helper to apply rotation to bytes for export (single-signature path only)
Uint8List? _rotatedForExport(Uint8List? src, double deg) {
if (src == null || src.isEmpty) return src;
final r = deg % 360;
if (r == 0) return src;
try {
final decoded = img.decodeImage(src);
if (decoded == null) return src;
final out = img.copyRotate(
decoded,
angle: r,
interpolation: img.Interpolation.linear,
);
return Uint8List.fromList(img.encodePng(out, level: 6));
} catch (_) {
return src;
}
}
if (kIsWeb) { if (kIsWeb) {
Uint8List? src = pdf.pickedPdfBytes; Uint8List? src = pdf.pickedPdfBytes;
if (src != null) { if (src != null) {
final processed = ref.read(processedSignatureImageProvider); final processed = ref.read(processedSignatureImageProvider);
final rotated = _rotatedForExport(
processed ?? sig.imageBytes,
sig.rotation,
);
final bytes = await exporter.exportSignedPdfFromBytes( final bytes = await exporter.exportSignedPdfFromBytes(
srcBytes: src, srcBytes: src,
signedPage: pdf.signedPage, signedPage: pdf.signedPage,
signatureRectUi: sig.rect, signatureRectUi: sig.rect,
uiPageSize: SignatureController.pageSize, uiPageSize: SignatureController.pageSize,
signatureImageBytes: processed ?? sig.imageBytes, signatureImageBytes: rotated,
placementsByPage: pdf.placementsByPage, placementsByPage: pdf.placementsByPage,
placementImageByPage: pdf.placementImageByPage, placementImageByPage: pdf.placementImageByPage,
libraryBytes: { libraryBytes: {
@ -174,12 +200,16 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
savedPath = fullPath; savedPath = fullPath;
if (pdf.pickedPdfBytes != null) { if (pdf.pickedPdfBytes != null) {
final processed = ref.read(processedSignatureImageProvider); final processed = ref.read(processedSignatureImageProvider);
final rotated = _rotatedForExport(
processed ?? sig.imageBytes,
sig.rotation,
);
final out = await exporter.exportSignedPdfFromBytes( final out = await exporter.exportSignedPdfFromBytes(
srcBytes: pdf.pickedPdfBytes!, srcBytes: pdf.pickedPdfBytes!,
signedPage: pdf.signedPage, signedPage: pdf.signedPage,
signatureRectUi: sig.rect, signatureRectUi: sig.rect,
uiPageSize: SignatureController.pageSize, uiPageSize: SignatureController.pageSize,
signatureImageBytes: processed ?? sig.imageBytes, signatureImageBytes: rotated,
placementsByPage: pdf.placementsByPage, placementsByPage: pdf.placementsByPage,
placementImageByPage: pdf.placementImageByPage, placementImageByPage: pdf.placementImageByPage,
libraryBytes: { libraryBytes: {
@ -200,13 +230,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
ok = true; ok = true;
} else { } else {
final processed = ref.read(processedSignatureImageProvider); final processed = ref.read(processedSignatureImageProvider);
final rotated = _rotatedForExport(
processed ?? sig.imageBytes,
sig.rotation,
);
ok = await exporter.exportSignedPdfFromFile( ok = await exporter.exportSignedPdfFromFile(
inputPath: pdf.pickedPdfPath!, inputPath: pdf.pickedPdfPath!,
outputPath: fullPath, outputPath: fullPath,
signedPage: pdf.signedPage, signedPage: pdf.signedPage,
signatureRectUi: sig.rect, signatureRectUi: sig.rect,
uiPageSize: SignatureController.pageSize, uiPageSize: SignatureController.pageSize,
signatureImageBytes: processed ?? sig.imageBytes, signatureImageBytes: rotated,
placementsByPage: pdf.placementsByPage, placementsByPage: pdf.placementsByPage,
placementImageByPage: pdf.placementImageByPage, placementImageByPage: pdf.placementImageByPage,
libraryBytes: { libraryBytes: {

View File

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../view_model/view_model.dart'; import '../view_model/pdf_controller.dart';
class PdfToolbar extends ConsumerStatefulWidget { class PdfToolbar extends ConsumerStatefulWidget {
const PdfToolbar({ const PdfToolbar({

View File

@ -0,0 +1,132 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/material.dart';
/// A lightweight widget to render signature bytes with rotation and an
/// angle-aware scale-to-fit so the rotated image stays within its bounds.
class RotatedSignatureImage extends StatefulWidget {
const RotatedSignatureImage({
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 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();
}
class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
ImageStream? _stream;
ImageStreamListener? _listener;
double? _derivedAspectRatio; // width / height
MemoryImage get _provider => MemoryImage(widget.bytes);
@override
void didChangeDependencies() {
super.didChangeDependencies();
_resolveImage();
}
@override
void didUpdateWidget(covariant RotatedSignatureImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.bytes, widget.bytes)) {
_derivedAspectRatio = null;
_resolveImage();
}
}
void _resolveImage() {
_unlisten();
// Only derive AR if not provided
if (widget.intrinsicAspectRatio != null) return;
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);
}
}
});
stream.addListener(_listener!);
}
void _unlisten() {
if (_stream != null && _listener != null) {
_stream!.removeListener(_listener!);
}
_stream = null;
_listener = null;
}
@override
void dispose() {
_unlisten();
super.dispose();
}
@override
Widget build(BuildContext context) {
final angle = widget.rotationDeg * math.pi / 180.0;
Widget img = Image.memory(
widget.bytes,
fit: widget.fit,
gaplessPlayback: widget.gaplessPlayback,
filterQuality: widget.filterQuality,
alignment: widget.alignment,
semanticLabel: widget.semanticLabel,
);
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),
);
} else {
img = Transform.rotate(angle: angle, child: img);
}
}
if (!widget.wrapInRepaintBoundary) return img;
return RepaintBoundary(child: img);
}
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_library.dart';
import 'signature_drag_data.dart'; import 'signature_drag_data.dart';
import '../../../common/menu_labels.dart'; import '../../../common/menu_labels.dart';
import 'rotated_signature_image.dart';
class SignatureCard extends StatelessWidget { class SignatureCard extends StatelessWidget {
const SignatureCard({ const SignatureCard({
@ -12,6 +13,7 @@ class SignatureCard extends StatelessWidget {
this.onTap, this.onTap,
this.onAdjust, this.onAdjust,
this.useCurrentBytesForDrag = false, this.useCurrentBytesForDrag = false,
this.rotationDeg = 0.0,
}); });
final SignatureAsset asset; final SignatureAsset asset;
final bool disabled; final bool disabled;
@ -19,10 +21,19 @@ class SignatureCard extends StatelessWidget {
final VoidCallback? onTap; final VoidCallback? onTap;
final VoidCallback? onAdjust; final VoidCallback? onAdjust;
final bool useCurrentBytesForDrag; final bool useCurrentBytesForDrag;
final double rotationDeg;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final img = Image.memory(asset.bytes, fit: BoxFit.contain); // Fit inside 96x64 with 6px padding using the shared rotated image widget
const boxW = 96.0, boxH = 64.0, pad = 6.0;
Widget img = RotatedSignatureImage(
bytes: asset.bytes,
rotationDeg: rotationDeg,
enableAngleAwareScale: true,
fit: BoxFit.contain,
wrapInRepaintBoundary: true,
);
Widget base = SizedBox( Widget base = SizedBox(
width: 96, width: 96,
height: 64, height: 64,
@ -36,7 +47,14 @@ class SignatureCard extends StatelessWidget {
border: Border.all(color: Theme.of(context).dividerColor), border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Padding(padding: const EdgeInsets.all(6), child: img), child: Padding(
padding: const EdgeInsets.all(pad),
child: SizedBox(
width: boxW - pad * 2,
height: boxH - pad * 2,
child: img,
),
),
), ),
), ),
Positioned( Positioned(
@ -142,7 +160,13 @@ class SignatureCard extends StatelessWidget {
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
child: Image.memory(asset.bytes, fit: BoxFit.contain), child: RotatedSignatureImage(
bytes: asset.bytes,
rotationDeg: rotationDeg,
enableAngleAwareScale: true,
fit: BoxFit.contain,
wrapInRepaintBoundary: true,
),
), ),
), ),
), ),

View File

@ -4,7 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/services/export_providers.dart'; import '../../../../data/services/export_providers.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_controller.dart';
import '../view_model/signature_library.dart';
import 'image_editor_dialog.dart'; import 'image_editor_dialog.dart';
import 'signature_card.dart'; import 'signature_card.dart';
@ -58,6 +59,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
name: a.name, name: a.name,
) )
: a, : a,
rotationDeg: (sig.assetId == a.id) ? sig.rotation : 0.0,
disabled: disabled, disabled: disabled,
onDelete: onDelete:
() => ref () => ref
@ -95,6 +97,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
? Text(l.noSignatureLoaded) ? Text(l.noSignatureLoaded)
: SignatureCard( : SignatureCard(
asset: SignatureAsset(id: '', bytes: bytes, name: ''), asset: SignatureAsset(id: '', bytes: bytes, name: ''),
rotationDeg: sig.rotation,
disabled: disabled, disabled: disabled,
useCurrentBytesForDrag: true, useCurrentBytesForDrag: true,
onDelete: () { onDelete: () {

View File

@ -5,9 +5,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart'; import '../../../../data/model/model.dart';
import '../view_model/view_model.dart'; import '../view_model/signature_controller.dart';
import '../view_model/pdf_controller.dart';
import '../view_model/signature_library.dart';
import 'image_editor_dialog.dart'; import 'image_editor_dialog.dart';
import '../../../common/menu_labels.dart'; import '../../../common/menu_labels.dart';
import '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.
class SignatureOverlay extends ConsumerWidget { class SignatureOverlay extends ConsumerWidget {
@ -90,6 +93,7 @@ class SignatureOverlay extends ConsumerWidget {
border: Border.all(color: borderColor, width: borderWidth), border: Border.all(color: borderColor, width: borderWidth),
), ),
child: Stack( child: Stack(
alignment: Alignment.center,
children: [ children: [
_SignatureImage( _SignatureImage(
interactive: interactive, interactive: interactive,
@ -115,7 +119,7 @@ class SignatureOverlay extends ConsumerWidget {
), ),
); );
if (interactive && sig.editingEnabled) { if (interactive) {
content = GestureDetector( content = GestureDetector(
key: const Key('signature_overlay'), key: const Key('signature_overlay'),
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@ -277,10 +281,12 @@ class _SignatureImage extends ConsumerWidget {
return Center(child: Text(label)); return Center(child: Text(label));
} }
Widget im = Image.memory(bytes, fit: BoxFit.contain); return RotatedSignatureImage(
if (sig.rotation % 360 != 0) { bytes: bytes,
im = Transform.rotate(angle: sig.rotation * math.pi / 180.0, child: im); rotationDeg: interactive ? sig.rotation : 0.0,
} enableAngleAwareScale: interactive,
return im; fit: BoxFit.contain,
wrapInRepaintBoundary: true,
);
} }
} }

View File

@ -7,7 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../pdf/view_model/view_model.dart'; import '../../pdf/view_model/signature_controller.dart';
import '../../pdf/view_model/pdf_controller.dart';
// Settings dialog is provided via global AppBar in MyApp // Settings dialog is provided via global AppBar in MyApp
// Abstraction to make drop handling testable without constructing // Abstraction to make drop handling testable without constructing

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a drawn signature exists in the canvas /// Usage: a drawn signature exists in the canvas

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a multi-page PDF is open /// Usage: a multi-page PDF is open

View File

@ -1,7 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a PDF is open and contains at least one placed signature /// Usage: a PDF is open and contains at least one placed signature

View File

@ -2,7 +2,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a PDF is open and contains multiple placed signatures across pages /// Usage: a PDF is open and contains multiple placed signatures across pages

View File

@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a PDF is open with no signatures placed /// Usage: a PDF is open with no signatures placed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a PDF page is selected for signing /// Usage: a PDF page is selected for signing

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a sample multi-page PDF (5 pages) is available /// Usage: a sample multi-page PDF (5 pages) is available

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a signature image is created /// Usage: a signature image is created

View File

@ -1,7 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a signature image is loaded or drawn /// Usage: a signature image is loaded or drawn

View File

@ -1,7 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a signature image is placed on the page /// Usage: a signature image is placed on the page

View File

@ -1,7 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a signature image is selected /// Usage: a signature image is selected

View File

@ -2,7 +2,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a signature is placed on page {2} /// Usage: a signature is placed on page {2}

View File

@ -2,7 +2,8 @@ import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: a signature is placed with a position and size relative to the page /// Usage: a signature is placed with a position and size relative to the page

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: adjusting one instance does not affect the others /// Usage: adjusting one instance does not affect the others

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: all placed signatures appear on their corresponding pages in the output /// Usage: all placed signatures appear on their corresponding pages in the output

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: an empty signature canvas /// Usage: an empty signature canvas

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: both signatures are shown on their respective pages /// Usage: both signatures are shown on their respective pages

View File

@ -1,7 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: dragging or resizing one does not change the other /// Usage: dragging or resizing one does not change the other

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: each signature can be dragged and resized independently /// Usage: each signature can be dragged and resized independently

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: identical signature instances appear in each location /// Usage: identical signature instances appear in each location

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: it is placed on the selected page /// Usage: it is placed on the selected page

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: multiple strokes were drawn /// Usage: multiple strokes were drawn

View File

@ -2,7 +2,7 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: near-white background becomes transparent in the preview /// Usage: near-white background becomes transparent in the preview

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: only the selected signature is removed /// Usage: only the selected signature is removed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: page {5} becomes visible in the scroll area /// Usage: page {5} becomes visible in the scroll area

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: page {1} is displayed /// Usage: page {1} is displayed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the canvas becomes blank /// Usage: the canvas becomes blank

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the document is open /// Usage: the document is open

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the first page is displayed /// Usage: the first page is displayed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the Go to input cannot be used /// Usage: the Go to input cannot be used

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the image is loaded and shown as a signature asset /// Usage: the image is loaded and shown as a signature asset

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the image is not added to the document /// Usage: the image is not added to the document

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the image scales proportionally /// Usage: the image scales proportionally

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the last page is displayed (page {5}) /// Usage: the last page is displayed (page {5})

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the last stroke is removed /// Usage: the last stroke is removed

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the left pages overview highlights page {5} /// Usage: the left pages overview highlights page {5}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the other signatures remain unchanged /// Usage: the other signatures remain unchanged

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the page label shows "Page {5} of {5}" /// Usage: the page label shows "Page {5} of {5}"

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the preview updates immediately /// Usage: the preview updates immediately

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the signature is stamped at the exact PDF page coordinates and size /// Usage: the signature is stamped at the exact PDF page coordinates and size

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the signature on page {5} is shown on page {5} /// Usage: the signature on page {5} is shown on page {5}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the signature on page {2} remains /// Usage: the signature on page {2} remains

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the signature remains within the page area /// Usage: the signature remains within the page area

View File

@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the signatures appear on the corresponding page in the output /// Usage: the signatures appear on the corresponding page in the output

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the size and position update in real time /// Usage: the size and position update in real time

View File

@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user attempts to save /// Usage: the user attempts to save

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user can apply or reset adjustments /// Usage: the user can apply or reset adjustments

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user can move to the next or previous page /// Usage: the user can move to the next or previous page

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user changes contrast and brightness controls /// Usage: the user changes contrast and brightness controls

View File

@ -1,7 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user chooses a signature image file /// Usage: the user chooses a signature image file

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user chooses undo /// Usage: the user chooses undo

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user clears the canvas /// Usage: the user clears the canvas

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user clicks the Go to apply button /// Usage: the user clicks the Go to apply button

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user clicks the thumbnail for page {2} /// Usage: the user clicks the thumbnail for page {2}

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user deletes one selected signature /// Usage: the user deletes one selected signature

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user drags handles to resize and drags to reposition /// Usage: the user drags handles to resize and drags to reposition

View File

@ -1,7 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user draws strokes and confirms /// Usage: the user draws strokes and confirms

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user enables aspect ratio lock and resizes /// Usage: the user enables aspect ratio lock and resizes

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user enables background removal /// Usage: the user enables background removal

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user enters {99} into the Go to input and applies it /// Usage: the user enters {99} into the Go to input and applies it

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user is notified of the issue /// Usage: the user is notified of the issue

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user jumps to page {2} /// Usage: the user jumps to page {2}

View File

@ -2,7 +2,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user navigates to page {3} and places another signature /// Usage: the user navigates to page {3} and places another signature

View File

@ -2,7 +2,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user places a signature from picture <second_image> on page <second_page> /// Usage: the user places a signature from picture <second_image> on page <second_page>

View File

@ -2,7 +2,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user places a signature on page {1} /// Usage: the user places a signature on page {1}

View File

@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user places it in multiple locations in the document /// Usage: the user places it in multiple locations in the document

View File

@ -1,8 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user places two signatures on the same page /// Usage: the user places two signatures on the same page

View File

@ -1,7 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user saves/exports the document /// Usage: the user saves/exports the document

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user selects "<file>" /// Usage: the user selects "<file>"

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the user types {3} into the Go to input and presses Enter /// Usage: the user types {3} into the Go to input and presses Enter

View File

@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import '_world.dart'; import '_world.dart';
/// Usage: three signatures are placed on the current page /// Usage: three signatures are placed on the current page

View File

@ -1,136 +0,0 @@
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image/image.dart' as img;
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
void main() {
// Open the active overlay context menu robustly (mouse right-click, fallback to long-press)
Future<void> _openActiveMenuAndConfirm(WidgetTester tester) async {
final overlay = find.byKey(const Key('signature_overlay'));
expect(overlay, findsOneWidget);
// Ensure visible before interacting
await tester.ensureVisible(overlay);
await tester.pumpAndSettle();
// Try right-click first
final center = tester.getCenter(overlay);
final TestGesture mouse = await tester.createGesture(
kind: ui.PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
await mouse.addPointer(location: center);
addTearDown(mouse.removePointer);
await tester.pump();
await mouse.down(center);
await tester.pump(const Duration(milliseconds: 30));
await mouse.up();
await tester.pumpAndSettle();
// If menu didn't appear, try long-press
if (find.byKey(const Key('ctx_active_confirm')).evaluate().isEmpty) {
await tester.longPress(overlay, warnIfMissed: false);
await tester.pumpAndSettle();
}
await tester.tap(find.byKey(const Key('ctx_active_confirm')));
await tester.pumpAndSettle();
}
// Build a simple in-memory PNG as a signature image
Uint8List _makeSig() {
final canvas = img.Image(width: 80, height: 40);
img.fill(canvas, color: img.ColorUint8.rgb(255, 255, 255));
img.drawLine(
canvas,
x1: 6,
y1: 20,
x2: 74,
y2: 20,
color: img.ColorUint8.rgb(0, 0, 0),
);
return Uint8List.fromList(img.encodePng(canvas));
}
testWidgets('E2E: select, place default, and confirm signature', (
tester,
) async {
final sigBytes = _makeSig();
await tester.pumpWidget(
ProviderScope(
overrides: [
// Open a PDF
pdfProvider.overrideWith(
(ref) => PdfController()..openPicked(path: 'test.pdf'),
),
// Provide one signature asset in the library
signatureLibraryProvider.overrideWith((ref) {
final c = SignatureLibraryController();
c.add(sigBytes, name: 'image');
return c;
}),
// Use mock continuous viewer for deterministic layout in widget tests
useMockViewerProvider.overrideWithValue(true),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const PdfSignatureHomePage(),
),
),
);
await tester.pumpAndSettle();
// Tap the signature card to set it as active overlay
final card = find.byKey(const Key('gd_signature_card_area')).first;
expect(card, findsOneWidget);
await tester.tap(card);
await tester.pump();
// Active overlay should appear
final active = find.byKey(const Key('signature_overlay'));
expect(active, findsOneWidget);
final sizeBefore = tester.getSize(active);
// Bring the overlay into the viewport (it's near the bottom of the page by default)
final listFinder = find.byKey(const Key('pdf_continuous_mock_list'));
if (listFinder.evaluate().isNotEmpty) {
// Ensure the active overlay is fully visible within the scrollable viewport
await tester.ensureVisible(active);
await tester.pumpAndSettle();
}
// Open context menu and confirm using a robust flow
await _openActiveMenuAndConfirm(tester);
// Verify active overlay gone and placed overlay shown
expect(find.byKey(const Key('signature_overlay')), findsNothing);
final placed = find.byKey(const Key('placed_signature_0'));
expect(placed, findsOneWidget);
final sizeAfter = tester.getSize(placed);
// Compare sizes: should be roughly equal (allowing small layout variance)
expect(
(sizeAfter.width - sizeBefore.width).abs() < sizeBefore.width * 0.15,
isTrue,
);
expect(
(sizeAfter.height - sizeBefore.height).abs() < sizeBefore.height * 0.15,
isTrue,
);
// Verify provider state reflects one placement on current page
final ctx = tester.element(find.byType(PdfSignatureHomePage));
final container = ProviderScope.containerOf(ctx);
final pdf = container.read(pdfProvider);
final list = pdf.placementsByPage[pdf.currentPage] ?? const [];
expect(list.length, 1);
});
}

View File

@ -4,7 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';

View File

@ -5,7 +5,8 @@ import 'package:image/image.dart' as img;
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
// preferences_providers.dart no longer exports pageViewModeProvider // preferences_providers.dart no longer exports pageViewModeProvider

View File

@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/data/model/model.dart'; import 'package:pdf_signature/data/model/model.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';

View File

@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/model/model.dart'; import 'package:pdf_signature/data/model/model.dart';

View File

@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/model/model.dart'; import 'package:pdf_signature/data/model/model.dart';

View File

@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/data/services/export_providers.dart'; import 'package:pdf_signature/data/services/export_providers.dart';
void main() { void main() {

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_library.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'helpers.dart'; import 'helpers.dart';

View File

@ -6,7 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart'; import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/signature_controller.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_controller.dart';
class _FakeDropReadable implements DropReadable { class _FakeDropReadable implements DropReadable {
final String _name; final String _name;

View File

@ -0,0 +1,73 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:pdf_signature/ui/features/pdf/widgets/rotated_signature_image.dart';
/// Generates a simple solid-color PNG with given width/height.
Uint8List makePng({required int w, required int h}) {
final im = img.Image(width: w, height: h);
// Fill with opaque white
img.fill(im, color: img.ColorRgba8(255, 255, 255, 255));
return Uint8List.fromList(img.encodePng(im));
}
void main() {
testWidgets('4:3 image rotated -90 deg scales to 3/4', (tester) async {
// 4:3 aspect image -> width/height = 4/3
final bytes = makePng(w: 400, h: 300);
// Pump widget under a fixed-size parent so Transform.scale is applied
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
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
),
),
),
),
),
);
// Find the Transform widget that applies the scale (the outer Transform.scale)
final transformFinder = find.byType(Transform);
expect(transformFinder, findsWidgets);
// Among the Transforms, we expect one to be a scale-only matrix.
// Grab the first Transform and assert the scale on x (m4x4 matrix) equals 0.75.
Transform? scaleTransform;
for (final e in tester.widgetList<Transform>(transformFinder)) {
final m = e.transform.storage;
// A scale-only matrix will have m[0] and m[5] as scale factors on x/y, with zeros elsewhere (except last row/column)
// Also rotation transform will have off-diagonal terms; we want the one with zeros in 1,4 and 4,1 positions approximately.
final isLikelyScale =
(m[1].abs() < 1e-6) &&
(m[4].abs() < 1e-6) &&
(m[12].abs() < 1e-6) &&
(m[13].abs() < 1e-6);
if (isLikelyScale) {
scaleTransform = e;
break;
}
}
expect(scaleTransform, isNotNull, reason: 'Scale Transform not found');
final scale = scaleTransform!.transform.storage[0];
expect(
(scale - 0.75).abs() < 1e-6,
isTrue,
reason: 'Expected scale 0.75 for 4:3 rotated -90°',
);
});
}