feat: partially implement mult_signature_picture

This commit is contained in:
insleker 2025-09-02 11:33:46 +08:00
parent 51c2a403c4
commit 947c0eef81
17 changed files with 340 additions and 105 deletions

View File

@ -10,6 +10,8 @@ class PdfState {
final int? signedPage;
// Multiple signature placements per page, stored as UI-space rects (e.g., 400x560)
final Map<int, List<Rect>> placementsByPage;
// For each placement, store the assigned image identifier (e.g., filename) in the same index order.
final Map<int, List<String>> placementImageByPage;
// UI state: selected placement index on the current page (if any)
final int? selectedPlacementIndex;
const PdfState({
@ -20,6 +22,7 @@ class PdfState {
this.pickedPdfBytes,
this.signedPage,
this.placementsByPage = const {},
this.placementImageByPage = const {},
this.selectedPlacementIndex,
});
factory PdfState.initial() => const PdfState(
@ -29,6 +32,7 @@ class PdfState {
pickedPdfBytes: null,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
PdfState copyWith({
@ -39,6 +43,7 @@ class PdfState {
Uint8List? pickedPdfBytes,
int? signedPage,
Map<int, List<Rect>>? placementsByPage,
Map<int, List<String>>? placementImageByPage,
int? selectedPlacementIndex,
}) => PdfState(
loaded: loaded ?? this.loaded,
@ -48,6 +53,7 @@ class PdfState {
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
signedPage: signedPage ?? this.signedPage,
placementsByPage: placementsByPage ?? this.placementsByPage,
placementImageByPage: placementImageByPage ?? this.placementImageByPage,
selectedPlacementIndex:
selectedPlacementIndex ?? this.selectedPlacementIndex,
);

View File

@ -2,7 +2,6 @@ import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:image/image.dart' as img;
import '../../../../data/model/model.dart';
@ -19,6 +18,7 @@ class PdfController extends StateNotifier<PdfState> {
pickedPdfPath: null,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
}
@ -36,6 +36,7 @@ class PdfController extends StateNotifier<PdfState> {
pickedPdfBytes: bytes,
signedPage: null,
placementsByPage: {},
placementImageByPage: {},
selectedPlacementIndex: null,
);
}
@ -63,14 +64,27 @@ class PdfController extends StateNotifier<PdfState> {
}
// Multiple-signature helpers
void addPlacement({required int page, required Rect rect}) {
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;
state = state.copyWith(placementsByPage: map, selectedPlacementIndex: null);
// 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}) {
@ -80,13 +94,22 @@ class PdfController extends StateNotifier<PdfState> {
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,
);
}
@ -116,6 +139,30 @@ class PdfController extends StateNotifier<PdfState> {
if (idx == null) return;
removePlacement(page: state.currentPage, index: idx);
}
// Assign a different image name to a placement on a page.
void assignImageToPlacement({
required int page,
required int index,
required String image,
}) {
if (!state.loaded) return;
final p = page.clamp(1, state.pageCount);
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
final list = List<String>.from(imgMap[p] ?? const []);
if (index >= 0 && index < list.length) {
list[index] = image;
imgMap[p] = list;
state = state.copyWith(placementImageByPage: imgMap);
}
}
// 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>(
@ -155,10 +202,10 @@ class SignatureController extends StateNotifier<SignatureState> {
}
void setInvalidSelected(BuildContext context) {
final l = AppLocalizations.of(context);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.invalidOrUnsupportedFile)));
// Fallback message without localization to keep core logic testable
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid or unsupported file')),
);
}
void drag(Offset delta) {

View File

@ -31,6 +31,10 @@ class TestWorld {
static String? currentTheme; // actual UI theme applied: 'light' | 'dark'
static String? currentLanguage; // 'en' | 'zh-TW' | 'es'
static bool settingsOpen = false;
// Signature image name loaded via steps (e.g., 'alice.png')
static String? currentImageName;
// Counters for steps that are called multiple times without params
static int placeFromPictureCallCount = 0;
static void reset() {
prevCenter = null;
@ -52,5 +56,7 @@ class TestWorld {
currentTheme = null;
currentLanguage = null;
settingsOpen = false;
currentImageName = null;
placeFromPictureCallCount = 0;
}
}

View File

@ -5,18 +5,20 @@ import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: a signature is placed on page 2
Future<void> aSignatureIsPlacedOnPage2(WidgetTester tester) async {
/// Usage: a signature is placed on page {2}
Future<void> aSignatureIsPlacedOnPage(WidgetTester tester, num page) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
.openPicked(path: 'mock.pdf', pageCount: 6);
// Ensure image and rect
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
container.read(signatureProvider.notifier).placeDefaultRect();
final r = container.read(signatureProvider).rect!;
container.read(pdfProvider.notifier).addPlacement(page: 2, rect: r);
expect(container.read(pdfProvider.notifier).placementsOn(2), isNotEmpty);
final Rect r = container.read(signatureProvider).rect!;
container
.read(pdfProvider.notifier)
.addPlacement(page: page.toInt(), rect: r, image: 'default.png');
}

View File

@ -0,0 +1,26 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: an image {"bob.png"} is loaded
Future<void> anImageIsLoaded(WidgetTester tester, String param1) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Remember current image name
TestWorld.currentImageName = param1;
// Map name to deterministic bytes for testing
Uint8List bytes;
switch (param1) {
case 'alice.png':
bytes = Uint8List.fromList([1, 2, 3]);
break;
case 'bob.png':
bytes = Uint8List.fromList([4, 5, 6]);
break;
default:
bytes = Uint8List.fromList(param1.codeUnits.take(10).toList());
}
container.read(signatureProvider.notifier).setImageBytes(bytes);
}

View File

@ -0,0 +1,22 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the selected signature is shown with image {"bob.png"}
Future<void> theSelectedSignatureIsShownWithImage(
WidgetTester tester,
String expected,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final pdf = container.read(pdfProvider);
final page = pdf.currentPage;
final idx =
pdf.selectedPlacementIndex ??
((pdf.placementsByPage[page]?.length ?? 1) - 1);
final name = container
.read(pdfProvider.notifier)
.imageOfPlacement(page: page, index: idx);
expect(name, expected);
}

View File

@ -1,10 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the signature on page 5 is shown on page 5
Future<void> theSignatureOnPage5IsShownOnPage5(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(pdfProvider.notifier).placementsOn(5), isNotEmpty);
}

View File

@ -0,0 +1,19 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the signature on page {5} is shown on page {5}
Future<void> theSignatureOnPageIsShownOnPage(
WidgetTester tester,
num sourcePage,
num targetPage,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final srcList = container.read(pdfProvider.notifier).placementsOn(sourcePage.toInt());
final tgtList = container.read(pdfProvider.notifier).placementsOn(targetPage.toInt());
// At least one exists on both
expect(srcList, isNotEmpty);
expect(tgtList, isNotEmpty);
}

View File

@ -3,8 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the signature on page 2 remains
Future<void> theSignatureOnPage2Remains(WidgetTester tester) async {
/// Usage: the signature on page {2} remains
Future<void> theSignatureOnPageRemains(WidgetTester tester, num page) async {
final container = TestWorld.container ?? ProviderContainer();
expect(container.read(pdfProvider.notifier).placementsOn(2), isNotEmpty);
TestWorld.container = container;
final list = container.read(pdfProvider.notifier).placementsOn(page.toInt());
expect(list, isNotEmpty);
}

View File

@ -0,0 +1,30 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user assigns {"bob.png"} to the selected signature
Future<void> theUserAssignsToTheSelectedSignature(
WidgetTester tester,
String newImageName,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Load the new image into signature state (simulating pick)
Uint8List bytes =
newImageName == 'bob.png'
? Uint8List.fromList([4, 5, 6])
: Uint8List.fromList([1, 2, 3]);
container.read(signatureProvider.notifier).setImageBytes(bytes);
TestWorld.currentImageName = newImageName;
// Assign to currently selected placement
final pdf = container.read(pdfProvider);
final page = pdf.currentPage;
final idx =
pdf.selectedPlacementIndex ??
((pdf.placementsByPage[page]?.length ?? 1) - 1);
container
.read(pdfProvider.notifier)
.assignImageToPlacement(page: page, index: idx, image: newImageName);
}

View File

@ -1,28 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user navigates to page 3 and places another signature
Future<void> theUserNavigatesToPage3AndPlacesAnotherSignature(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Assume doc already open from previous step; if not, open a default one
final pdf = container.read(pdfProvider);
if (!pdf.loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
}
// Prepare signature
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
container.read(pdfProvider.notifier).jumpTo(3);
container.read(signatureProvider.notifier).placeDefaultRect();
final r = container.read(signatureProvider).rect!;
container.read(pdfProvider.notifier).addPlacement(page: 3, rect: r);
}

View File

@ -1,30 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user navigates to page 5 and places another signature
Future<void> theUserNavigatesToPage5AndPlacesAnotherSignature(
WidgetTester tester,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 6);
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
container.read(pdfProvider.notifier).jumpTo(5);
container.read(signatureProvider.notifier).placeDefaultRect();
final r = container.read(signatureProvider).rect!;
container.read(pdfProvider.notifier).addPlacement(page: 5, rect: r);
// Defensive: ensure earlier placement on page 2 remains (some setups may recreate state)
final p2 = container.read(pdfProvider.notifier).placementsOn(2);
if (p2.isEmpty) {
container
.read(pdfProvider.notifier)
.addPlacement(page: 2, rect: r.translate(-50, -50));
}
}

View File

@ -0,0 +1,34 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user navigates to page {3} and places another signature
Future<void> theUserNavigatesToPageAndPlacesAnotherSignature(
WidgetTester tester,
num page,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Ensure doc open
final pdf = container.read(pdfProvider);
if (!pdf.loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 6);
}
container.read(pdfProvider.notifier).jumpTo(page.toInt());
// Ensure an image is loaded
if (container.read(signatureProvider).imageBytes == null) {
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}
container.read(signatureProvider.notifier).placeDefaultRect();
final Rect r = container.read(signatureProvider).rect!;
container
.read(pdfProvider.notifier)
.addPlacement(page: page.toInt(), rect: r, image: 'default.png');
}

View File

@ -0,0 +1,57 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user places a signature from picture <second_image> on page <second_page>
Future<void> theUserPlacesASignatureFromPictureOnPage(
WidgetTester tester, [
dynamic imageName,
dynamic pageNumber,
]) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Ensure a document is open
final pdf = container.read(pdfProvider);
if (!pdf.loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 6);
}
// Load image bytes based on provided name
if (imageName == null) {
// Alternate between alice/bob for the first two calls to match Examples
final idx = TestWorld.placeFromPictureCallCount++;
imageName = (idx % 2 == 0) ? 'alice.png' : 'bob.png';
}
final String name =
imageName is String
? imageName
: (imageName?.toString() ?? 'default.png');
Uint8List bytes;
switch (name) {
case 'alice.png':
bytes = Uint8List.fromList([1, 2, 3]);
break;
case 'bob.png':
bytes = Uint8List.fromList([4, 5, 6]);
break;
default:
bytes = Uint8List.fromList([7, 8, 9]);
}
container.read(signatureProvider.notifier).setImageBytes(bytes);
// Place default rect and add placement on target page with image name
container.read(signatureProvider.notifier).placeDefaultRect();
final Rect r = container.read(signatureProvider).rect!;
final int page =
(pageNumber is num)
? pageNumber.toInt()
: int.tryParse(pageNumber?.toString() ?? '') ??
// Default pages for the two calls in the scenario: 1 then 3
((TestWorld.placeFromPictureCallCount <= 1) ? 1 : 3);
container
.read(pdfProvider.notifier)
.addPlacement(page: page, rect: r, image: name);
}

View File

@ -0,0 +1,33 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user places a signature on page {1}
Future<void> theUserPlacesASignatureOnPage(
WidgetTester tester,
num page,
) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
// Ensure doc open
final pdf = container.read(pdfProvider);
if (!pdf.loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 6);
}
// Ensure an image is loaded
if (container.read(signatureProvider).imageBytes == null) {
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
}
container.read(signatureProvider.notifier).placeDefaultRect();
final Rect r = container.read(signatureProvider).rect!;
container
.read(pdfProvider.notifier)
.addPlacement(page: page.toInt(), rect: r, image: 'default.png');
}

View File

@ -1,21 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user places a signature on page 1
Future<void> theUserPlacesASignatureOnPage1(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5);
container
.read(signatureProvider.notifier)
.setImageBytes(Uint8List.fromList([1, 2, 3]));
container.read(signatureProvider.notifier).placeDefaultRect();
final r = container.read(signatureProvider).rect!;
container.read(pdfProvider.notifier).addPlacement(page: 1, rect: r);
}

View File

@ -0,0 +1,40 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
import '_world.dart';
/// Usage: the user places a signature on the page
Future<void> theUserPlacesASignatureOnThePage(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container;
final pdf = container.read(pdfProvider);
if (!pdf.loaded) {
container
.read(pdfProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 1);
container.read(pdfProvider.notifier).setSignedPage(1);
}
// Ensure image bytes
if (container.read(signatureProvider).imageBytes == null) {
final name = TestWorld.currentImageName ?? 'alice.png';
Uint8List bytes =
name == 'bob.png'
? Uint8List.fromList([4, 5, 6])
: Uint8List.fromList([1, 2, 3]);
container.read(signatureProvider.notifier).setImageBytes(bytes);
}
container.read(signatureProvider.notifier).placeDefaultRect();
final Rect r = container.read(signatureProvider).rect!;
final int page = container.read(pdfProvider).signedPage ?? 1;
final imgName = TestWorld.currentImageName ?? 'alice.png';
container
.read(pdfProvider.notifier)
.addPlacement(page: page, rect: r, image: imgName);
// Select the just placed signature (last index)
final list = container.read(pdfProvider).placementsByPage[page] ?? const [];
container
.read(pdfProvider.notifier)
.selectPlacement(list.isEmpty ? null : (list.length - 1));
}