feat: found root cause of slow image process is store them in bytes rather than image object

This commit is contained in:
insleker 2025-09-19 21:55:59 +08:00
parent 8daf5ea3ca
commit 81a352a513
12 changed files with 70 additions and 131 deletions

View File

@ -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(

View File

@ -54,8 +54,8 @@ class _DrawCanvasState extends State<DrawCanvas> {
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,

View File

@ -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<Offset> onDragSignature;
final ValueChanged<Offset> onResizeSignature;
final VoidCallback onConfirmSignature;
final VoidCallback onClearActiveOverlay;
final ValueChanged<int?> onSelectPlaced;
final PdfViewerController controller;
@override
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
@ -156,11 +145,6 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
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,

View File

@ -109,22 +109,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
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<Uint8List?> _openDrawCanvas() async {
final result = await showModalBottomSheet<Uint8List>(
context: context,
@ -323,11 +307,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
controller: _viewModel.controller,
key: const ValueKey('pdf_page_area'),
pageSize: _pageSize,
onDragSignature: _onDragSignature,
onResizeSignature: _onResizeSignature,
onConfirmSignature: _confirmSignature,
onClearActiveOverlay: () {},
onSelectPlaced: _onSelectPlaced,
),
),
),

View File

@ -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<Offset> onDragSignature;
final ValueChanged<Offset> onResizeSignature;
final VoidCallback onConfirmSignature;
final VoidCallback onClearActiveOverlay;
final ValueChanged<int?> onSelectPlaced;
final GlobalKey Function(int page)? pageKeyBuilder;
final void Function(int page)? scrollToPage;
final PdfViewerController controller;
@ -88,11 +78,6 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
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<PdfViewerWidget> {
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,
),
];
},

View File

@ -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<RotatedSignatureImage> createState() => _RotatedSignatureImageState();
@ -67,16 +73,6 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
_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<RotatedSignatureImage> {
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

View File

@ -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<SignatureCardView> createState() => _SignatureCardViewState();
}
class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
Uint8List? _lastBytesRef;
Future<void> _showContextMenu(BuildContext context, Offset position) async {
final selected = await showMenu<String>(
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<SignatureDragData>(
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,
),
),
),

View File

@ -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});
}

View File

@ -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<SignatureDrawer> {
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,

View File

@ -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

View File

@ -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

View File

@ -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