feat: found root cause of slow image process is store them in bytes rather than image object
This commit is contained in:
parent
8daf5ea3ca
commit
81a352a513
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,31 +134,29 @@ 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(
|
||||
data: SignatureDragData(
|
||||
card: domain.SignatureCard(
|
||||
asset: asset,
|
||||
rotationDeg: rotationDeg,
|
||||
graphicAdjust: graphicAdjust,
|
||||
asset: widget.asset,
|
||||
rotationDeg: widget.rotationDeg,
|
||||
graphicAdjust: widget.graphicAdjust,
|
||||
),
|
||||
),
|
||||
onDragStarted: () {
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue