feat: implement image processing and caching in signatureCard
repository
This commit is contained in:
parent
80cf115ab3
commit
26a0c93390
|
|
@ -1,16 +1,73 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../domain/models/model.dart';
|
||||
import '../../data/services/signature_image_processing_service.dart';
|
||||
|
||||
class SignatureCardStateNotifier extends StateNotifier<List<SignatureCard>> {
|
||||
SignatureCardStateNotifier() : super(const []);
|
||||
/// CachedSignatureCard extends SignatureCard with an internal processed cache
|
||||
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) {
|
||||
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) {
|
||||
state = List.of(state)
|
||||
..add(SignatureCard(asset: asset, rotationDeg: rotationDeg));
|
||||
final next = List<CachedSignatureCard>.of(state)
|
||||
..add(CachedSignatureCard(asset: asset, rotationDeg: rotationDeg));
|
||||
state = List<CachedSignatureCard>.unmodifiable(next);
|
||||
}
|
||||
|
||||
void update(
|
||||
|
|
@ -18,30 +75,78 @@ class SignatureCardStateNotifier extends StateNotifier<List<SignatureCard>> {
|
|||
double? rotationDeg,
|
||||
GraphicAdjust? graphicAdjust,
|
||||
) {
|
||||
final list = List<SignatureCard>.of(state);
|
||||
final list = List<CachedSignatureCard>.of(state);
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
final c = list[i];
|
||||
if (c == card) {
|
||||
list[i] = c.copyWith(
|
||||
final updated = c.copyWith(
|
||||
rotationDeg: rotationDeg ?? c.rotationDeg,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
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 =
|
||||
StateNotifierProvider<SignatureCardStateNotifier, List<SignatureCard>>(
|
||||
(ref) => SignatureCardStateNotifier(),
|
||||
);
|
||||
final signatureCardRepositoryProvider = StateNotifierProvider<
|
||||
SignatureCardStateNotifier,
|
||||
List<CachedSignatureCard>
|
||||
>((ref) => SignatureCardStateNotifier());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,4 +18,17 @@ class GraphicAdjust {
|
|||
brightness: brightness ?? this.brightness,
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,22 @@ class SignatureAsset {
|
|||
// List<List<Offset>>? strokes;
|
||||
final String? name; // optional display name (e.g., filename)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class DrawCanvas extends StatefulWidget {
|
|||
this.control,
|
||||
this.onConfirm,
|
||||
this.debugBytesSink,
|
||||
this.closeOnConfirmImmediately = false,
|
||||
});
|
||||
|
||||
final hand.HandSignatureControl? control;
|
||||
|
|
@ -17,6 +18,9 @@ class DrawCanvas extends StatefulWidget {
|
|||
// For tests: allows observing exported bytes without relying on Navigator
|
||||
@visibleForTesting
|
||||
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
|
||||
State<DrawCanvas> createState() => _DrawCanvasState();
|
||||
|
|
@ -48,6 +52,12 @@ class _DrawCanvasState extends State<DrawCanvas> {
|
|||
ElevatedButton(
|
||||
key: const Key('btn_canvas_confirm'),
|
||||
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
|
||||
final byteData = await _control.toImage(
|
||||
width: 1024,
|
||||
|
|
@ -60,7 +70,7 @@ class _DrawCanvasState extends State<DrawCanvas> {
|
|||
widget.debugBytesSink?.value = bytes;
|
||||
if (widget.onConfirm != null) {
|
||||
widget.onConfirm!(bytes);
|
||||
} else {
|
||||
} else if (!widget.closeOnConfirmImmediately) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(bytes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,10 +124,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
builder:
|
||||
(_) => DrawCanvas(
|
||||
onConfirm: (bytes) => Navigator.of(context).pop(bytes),
|
||||
),
|
||||
builder: (_) => const DrawCanvas(closeOnConfirmImmediately: true),
|
||||
);
|
||||
if (result != null && result.isNotEmpty) {
|
||||
// In simplified UI, adding to library isn't implemented
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../domain/models/model.dart';
|
||||
import '../../signature/widgets/rotated_signature_image.dart';
|
||||
import '../../signature/view_model/signature_view_model.dart';
|
||||
|
||||
/// Minimal overlay widget for rendering a placed signature.
|
||||
class SignatureOverlay extends StatelessWidget {
|
||||
class SignatureOverlay extends ConsumerWidget {
|
||||
const SignatureOverlay({
|
||||
super.key,
|
||||
required this.pageSize,
|
||||
|
|
@ -18,7 +20,10 @@ class SignatureOverlay extends StatelessWidget {
|
|||
final int placedIndex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final processedBytes = ref
|
||||
.watch(signatureViewModelProvider)
|
||||
.getProcessedBytes(placement.asset, placement.graphicAdjust);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final left = rect.left * constraints.maxWidth;
|
||||
|
|
@ -40,7 +45,7 @@ class SignatureOverlay extends StatelessWidget {
|
|||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: RotatedSignatureImage(
|
||||
bytes: placement.asset.bytes,
|
||||
bytes: processedBytes,
|
||||
rotationDeg: placement.rotationDeg,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
import 'dart:typed_data';
|
||||
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 {
|
||||
final Ref 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) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import 'dart:typed_data';
|
||||
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 '../../pdf/widgets/adjustments_panel.dart';
|
||||
import '../../../../domain/models/model.dart' as domain;
|
||||
import '../view_model/signature_view_model.dart';
|
||||
import 'rotated_signature_image.dart';
|
||||
import '../../../../data/services/signature_image_processing_service.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
class ImageEditorResult {
|
||||
final double rotation;
|
||||
|
|
@ -16,7 +19,7 @@ class ImageEditorResult {
|
|||
});
|
||||
}
|
||||
|
||||
class ImageEditorDialog extends StatefulWidget {
|
||||
class ImageEditorDialog extends ConsumerStatefulWidget {
|
||||
const ImageEditorDialog({
|
||||
super.key,
|
||||
required this.asset,
|
||||
|
|
@ -29,16 +32,20 @@ class ImageEditorDialog extends StatefulWidget {
|
|||
final domain.GraphicAdjust initialGraphicAdjust;
|
||||
|
||||
@override
|
||||
State<ImageEditorDialog> createState() => _ImageEditorDialogState();
|
||||
ConsumerState<ImageEditorDialog> createState() => _ImageEditorDialogState();
|
||||
}
|
||||
|
||||
class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||
class _ImageEditorDialogState extends ConsumerState<ImageEditorDialog> {
|
||||
late bool _aspectLocked;
|
||||
late bool _bgRemoval;
|
||||
late double _contrast;
|
||||
late double _brightness;
|
||||
late double _rotation;
|
||||
late Uint8List _processedBytes;
|
||||
img.Image? _decodedSource; // Reused decoded source for fast previews
|
||||
bool _previewScheduled = false;
|
||||
bool _previewDirty = false;
|
||||
late final SignatureImageProcessingService _svc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -48,62 +55,47 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
|||
_contrast = widget.initialGraphicAdjust.contrast;
|
||||
_brightness = 1.0; // Changed from 0.0 to 1.0
|
||||
_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() {
|
||||
try {
|
||||
final decoded = img.decodeImage(widget.asset.bytes);
|
||||
_previewDirty = true;
|
||||
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) {
|
||||
img.Image processed = decoded;
|
||||
|
||||
// Apply contrast and brightness first
|
||||
if (_contrast != 1.0 || _brightness != 1.0) {
|
||||
processed = img.adjustColor(
|
||||
processed,
|
||||
contrast: _contrast,
|
||||
brightness: _brightness,
|
||||
);
|
||||
}
|
||||
|
||||
// Apply background removal after color adjustments
|
||||
if (_bgRemoval) {
|
||||
processed = _removeBackground(processed);
|
||||
}
|
||||
|
||||
// Encode back to PNG to preserve transparency
|
||||
_processedBytes = Uint8List.fromList(img.encodePng(processed));
|
||||
final preview = _svc.processPreviewFromDecoded(decoded, adjust);
|
||||
if (mounted) setState(() => _processedBytes = preview);
|
||||
} else {
|
||||
// Fallback to repository path if decode failed
|
||||
final bytes = ref
|
||||
.read(signatureViewModelProvider)
|
||||
.getProcessedBytes(widget.asset, adjust);
|
||||
if (mounted) setState(() => _processedBytes = bytes);
|
||||
}
|
||||
} 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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/domain/models/model.dart' as domain;
|
||||
import 'signature_drag_data.dart';
|
||||
import 'rotated_signature_image.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({
|
||||
super.key,
|
||||
required this.asset,
|
||||
|
|
@ -26,11 +28,14 @@ class SignatureCard extends StatelessWidget {
|
|||
final domain.GraphicAdjust graphicAdjust;
|
||||
|
||||
@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
|
||||
const boxW = 96.0, boxH = 64.0, pad = 6.0;
|
||||
Widget img = RotatedSignatureImage(
|
||||
bytes: asset.bytes,
|
||||
bytes: processedBytes,
|
||||
rotationDeg: rotationDeg,
|
||||
);
|
||||
Widget base = SizedBox(
|
||||
|
|
@ -166,7 +171,7 @@ class SignatureCard extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: RotatedSignatureImage(
|
||||
bytes: asset.bytes,
|
||||
bytes: processedBytes,
|
||||
rotationDeg: rotationDeg,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
|||
if (!mounted) return;
|
||||
final result = await showDialog<ImageEditorResult>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(_) => ImageEditorDialog(
|
||||
asset: card.asset,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
|
|||
container.read(documentRepositoryProvider.notifier).state =
|
||||
Document.initial();
|
||||
container.read(signatureCardRepositoryProvider.notifier).state = [
|
||||
SignatureCard.initial(),
|
||||
CachedSignatureCard.initial(),
|
||||
];
|
||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
|
||||
// Reset page state providers
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Future<void> aSignatureAssetIsLoadedOrDrawn(WidgetTester tester) async {
|
|||
container.read(documentRepositoryProvider.notifier).state =
|
||||
Document.initial();
|
||||
container.read(signatureCardRepositoryProvider.notifier).state = [
|
||||
SignatureCard.initial(),
|
||||
CachedSignatureCard.initial(),
|
||||
];
|
||||
// Use a tiny valid PNG so any later image decoding succeeds.
|
||||
final bytes = Uint8List.fromList([
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Future<void> aSignatureAssetLoadedOrDrawnIsWrappedInASignatureCard(
|
|||
container.read(documentRepositoryProvider.notifier).state =
|
||||
Document.initial();
|
||||
container.read(signatureCardRepositoryProvider.notifier).state = [
|
||||
SignatureCard.initial(),
|
||||
CachedSignatureCard.initial(),
|
||||
];
|
||||
final bytes = Uint8List.fromList([1, 2, 3, 4, 5]);
|
||||
container
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
|
|||
container.read(documentRepositoryProvider.notifier).state =
|
||||
Document.initial();
|
||||
container.read(signatureCardRepositoryProvider.notifier).state = [
|
||||
SignatureCard.initial(),
|
||||
CachedSignatureCard.initial(),
|
||||
];
|
||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
|
||||
final pdfN = container.read(documentRepositoryProvider.notifier);
|
||||
|
|
|
|||
Loading…
Reference in New Issue