From 81a352a5133fcbb52a479ec1ad0f06287a0dfdfb Mon Sep 17 00:00:00 2001 From: insleker Date: Fri, 19 Sep 2025 21:55:59 +0800 Subject: [PATCH] feat: found root cause of slow image process is store them in bytes rather than image object --- lib/domain/models/signature_card.dart | 2 +- lib/ui/features/pdf/widgets/draw_canvas.dart | 4 +- .../features/pdf/widgets/pdf_page_area.dart | 16 ---- lib/ui/features/pdf/widgets/pdf_screen.dart | 21 ------ .../pdf/widgets/pdf_viewer_widget.dart | 20 ----- .../widgets/rotated_signature_image.dart | 21 +++--- ...ure_card.dart => signature_card_view.dart} | 75 ++++++++++++------- .../widgets/signature_drag_data.dart | 4 +- .../signature/widgets/signature_drawer.dart | 6 +- .../widget/pdf_page_area_early_jump_test.dart | 9 +-- test/widget/pdf_page_area_jump_test.dart | 9 +-- test/widget/pdf_page_area_test.dart | 14 +--- 12 files changed, 70 insertions(+), 131 deletions(-) rename lib/ui/features/signature/widgets/{signature_card.dart => signature_card_view.dart} (72%) diff --git a/lib/domain/models/signature_card.dart b/lib/domain/models/signature_card.dart index c6aeafe..352c02c 100644 --- a/lib/domain/models/signature_card.dart +++ b/lib/domain/models/signature_card.dart @@ -18,7 +18,7 @@ class SignatureCard { }); SignatureCard copyWith({ - double? rotationDeg, + double? rotationDeg, //z axis is out of the screen, positive is CCW SignatureAsset? asset, GraphicAdjust? graphicAdjust, }) => SignatureCard( diff --git a/lib/ui/features/pdf/widgets/draw_canvas.dart b/lib/ui/features/pdf/widgets/draw_canvas.dart index 25cb0aa..30ce0b1 100644 --- a/lib/ui/features/pdf/widgets/draw_canvas.dart +++ b/lib/ui/features/pdf/widgets/draw_canvas.dart @@ -54,8 +54,8 @@ class _DrawCanvasState extends State { onPressed: () async { // Export signature to PNG bytes first final byteData = await _control.toImage( - width: 1024, - height: 512, + width: 512, + height: 256, fit: true, color: Colors.black, background: Colors.transparent, diff --git a/lib/ui/features/pdf/widgets/pdf_page_area.dart b/lib/ui/features/pdf/widgets/pdf_page_area.dart index 5da75db..fd3218a 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_area.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_area.dart @@ -11,21 +11,10 @@ class PdfPageArea extends ConsumerStatefulWidget { const PdfPageArea({ super.key, required this.pageSize, - required this.onDragSignature, - required this.onResizeSignature, - required this.onConfirmSignature, - required this.onClearActiveOverlay, - required this.onSelectPlaced, required this.controller, }); final Size pageSize; - // viewerController removed in migration - final ValueChanged onDragSignature; - final ValueChanged onResizeSignature; - final VoidCallback onConfirmSignature; - final VoidCallback onClearActiveOverlay; - final ValueChanged onSelectPlaced; final PdfViewerController controller; @override ConsumerState createState() => _PdfPageAreaState(); @@ -156,11 +145,6 @@ class _PdfPageAreaState extends ConsumerState { if (isContinuous) { return PdfViewerWidget( pageSize: widget.pageSize, - onDragSignature: widget.onDragSignature, - onResizeSignature: widget.onResizeSignature, - onConfirmSignature: widget.onConfirmSignature, - onClearActiveOverlay: widget.onClearActiveOverlay, - onSelectPlaced: widget.onSelectPlaced, pageKeyBuilder: _pageKey, scrollToPage: _scrollToPage, controller: widget.controller, diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index ea76d27..f9f918c 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -109,22 +109,6 @@ class _PdfSignatureHomePageState extends ConsumerState { return bytes; } - void _confirmSignature() { - // In simplified UI, confirmation is a no-op - } - - void _onDragSignature(Offset delta) { - // In simplified UI, interactive overlay disabled - } - - void _onResizeSignature(Offset delta) { - // In simplified UI, interactive overlay disabled - } - - void _onSelectPlaced(int? index) { - // In simplified UI, selection is a no-op for tests - } - Future _openDrawCanvas() async { final result = await showModalBottomSheet( context: context, @@ -323,11 +307,6 @@ class _PdfSignatureHomePageState extends ConsumerState { controller: _viewModel.controller, key: const ValueKey('pdf_page_area'), pageSize: _pageSize, - onDragSignature: _onDragSignature, - onResizeSignature: _onResizeSignature, - onConfirmSignature: _confirmSignature, - onClearActiveOverlay: () {}, - onSelectPlaced: _onSelectPlaced, ), ), ), diff --git a/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart b/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart index 719d2d4..a5ff410 100644 --- a/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart +++ b/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart @@ -10,22 +10,12 @@ class PdfViewerWidget extends ConsumerStatefulWidget { const PdfViewerWidget({ super.key, required this.pageSize, - required this.onDragSignature, - required this.onResizeSignature, - required this.onConfirmSignature, - required this.onClearActiveOverlay, - required this.onSelectPlaced, this.pageKeyBuilder, this.scrollToPage, required this.controller, }); final Size pageSize; - final ValueChanged onDragSignature; - final ValueChanged onResizeSignature; - final VoidCallback onConfirmSignature; - final VoidCallback onClearActiveOverlay; - final ValueChanged onSelectPlaced; final GlobalKey Function(int page)? pageKeyBuilder; final void Function(int page)? scrollToPage; final PdfViewerController controller; @@ -88,11 +78,6 @@ class _PdfViewerWidgetState extends ConsumerState { widget.pageKeyBuilder ?? (page) => GlobalKey(debugLabel: 'page_$page'), scrollToPage: widget.scrollToPage ?? (page) {}, - onDragSignature: widget.onDragSignature, - onResizeSignature: widget.onResizeSignature, - onConfirmSignature: widget.onConfirmSignature, - onClearActiveOverlay: widget.onClearActiveOverlay, - onSelectPlaced: widget.onSelectPlaced, ); } @@ -163,11 +148,6 @@ class _PdfViewerWidgetState extends ConsumerState { PdfPageOverlays( pageSize: Size(pageRect.width, pageRect.height), pageNumber: page.pageNumber, - onDragSignature: widget.onDragSignature, - onResizeSignature: widget.onResizeSignature, - onConfirmSignature: widget.onConfirmSignature, - onClearActiveOverlay: widget.onClearActiveOverlay, - onSelectPlaced: widget.onSelectPlaced, ), ]; }, diff --git a/lib/ui/features/signature/widgets/rotated_signature_image.dart b/lib/ui/features/signature/widgets/rotated_signature_image.dart index 75ca9cd..f27f693 100644 --- a/lib/ui/features/signature/widgets/rotated_signature_image.dart +++ b/lib/ui/features/signature/widgets/rotated_signature_image.dart @@ -1,10 +1,10 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:image/image.dart' as img; import '../../../../utils/rotation_utils.dart' as rot; /// A lightweight widget to render signature bytes with rotation and an /// angle-aware scale-to-fit so the rotated image stays within its bounds. +/// Aware that `decodeImage` large images can be crazily slow, especially on web. class RotatedSignatureImage extends StatefulWidget { const RotatedSignatureImage({ super.key, @@ -12,6 +12,8 @@ class RotatedSignatureImage extends StatefulWidget { this.rotationDeg = 0.0, // counterclockwise as positive this.filterQuality = FilterQuality.low, this.semanticLabel, + this.cacheWidth, + this.cacheHeight, }); final Uint8List bytes; @@ -22,6 +24,10 @@ class RotatedSignatureImage extends StatefulWidget { final Alignment alignment = Alignment.center; final bool wrapInRepaintBoundary = true; final String? semanticLabel; + // Hint the decoder to decode at a smaller size to reduce memory/latency. + // On some platforms these may be ignored, but they are safe no-ops. + final int? cacheWidth; + final int? cacheHeight; @override State createState() => _RotatedSignatureImageState(); @@ -67,16 +73,6 @@ class _RotatedSignatureImageState extends State { _setAspectRatio(1.0); // safe fallback return; } - // One-time synchronous header decode to establish aspect ratio quickly. - // This only runs when bytes change (not on rotation), so it's acceptable. - try { - final decoded = img.decodeImage(widget.bytes); - if (decoded != null && decoded.width > 0 && decoded.height > 0) { - _setAspectRatio(decoded.width / decoded.height); - } - } catch (_) { - // ignore decode errors and rely on image stream listener - } final stream = _provider.resolve(createLocalImageConfiguration(context)); _stream = stream; _listener = ImageStreamListener((ImageInfo info, bool sync) { @@ -113,6 +109,9 @@ class _RotatedSignatureImageState extends State { filterQuality: widget.filterQuality, alignment: widget.alignment, semanticLabel: widget.semanticLabel, + // Provide at most one dimension to preserve aspect ratio if only one is set + cacheWidth: widget.cacheWidth, + cacheHeight: widget.cacheHeight, isAntiAlias: false, errorBuilder: (context, error, stackTrace) { // Return a placeholder for invalid images diff --git a/lib/ui/features/signature/widgets/signature_card.dart b/lib/ui/features/signature/widgets/signature_card_view.dart similarity index 72% rename from lib/ui/features/signature/widgets/signature_card.dart rename to lib/ui/features/signature/widgets/signature_card_view.dart index ca68456..7cd9680 100644 --- a/lib/ui/features/signature/widgets/signature_card.dart +++ b/lib/ui/features/signature/widgets/signature_card_view.dart @@ -1,3 +1,4 @@ +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/domain/models/model.dart' as domain; @@ -7,15 +8,14 @@ import 'package:pdf_signature/l10n/app_localizations.dart'; import '../view_model/signature_view_model.dart'; import '../view_model/dragging_signature_view_model.dart'; -class SignatureCard extends ConsumerWidget { - const SignatureCard({ +class SignatureCardView extends ConsumerStatefulWidget { + const SignatureCardView({ super.key, required this.asset, required this.disabled, required this.onDelete, this.onTap, this.onAdjust, - this.useCurrentBytesForDrag = false, this.rotationDeg = 0.0, this.graphicAdjust = const domain.GraphicAdjust(), }); @@ -24,9 +24,14 @@ class SignatureCard extends ConsumerWidget { final VoidCallback onDelete; final VoidCallback? onTap; final VoidCallback? onAdjust; - final bool useCurrentBytesForDrag; final double rotationDeg; final domain.GraphicAdjust graphicAdjust; + @override + ConsumerState createState() => _SignatureCardViewState(); +} + +class _SignatureCardViewState extends ConsumerState { + Uint8List? _lastBytesRef; Future _showContextMenu(BuildContext context, Offset position) async { final selected = await showMenu( context: context, @@ -50,22 +55,40 @@ class SignatureCard extends ConsumerWidget { ], ); if (selected == 'adjust') { - onAdjust?.call(); + widget.onAdjust?.call(); } else if (selected == 'delete') { - onDelete(); + widget.onDelete(); } } + void _maybePrecache(Uint8List bytes) { + if (identical(_lastBytesRef, bytes)) return; + _lastBytesRef = bytes; + // Schedule after frame to avoid doing work during build. + WidgetsBinding.instance.addPostFrameCallback((_) { + // Use single-dimension hints to preserve aspect ratio. + final img128 = ResizeImage(MemoryImage(bytes), height: 128); + final img256 = ResizeImage(MemoryImage(bytes), height: 256); + precacheImage(img128, context); + precacheImage(img256, context); + }); + } + @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final displayData = ref .watch(signatureViewModelProvider) - .getDisplaySignatureData(asset, graphicAdjust); + .getDisplaySignatureData(widget.asset, widget.graphicAdjust); + _maybePrecache(displayData.bytes); // Fit inside 96x64 with 6px padding using the shared rotated image widget const boxW = 96.0, boxH = 64.0, pad = 6.0; + // Hint decoder with small target size to reduce decode cost. + // The card shows inside 96x64 with 6px padding; request ~128px max. Widget coreImage = RotatedSignatureImage( bytes: displayData.bytes, - rotationDeg: rotationDeg, + rotationDeg: widget.rotationDeg, + // Only set one dimension to keep aspect ratio + cacheHeight: 128, ); Widget img = (displayData.colorMatrix != null) @@ -102,7 +125,7 @@ class SignatureCard extends ConsumerWidget { top: 0, child: IconButton( icon: const Icon(Icons.close, size: 16), - onPressed: disabled ? null : onDelete, + onPressed: widget.disabled ? null : widget.onDelete, tooltip: 'Remove', padding: const EdgeInsets.all(2), ), @@ -111,33 +134,31 @@ class SignatureCard extends ConsumerWidget { ), ), ); - Widget child = onTap != null ? InkWell(onTap: onTap, child: base) : base; + Widget child = + widget.onTap != null ? InkWell(onTap: widget.onTap, child: base) : base; // Add context menu for adjust/delete on right-click or long-press child = GestureDetector( key: const Key('gd_signature_card_area'), behavior: HitTestBehavior.opaque, onSecondaryTapDown: - disabled + widget.disabled ? null : (details) => _showContextMenu(context, details.globalPosition), onLongPressStart: - disabled + widget.disabled ? null : (details) => _showContextMenu(context, details.globalPosition), child: child, ); - if (disabled) return child; + if (widget.disabled) return child; return Draggable( - data: - useCurrentBytesForDrag - ? const SignatureDragData() - : SignatureDragData( - card: domain.SignatureCard( - asset: asset, - rotationDeg: rotationDeg, - graphicAdjust: graphicAdjust, - ), - ), + data: SignatureDragData( + card: domain.SignatureCard( + asset: widget.asset, + rotationDeg: widget.rotationDeg, + graphicAdjust: widget.graphicAdjust, + ), + ), onDragStarted: () { ref.read(isDraggingSignatureViewModelProvider.notifier).state = true; }, @@ -166,12 +187,14 @@ class SignatureCard extends ConsumerWidget { ), child: RotatedSignatureImage( bytes: displayData.bytes, - rotationDeg: rotationDeg, + rotationDeg: widget.rotationDeg, + cacheHeight: 256, ), ) : RotatedSignatureImage( bytes: displayData.bytes, - rotationDeg: rotationDeg, + rotationDeg: widget.rotationDeg, + cacheHeight: 256, ), ), ), diff --git a/lib/ui/features/signature/widgets/signature_drag_data.dart b/lib/ui/features/signature/widgets/signature_drag_data.dart index 12facf6..4a4452a 100644 --- a/lib/ui/features/signature/widgets/signature_drag_data.dart +++ b/lib/ui/features/signature/widgets/signature_drag_data.dart @@ -1,6 +1,6 @@ import 'package:pdf_signature/domain/models/model.dart'; class SignatureDragData { - final SignatureCard? card; // null means use current processed signature - const SignatureDragData({this.card}); + final SignatureCard card; // null means use current processed signature + const SignatureDragData({required this.card}); } diff --git a/lib/ui/features/signature/widgets/signature_drawer.dart b/lib/ui/features/signature/widgets/signature_drawer.dart index a1662e3..da3b4c0 100644 --- a/lib/ui/features/signature/widgets/signature_drawer.dart +++ b/lib/ui/features/signature/widgets/signature_drawer.dart @@ -6,9 +6,9 @@ import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; -import 'package:pdf_signature/domain/models/model.dart' hide SignatureCard; +import 'package:pdf_signature/domain/models/signature_asset.dart'; import 'image_editor_dialog.dart'; -import 'signature_card.dart'; +import 'signature_card_view.dart'; import '../../pdf/view_model/pdf_view_model.dart'; /// Data for drag-and-drop is in signature_drag_data.dart @@ -49,7 +49,7 @@ class _SignatureDrawerState extends ConsumerState { margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(12), - child: SignatureCard( + child: SignatureCardView( key: ValueKey('sig_card_${library.indexOf(card)}'), asset: card.asset, rotationDeg: card.rotationDeg, diff --git a/test/widget/pdf_page_area_early_jump_test.dart b/test/widget/pdf_page_area_early_jump_test.dart index c7e5d6f..9ead012 100644 --- a/test/widget/pdf_page_area_early_jump_test.dart +++ b/test/widget/pdf_page_area_early_jump_test.dart @@ -42,11 +42,6 @@ void main() { height: 520, child: PdfPageArea( pageSize: Size(676, 400), - onDragSignature: _noopOffset, - onResizeSignature: _noopOffset, - onConfirmSignature: _noop, - onClearActiveOverlay: _noop, - onSelectPlaced: _noopInt, controller: PdfViewerController(), ), ), @@ -75,6 +70,4 @@ void main() { }); } -void _noop() {} -void _noopInt(int? _) {} -void _noopOffset(Offset _) {} +// No extra callbacks required in the new API diff --git a/test/widget/pdf_page_area_jump_test.dart b/test/widget/pdf_page_area_jump_test.dart index da30aa1..c482875 100644 --- a/test/widget/pdf_page_area_jump_test.dart +++ b/test/widget/pdf_page_area_jump_test.dart @@ -43,11 +43,6 @@ void main() { height: 520, child: PdfPageArea( pageSize: Size(676, 400), - onDragSignature: _noopOffset, - onResizeSignature: _noopOffset, - onConfirmSignature: _noop, - onClearActiveOverlay: _noop, - onSelectPlaced: _noopInt, controller: PdfViewerController(), ), ), @@ -106,6 +101,4 @@ void main() { ); } -void _noop() {} -void _noopInt(int? _) {} -void _noopOffset(Offset _) {} +// No extra callbacks required in the new API diff --git a/test/widget/pdf_page_area_test.dart b/test/widget/pdf_page_area_test.dart index b4191e5..dc08759 100644 --- a/test/widget/pdf_page_area_test.dart +++ b/test/widget/pdf_page_area_test.dart @@ -43,11 +43,6 @@ void main() { height: 520, child: PdfPageArea( pageSize: const Size(676, 400), - onDragSignature: _noopOffset, - onResizeSignature: _noopOffset, - onConfirmSignature: _noop, - onClearActiveOverlay: _noop, - onSelectPlaced: _noopInt, controller: PdfViewerController(), ), ), @@ -91,11 +86,6 @@ void main() { // Keep aspect ratio consistent with uiPageSize child: PdfPageArea( pageSize: uiPageSize, - onDragSignature: _noopOffset, - onResizeSignature: _noopOffset, - onConfirmSignature: _noop, - onClearActiveOverlay: _noop, - onSelectPlaced: _noopInt, controller: PdfViewerController(), ), ), @@ -170,6 +160,4 @@ void main() { }); } -void _noop() {} -void _noopInt(int? _) {} -void _noopOffset(Offset _) {} +// No extra callbacks required in the new API