feat: implement image processing and caching in signatureCard

repository
This commit is contained in:
insleker 2025-09-17 08:15:35 +08:00
parent 80cf115ab3
commit 26a0c93390
15 changed files with 373 additions and 86 deletions

View File

@ -1,16 +1,73 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/models/model.dart'; import '../../domain/models/model.dart';
import '../../data/services/signature_image_processing_service.dart';
class SignatureCardStateNotifier extends StateNotifier<List<SignatureCard>> { /// CachedSignatureCard extends SignatureCard with an internal processed cache
SignatureCardStateNotifier() : super(const []); class CachedSignatureCard extends SignatureCard {
Uint8List? _cachedProcessed;
CachedSignatureCard({
required super.asset,
required super.rotationDeg,
super.graphicAdjust,
Uint8List? initialProcessed,
});
/// Returns cached processed bytes for the current [graphicAdjust], computing
/// via [service] if not cached yet.
Uint8List getOrComputeProcessed(SignatureImageProcessingService service) {
final existing = _cachedProcessed;
if (existing != null) return existing;
final computed = service.processImage(asset.bytes, graphicAdjust);
_cachedProcessed = computed;
return computed;
}
/// Invalidate the cached processed bytes, forcing recompute next time.
void invalidateCache() {
_cachedProcessed = null;
}
/// Sets/updates the processed bytes explicitly (used after adjustments update)
void setProcessed(Uint8List bytes) {
_cachedProcessed = bytes;
}
factory CachedSignatureCard.initial() => CachedSignatureCard(
asset: SignatureCard.initial().asset,
rotationDeg: SignatureCard.initial().rotationDeg,
graphicAdjust: SignatureCard.initial().graphicAdjust,
);
}
class SignatureCardStateNotifier
extends StateNotifier<List<CachedSignatureCard>> {
SignatureCardStateNotifier() : super(const []) {
state = const <CachedSignatureCard>[];
}
// Stateless image processing service used by this repository
final SignatureImageProcessingService _processingService =
SignatureImageProcessingService();
void add(SignatureCard card) { void add(SignatureCard card) {
state = List.of(state)..add(card); final wrapped =
card is CachedSignatureCard
? card
: CachedSignatureCard(
asset: card.asset,
rotationDeg: card.rotationDeg,
graphicAdjust: card.graphicAdjust,
);
final next = List<CachedSignatureCard>.of(state)..add(wrapped);
state = List<CachedSignatureCard>.unmodifiable(next);
} }
void addWithAsset(SignatureAsset asset, double rotationDeg) { void addWithAsset(SignatureAsset asset, double rotationDeg) {
state = List.of(state) final next = List<CachedSignatureCard>.of(state)
..add(SignatureCard(asset: asset, rotationDeg: rotationDeg)); ..add(CachedSignatureCard(asset: asset, rotationDeg: rotationDeg));
state = List<CachedSignatureCard>.unmodifiable(next);
} }
void update( void update(
@ -18,30 +75,78 @@ class SignatureCardStateNotifier extends StateNotifier<List<SignatureCard>> {
double? rotationDeg, double? rotationDeg,
GraphicAdjust? graphicAdjust, GraphicAdjust? graphicAdjust,
) { ) {
final list = List<SignatureCard>.of(state); final list = List<CachedSignatureCard>.of(state);
for (var i = 0; i < list.length; i++) { for (var i = 0; i < list.length; i++) {
final c = list[i]; final c = list[i];
if (c == card) { if (c == card) {
list[i] = c.copyWith( final updated = c.copyWith(
rotationDeg: rotationDeg ?? c.rotationDeg, rotationDeg: rotationDeg ?? c.rotationDeg,
graphicAdjust: graphicAdjust ?? c.graphicAdjust, graphicAdjust: graphicAdjust ?? c.graphicAdjust,
); );
state = list; // Compute and set the single processed bytes for the updated adjust
final processed = _processingService.processImage(
updated.asset.bytes,
updated.graphicAdjust,
);
final next = CachedSignatureCard(
asset: updated.asset,
rotationDeg: updated.rotationDeg,
graphicAdjust: updated.graphicAdjust,
);
next.setProcessed(processed);
list[i] = next;
state = List<CachedSignatureCard>.unmodifiable(list);
return; return;
} }
} }
} }
void remove(SignatureCard card) { void remove(SignatureCard card) {
state = state.where((c) => c != card).toList(growable: false); state = List<CachedSignatureCard>.unmodifiable(
state.where((c) => c != card).toList(growable: false),
);
} }
void clearAll() { void clearAll() {
state = const []; state = const <CachedSignatureCard>[];
}
/// Returns processed image bytes for the given asset + adjustments.
/// Uses an internal cache to avoid re-processing.
Uint8List getProcessedBytes(SignatureAsset asset, GraphicAdjust adjust) {
// Try to find a matching card by asset
for (final c in state) {
if (c.asset == asset) {
// If requested adjust equals the card's current adjust, use per-card cache
if (c.graphicAdjust == adjust) {
return c.getOrComputeProcessed(_processingService);
}
// Previewing unsaved adjustments: compute without caching
return _processingService.processImage(asset.bytes, adjust);
}
}
// Asset not found among cards (e.g., preview in dialog): compute on-the-fly
return _processingService.processImage(asset.bytes, adjust);
}
/// Clears all cached processed images.
void clearProcessedCache() {
for (final c in state) {
c.invalidateCache();
}
}
/// Clears cached processed images for a specific asset only.
void clearCacheForAsset(SignatureAsset asset) {
for (final c in state) {
if (c.asset == asset) {
c.invalidateCache();
}
}
} }
} }
final signatureCardRepositoryProvider = final signatureCardRepositoryProvider = StateNotifierProvider<
StateNotifierProvider<SignatureCardStateNotifier, List<SignatureCard>>( SignatureCardStateNotifier,
(ref) => SignatureCardStateNotifier(), List<CachedSignatureCard>
); >((ref) => SignatureCardStateNotifier());

View File

@ -0,0 +1,126 @@
import 'dart:typed_data';
import 'package:image/image.dart' as img;
import '../../domain/models/model.dart' as domain;
/// Service for processing signature images with graphic adjustments
class SignatureImageProcessingService {
/// Decode image bytes once and reuse the decoded image for preview processing.
img.Image? decode(Uint8List bytes) {
try {
return img.decodeImage(bytes);
} catch (_) {
return null;
}
}
/// Process image bytes with the given graphic adjustments
Uint8List processImage(Uint8List bytes, domain.GraphicAdjust adjust) {
if (adjust.contrast == 1.0 &&
adjust.brightness == 0.0 &&
!adjust.bgRemoval) {
return bytes; // No processing needed
}
try {
final decoded = img.decodeImage(bytes);
if (decoded != null) {
img.Image processed = decoded;
// Apply contrast and brightness first
if (adjust.contrast != 1.0 || adjust.brightness != 0.0) {
processed = img.adjustColor(
processed,
contrast: adjust.contrast,
brightness: adjust.brightness,
);
}
// Apply background removal after color adjustments
if (adjust.bgRemoval) {
processed = _removeBackground(processed);
}
// Encode back to PNG to preserve transparency
return Uint8List.fromList(img.encodePng(processed));
} else {
return bytes;
}
} catch (e) {
// If processing fails, return original bytes
return bytes;
}
}
/// Fast preview processing:
/// - Reuses a decoded image
/// - Downscales to a small size for UI preview
/// - Uses low-compression PNG to reduce CPU cost
Uint8List processPreviewFromDecoded(
img.Image decoded,
domain.GraphicAdjust adjust, {
int maxDimension = 256,
}) {
try {
// Create a small working copy for quick adjustments
final int w = decoded.width;
final int h = decoded.height;
final double scale = (w > h ? maxDimension / w : maxDimension / h).clamp(
0.0,
1.0,
);
img.Image work =
(scale < 1.0)
? img.copyResize(decoded, width: (w * scale).round())
: img.Image.from(decoded);
// Apply contrast and brightness
if (adjust.contrast != 1.0 || adjust.brightness != 0.0) {
work = img.adjustColor(
work,
contrast: adjust.contrast,
brightness: adjust.brightness,
);
}
// Background removal on downscaled image for speed
if (adjust.bgRemoval) {
work = _removeBackground(work);
}
// Encode with low compression (level 0) for speed
return Uint8List.fromList(img.encodePng(work, level: 0));
} catch (_) {
// Fall back to original size path if something goes wrong
return processImage(
Uint8List.fromList(img.encodePng(decoded, level: 0)),
adjust,
);
}
}
/// Remove near-white background using simple threshold approach for maximum speed
img.Image _removeBackground(img.Image image) {
final result =
image.hasAlpha ? img.Image.from(image) : image.convert(numChannels: 4);
// Simple and fast: single pass through all pixels
for (int y = 0; y < result.height; y++) {
for (int x = 0; x < result.width; x++) {
final pixel = result.getPixel(x, y);
final r = pixel.r;
final g = pixel.g;
final b = pixel.b;
// Simple threshold: if pixel is close to white, make it transparent
const int threshold = 240; // Very close to white
if (r >= threshold && g >= threshold && b >= threshold) {
result.setPixel(
x,
y,
img.ColorRgba8(r.toInt(), g.toInt(), b.toInt(), 0),
);
}
}
}
return result;
}
}

View File

@ -18,4 +18,17 @@ class GraphicAdjust {
brightness: brightness ?? this.brightness, brightness: brightness ?? this.brightness,
bgRemoval: bgRemoval ?? this.bgRemoval, bgRemoval: bgRemoval ?? this.bgRemoval,
); );
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GraphicAdjust &&
runtimeType == other.runtimeType &&
contrast == other.contrast &&
brightness == other.brightness &&
bgRemoval == other.bgRemoval;
@override
int get hashCode =>
contrast.hashCode ^ brightness.hashCode ^ bgRemoval.hashCode;
} }

View File

@ -6,4 +6,22 @@ class SignatureAsset {
// List<List<Offset>>? strokes; // List<List<Offset>>? strokes;
final String? name; // optional display name (e.g., filename) final String? name; // optional display name (e.g., filename)
const SignatureAsset({required this.bytes, this.name}); const SignatureAsset({required this.bytes, this.name});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SignatureAsset &&
name == other.name &&
_bytesEqual(bytes, other.bytes);
@override
int get hashCode => name.hashCode ^ bytes.length.hashCode;
static bool _bytesEqual(Uint8List a, Uint8List b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
} }

View File

@ -10,6 +10,7 @@ class DrawCanvas extends StatefulWidget {
this.control, this.control,
this.onConfirm, this.onConfirm,
this.debugBytesSink, this.debugBytesSink,
this.closeOnConfirmImmediately = false,
}); });
final hand.HandSignatureControl? control; final hand.HandSignatureControl? control;
@ -17,6 +18,9 @@ class DrawCanvas extends StatefulWidget {
// For tests: allows observing exported bytes without relying on Navigator // For tests: allows observing exported bytes without relying on Navigator
@visibleForTesting @visibleForTesting
final ValueNotifier<Uint8List?>? debugBytesSink; final ValueNotifier<Uint8List?>? debugBytesSink;
// When true (used by bottom sheet), the sheet will be closed immediately
// on confirm without waiting for export to finish.
final bool closeOnConfirmImmediately;
@override @override
State<DrawCanvas> createState() => _DrawCanvasState(); State<DrawCanvas> createState() => _DrawCanvasState();
@ -48,6 +52,12 @@ class _DrawCanvasState extends State<DrawCanvas> {
ElevatedButton( ElevatedButton(
key: const Key('btn_canvas_confirm'), key: const Key('btn_canvas_confirm'),
onPressed: () async { onPressed: () async {
// If requested, close the sheet immediately without waiting
// for the potentially heavy export.
if (widget.closeOnConfirmImmediately &&
Navigator.canPop(context)) {
Navigator.of(context).pop();
}
// Export signature to PNG bytes // Export signature to PNG bytes
final byteData = await _control.toImage( final byteData = await _control.toImage(
width: 1024, width: 1024,
@ -60,7 +70,7 @@ class _DrawCanvasState extends State<DrawCanvas> {
widget.debugBytesSink?.value = bytes; widget.debugBytesSink?.value = bytes;
if (widget.onConfirm != null) { if (widget.onConfirm != null) {
widget.onConfirm!(bytes); widget.onConfirm!(bytes);
} else { } else if (!widget.closeOnConfirmImmediately) {
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pop(bytes); Navigator.of(context).pop(bytes);
} }

View File

@ -124,10 +124,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
enableDrag: false, enableDrag: false,
builder: builder: (_) => const DrawCanvas(closeOnConfirmImmediately: true),
(_) => DrawCanvas(
onConfirm: (bytes) => Navigator.of(context).pop(bytes),
),
); );
if (result != null && result.isNotEmpty) { if (result != null && result.isNotEmpty) {
// In simplified UI, adding to library isn't implemented // In simplified UI, adding to library isn't implemented

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../domain/models/model.dart'; import '../../../../domain/models/model.dart';
import '../../signature/widgets/rotated_signature_image.dart'; import '../../signature/widgets/rotated_signature_image.dart';
import '../../signature/view_model/signature_view_model.dart';
/// Minimal overlay widget for rendering a placed signature. /// Minimal overlay widget for rendering a placed signature.
class SignatureOverlay extends StatelessWidget { class SignatureOverlay extends ConsumerWidget {
const SignatureOverlay({ const SignatureOverlay({
super.key, super.key,
required this.pageSize, required this.pageSize,
@ -18,7 +20,10 @@ class SignatureOverlay extends StatelessWidget {
final int placedIndex; final int placedIndex;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final processedBytes = ref
.watch(signatureViewModelProvider)
.getProcessedBytes(placement.asset, placement.graphicAdjust);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final left = rect.left * constraints.maxWidth; final left = rect.left * constraints.maxWidth;
@ -40,7 +45,7 @@ class SignatureOverlay extends StatelessWidget {
child: FittedBox( child: FittedBox(
fit: BoxFit.contain, fit: BoxFit.contain,
child: RotatedSignatureImage( child: RotatedSignatureImage(
bytes: placement.asset.bytes, bytes: processedBytes,
rotationDeg: placement.rotationDeg, rotationDeg: placement.rotationDeg,
), ),
), ),

View File

@ -1,11 +1,26 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/domain/models/model.dart' as domain;
import 'package:pdf_signature/data/repositories/signature_card_repository.dart'
as repo;
class SignatureViewModel { class SignatureViewModel {
final Ref ref; final Ref ref;
SignatureViewModel(this.ref); SignatureViewModel(this.ref);
// Add methods as needed Uint8List getProcessedBytes(
domain.SignatureAsset asset,
domain.GraphicAdjust adjust,
) {
final notifier = ref.read(repo.signatureCardRepositoryProvider.notifier);
return notifier.getProcessedBytes(asset, adjust);
}
void clearCache() {
final notifier = ref.read(repo.signatureCardRepositoryProvider.notifier);
notifier.clearProcessedCache();
}
} }
final signatureViewModelProvider = Provider<SignatureViewModel>((ref) { final signatureViewModelProvider = Provider<SignatureViewModel>((ref) {

View File

@ -1,10 +1,13 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image/image.dart' as img; 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/widgets/adjustments_panel.dart'; import '../../pdf/widgets/adjustments_panel.dart';
import '../../../../domain/models/model.dart' as domain; import '../../../../domain/models/model.dart' as domain;
import '../view_model/signature_view_model.dart';
import 'rotated_signature_image.dart'; import 'rotated_signature_image.dart';
import '../../../../data/services/signature_image_processing_service.dart';
import 'package:image/image.dart' as img;
class ImageEditorResult { class ImageEditorResult {
final double rotation; final double rotation;
@ -16,7 +19,7 @@ class ImageEditorResult {
}); });
} }
class ImageEditorDialog extends StatefulWidget { class ImageEditorDialog extends ConsumerStatefulWidget {
const ImageEditorDialog({ const ImageEditorDialog({
super.key, super.key,
required this.asset, required this.asset,
@ -29,16 +32,20 @@ class ImageEditorDialog extends StatefulWidget {
final domain.GraphicAdjust initialGraphicAdjust; final domain.GraphicAdjust initialGraphicAdjust;
@override @override
State<ImageEditorDialog> createState() => _ImageEditorDialogState(); ConsumerState<ImageEditorDialog> createState() => _ImageEditorDialogState();
} }
class _ImageEditorDialogState extends State<ImageEditorDialog> { class _ImageEditorDialogState extends ConsumerState<ImageEditorDialog> {
late bool _aspectLocked; late bool _aspectLocked;
late bool _bgRemoval; late bool _bgRemoval;
late double _contrast; late double _contrast;
late double _brightness; late double _brightness;
late double _rotation; late double _rotation;
late Uint8List _processedBytes; late Uint8List _processedBytes;
img.Image? _decodedSource; // Reused decoded source for fast previews
bool _previewScheduled = false;
bool _previewDirty = false;
late final SignatureImageProcessingService _svc;
@override @override
void initState() { void initState() {
@ -48,62 +55,47 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
_contrast = widget.initialGraphicAdjust.contrast; _contrast = widget.initialGraphicAdjust.contrast;
_brightness = 1.0; // Changed from 0.0 to 1.0 _brightness = 1.0; // Changed from 0.0 to 1.0
_rotation = widget.initialRotation; _rotation = widget.initialRotation;
_processedBytes = widget.asset.bytes; // Initialize with original bytes _processedBytes = widget.asset.bytes; // initial preview
_svc = SignatureImageProcessingService();
// Decode once for preview reuse
// Note: package:image lives in service; expose decode via service
_decodedSource = _svc.decode(widget.asset.bytes);
} }
/// Update processed image bytes when processing parameters change @override
void dispose() {
// Frame callbacks are tied to mounting; nothing to cancel explicitly
super.dispose();
}
/// Update processed image bytes when processing parameters change.
/// Coalesce rapid changes once per frame to keep UI responsive and tests stable.
void _updateProcessedBytes() { void _updateProcessedBytes() {
try { _previewDirty = true;
final decoded = img.decodeImage(widget.asset.bytes); if (_previewScheduled) return;
_previewScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_previewScheduled = false;
if (!mounted || !_previewDirty) return;
_previewDirty = false;
final adjust = domain.GraphicAdjust(
contrast: _contrast,
brightness: _brightness,
bgRemoval: _bgRemoval,
);
// Fast preview path: reuse decoded, downscale, low-compression encode
final decoded = _decodedSource;
if (decoded != null) { if (decoded != null) {
img.Image processed = decoded; final preview = _svc.processPreviewFromDecoded(decoded, adjust);
if (mounted) setState(() => _processedBytes = preview);
// Apply contrast and brightness first } else {
if (_contrast != 1.0 || _brightness != 1.0) { // Fallback to repository path if decode failed
processed = img.adjustColor( final bytes = ref
processed, .read(signatureViewModelProvider)
contrast: _contrast, .getProcessedBytes(widget.asset, adjust);
brightness: _brightness, if (mounted) setState(() => _processedBytes = bytes);
);
}
// Apply background removal after color adjustments
if (_bgRemoval) {
processed = _removeBackground(processed);
}
// Encode back to PNG to preserve transparency
_processedBytes = Uint8List.fromList(img.encodePng(processed));
} }
} catch (e) { });
// If processing fails, keep original bytes
_processedBytes = widget.asset.bytes;
}
}
/// Remove near-white background using simple threshold approach for maximum speed
/// TODO: remove double loops with SIMD matrix
img.Image _removeBackground(img.Image image) {
final result =
image.hasAlpha ? img.Image.from(image) : image.convert(numChannels: 4);
// Simple and fast: single pass through all pixels
for (int y = 0; y < result.height; y++) {
for (int x = 0; x < result.width; x++) {
final pixel = result.getPixel(x, y);
final r = pixel.r;
final g = pixel.g;
final b = pixel.b;
// Simple threshold: if pixel is close to white, make it transparent
const int threshold = 240; // Very close to white
if (r >= threshold && g >= threshold && b >= threshold) {
result.setPixelRgba(x, y, r, g, b, 0);
}
}
}
return result;
} }
@override @override

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/domain/models/model.dart' as domain; import 'package:pdf_signature/domain/models/model.dart' as domain;
import 'signature_drag_data.dart'; import 'signature_drag_data.dart';
import 'rotated_signature_image.dart'; import 'rotated_signature_image.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../view_model/signature_view_model.dart';
class SignatureCard extends StatelessWidget { class SignatureCard extends ConsumerWidget {
const SignatureCard({ const SignatureCard({
super.key, super.key,
required this.asset, required this.asset,
@ -26,11 +28,14 @@ class SignatureCard extends StatelessWidget {
final domain.GraphicAdjust graphicAdjust; final domain.GraphicAdjust graphicAdjust;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final processedBytes = ref
.watch(signatureViewModelProvider)
.getProcessedBytes(asset, graphicAdjust);
// Fit inside 96x64 with 6px padding using the shared rotated image widget // Fit inside 96x64 with 6px padding using the shared rotated image widget
const boxW = 96.0, boxH = 64.0, pad = 6.0; const boxW = 96.0, boxH = 64.0, pad = 6.0;
Widget img = RotatedSignatureImage( Widget img = RotatedSignatureImage(
bytes: asset.bytes, bytes: processedBytes,
rotationDeg: rotationDeg, rotationDeg: rotationDeg,
); );
Widget base = SizedBox( Widget base = SizedBox(
@ -166,7 +171,7 @@ class SignatureCard extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
child: RotatedSignatureImage( child: RotatedSignatureImage(
bytes: asset.bytes, bytes: processedBytes,
rotationDeg: rotationDeg, rotationDeg: rotationDeg,
), ),
), ),

View File

@ -62,6 +62,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
if (!mounted) return; if (!mounted) return;
final result = await showDialog<ImageEditorResult>( final result = await showDialog<ImageEditorResult>(
context: context, context: context,
barrierDismissible: false,
builder: builder:
(_) => ImageEditorDialog( (_) => ImageEditorDialog(
asset: card.asset, asset: card.asset,

View File

@ -16,7 +16,7 @@ Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
container.read(documentRepositoryProvider.notifier).state = container.read(documentRepositoryProvider.notifier).state =
Document.initial(); Document.initial();
container.read(signatureCardRepositoryProvider.notifier).state = [ container.read(signatureCardRepositoryProvider.notifier).state = [
SignatureCard.initial(), CachedSignatureCard.initial(),
]; ];
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
// Reset page state providers // Reset page state providers

View File

@ -15,7 +15,7 @@ Future<void> aSignatureAssetIsLoadedOrDrawn(WidgetTester tester) async {
container.read(documentRepositoryProvider.notifier).state = container.read(documentRepositoryProvider.notifier).state =
Document.initial(); Document.initial();
container.read(signatureCardRepositoryProvider.notifier).state = [ container.read(signatureCardRepositoryProvider.notifier).state = [
SignatureCard.initial(), CachedSignatureCard.initial(),
]; ];
// Use a tiny valid PNG so any later image decoding succeeds. // Use a tiny valid PNG so any later image decoding succeeds.
final bytes = Uint8List.fromList([ final bytes = Uint8List.fromList([

View File

@ -17,7 +17,7 @@ Future<void> aSignatureAssetLoadedOrDrawnIsWrappedInASignatureCard(
container.read(documentRepositoryProvider.notifier).state = container.read(documentRepositoryProvider.notifier).state =
Document.initial(); Document.initial();
container.read(signatureCardRepositoryProvider.notifier).state = [ container.read(signatureCardRepositoryProvider.notifier).state = [
SignatureCard.initial(), CachedSignatureCard.initial(),
]; ];
final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); final bytes = Uint8List.fromList([1, 2, 3, 4, 5]);
container container

View File

@ -20,7 +20,7 @@ Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
container.read(documentRepositoryProvider.notifier).state = container.read(documentRepositoryProvider.notifier).state =
Document.initial(); Document.initial();
container.read(signatureCardRepositoryProvider.notifier).state = [ container.read(signatureCardRepositoryProvider.notifier).state = [
SignatureCard.initial(), CachedSignatureCard.initial(),
]; ];
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);