Compare commits
No commits in common. "81a352a5133fcbb52a479ec1ad0f06287a0dfdfb" and "5a03793b546ea9ebcd23a20e3e245c2fe8419047" have entirely different histories.
81a352a513
...
5a03793b54
|
|
@ -7,8 +7,6 @@ import 'package:pdf/pdf.dart' as pdf;
|
||||||
import 'package:printing/printing.dart' as printing;
|
import 'package:printing/printing.dart' as printing;
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import '../../domain/models/model.dart';
|
import '../../domain/models/model.dart';
|
||||||
// math moved to utils in rot
|
|
||||||
import '../../utils/rotation_utils.dart' as rot;
|
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
// - This exporter uses a raster snapshot of the UI (RepaintBoundary) and embeds it into a new PDF.
|
// - This exporter uses a raster snapshot of the UI (RepaintBoundary) and embeds it into a new PDF.
|
||||||
|
|
@ -17,6 +15,50 @@ import '../../utils/rotation_utils.dart' as rot;
|
||||||
// cannot import/modify existing PDF pages. If/when a suitable FOSS library exists, wire it here.
|
// cannot import/modify existing PDF pages. If/when a suitable FOSS library exists, wire it here.
|
||||||
|
|
||||||
class ExportService {
|
class ExportService {
|
||||||
|
/// Compose a new PDF by rasterizing the original PDF pages (via pdfrx engine)
|
||||||
|
/// and optionally stamping a signature image on the specified page.
|
||||||
|
///
|
||||||
|
/// Inputs:
|
||||||
|
/// - [inputPath]: Path to the original PDF to read
|
||||||
|
/// - [outputPath]: Path to write the composed PDF
|
||||||
|
/// - [uiPageSize]: The logical page size used by the UI layout (SignatureCardStateNotifier.pageSize)
|
||||||
|
/// - [signatureImageBytes]: PNG/JPEG bytes of the signature image to overlay
|
||||||
|
/// - [targetDpi]: Rasterization DPI for background pages
|
||||||
|
Future<bool> exportSignedPdfFromFile({
|
||||||
|
required String inputPath,
|
||||||
|
required String outputPath,
|
||||||
|
required Size uiPageSize,
|
||||||
|
required Uint8List? signatureImageBytes,
|
||||||
|
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||||
|
Map<String, Uint8List>? libraryBytes,
|
||||||
|
double targetDpi = 144.0,
|
||||||
|
}) async {
|
||||||
|
// Read source bytes and delegate to bytes-based exporter
|
||||||
|
Uint8List? srcBytes;
|
||||||
|
try {
|
||||||
|
srcBytes = await File(inputPath).readAsBytes();
|
||||||
|
} catch (_) {
|
||||||
|
srcBytes = null;
|
||||||
|
}
|
||||||
|
if (srcBytes == null) return false;
|
||||||
|
final bytes = await exportSignedPdfFromBytes(
|
||||||
|
srcBytes: srcBytes,
|
||||||
|
uiPageSize: uiPageSize,
|
||||||
|
signatureImageBytes: signatureImageBytes,
|
||||||
|
placementsByPage: placementsByPage,
|
||||||
|
libraryBytes: libraryBytes,
|
||||||
|
targetDpi: targetDpi,
|
||||||
|
);
|
||||||
|
if (bytes == null) return false;
|
||||||
|
try {
|
||||||
|
final file = File(outputPath);
|
||||||
|
await file.writeAsBytes(bytes, flush: true);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Compose a new PDF from source PDF bytes; returns the resulting PDF bytes.
|
/// Compose a new PDF from source PDF bytes; returns the resulting PDF bytes.
|
||||||
Future<Uint8List?> exportSignedPdfFromBytes({
|
Future<Uint8List?> exportSignedPdfFromBytes({
|
||||||
required Uint8List srcBytes,
|
required Uint8List srcBytes,
|
||||||
|
|
@ -26,131 +68,6 @@ class ExportService {
|
||||||
Map<String, Uint8List>? libraryBytes,
|
Map<String, Uint8List>? libraryBytes,
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
// Per-call caches to avoid redundant decode/encode and image embedding work
|
|
||||||
final Map<String, Uint8List> _processedBytesCache = <String, Uint8List>{};
|
|
||||||
final Map<String, pw.MemoryImage> _memoryImageCache =
|
|
||||||
<String, pw.MemoryImage>{};
|
|
||||||
final Map<String, double> _aspectRatioCache = <String, double>{};
|
|
||||||
|
|
||||||
// Returns a stable-ish cache key for bytes within this process (not content-hash, but good enough per-call)
|
|
||||||
String _baseKeyForBytes(Uint8List b) =>
|
|
||||||
'${identityHashCode(b)}:${b.length}';
|
|
||||||
|
|
||||||
// Fast PNG signature check (no string allocation)
|
|
||||||
bool _isPng(Uint8List bytes) {
|
|
||||||
if (bytes.length < 8) return false;
|
|
||||||
return bytes[0] == 0x89 &&
|
|
||||||
bytes[1] == 0x50 && // P
|
|
||||||
bytes[2] == 0x4E && // N
|
|
||||||
bytes[3] == 0x47 && // G
|
|
||||||
bytes[4] == 0x0D &&
|
|
||||||
bytes[5] == 0x0A &&
|
|
||||||
bytes[6] == 0x1A &&
|
|
||||||
bytes[7] == 0x0A;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve base (unprocessed) bytes for a placement, considering library override.
|
|
||||||
Uint8List _getBaseBytes(SignaturePlacement placement) {
|
|
||||||
Uint8List baseBytes = placement.asset.bytes;
|
|
||||||
final libKey = placement.asset.name;
|
|
||||||
if (libKey != null && libraryBytes != null) {
|
|
||||||
final libBytes = libraryBytes[libKey];
|
|
||||||
if (libBytes != null && libBytes.isNotEmpty) {
|
|
||||||
baseBytes = libBytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return baseBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get processed bytes for a placement, with caching.
|
|
||||||
Uint8List _getProcessedBytes(SignaturePlacement placement) {
|
|
||||||
final Uint8List baseBytes = _getBaseBytes(placement);
|
|
||||||
|
|
||||||
final adj = placement.graphicAdjust;
|
|
||||||
final cacheKey =
|
|
||||||
'${_baseKeyForBytes(baseBytes)}|c=${adj.contrast}|b=${adj.brightness}|bg=${adj.bgRemoval}';
|
|
||||||
final cached = _processedBytesCache[cacheKey];
|
|
||||||
if (cached != null) return cached;
|
|
||||||
|
|
||||||
// If no graphic changes requested, return bytes as-is (conversion to PNG is deferred to MemoryImage step)
|
|
||||||
final bool needsAdjust =
|
|
||||||
(adj.contrast != 1.0 || adj.brightness != 1.0 || adj.bgRemoval);
|
|
||||||
if (!needsAdjust) {
|
|
||||||
_processedBytesCache[cacheKey] = baseBytes;
|
|
||||||
return baseBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final decoded = img.decodeImage(baseBytes);
|
|
||||||
if (decoded == null) {
|
|
||||||
_processedBytesCache[cacheKey] = baseBytes;
|
|
||||||
return baseBytes;
|
|
||||||
}
|
|
||||||
img.Image processed = decoded;
|
|
||||||
|
|
||||||
if (adj.contrast != 1.0 || adj.brightness != 1.0) {
|
|
||||||
processed = img.adjustColor(
|
|
||||||
processed,
|
|
||||||
contrast: adj.contrast,
|
|
||||||
brightness: adj.brightness,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adj.bgRemoval) {
|
|
||||||
processed = _removeBackground(processed);
|
|
||||||
}
|
|
||||||
|
|
||||||
final outBytes = Uint8List.fromList(img.encodePng(processed));
|
|
||||||
_processedBytesCache[cacheKey] = outBytes;
|
|
||||||
return outBytes;
|
|
||||||
} catch (_) {
|
|
||||||
// If processing fails, fall back to original
|
|
||||||
_processedBytesCache[cacheKey] = baseBytes;
|
|
||||||
return baseBytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap bytes in a pw.MemoryImage with caching, converting to PNG only when necessary.
|
|
||||||
pw.MemoryImage? _getMemoryImage(Uint8List bytes) {
|
|
||||||
final key = _baseKeyForBytes(bytes);
|
|
||||||
final cached = _memoryImageCache[key];
|
|
||||||
if (cached != null) return cached;
|
|
||||||
try {
|
|
||||||
if (_isPng(bytes)) {
|
|
||||||
final imgObj = pw.MemoryImage(bytes);
|
|
||||||
_memoryImageCache[key] = imgObj;
|
|
||||||
return imgObj;
|
|
||||||
}
|
|
||||||
// Convert to PNG to preserve transparency if not already PNG
|
|
||||||
final decoded = img.decodeImage(bytes);
|
|
||||||
if (decoded == null) return null;
|
|
||||||
final png = Uint8List.fromList(img.encodePng(decoded, level: 6));
|
|
||||||
final imgObj = pw.MemoryImage(png);
|
|
||||||
_memoryImageCache[key] = imgObj;
|
|
||||||
return imgObj;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute and cache aspect ratio (width/height) for given bytes
|
|
||||||
double? _getAspectRatioFromBytes(Uint8List bytes) {
|
|
||||||
final key = _baseKeyForBytes(bytes);
|
|
||||||
final c = _aspectRatioCache[key];
|
|
||||||
if (c != null) return c;
|
|
||||||
try {
|
|
||||||
final decoded = img.decodeImage(bytes);
|
|
||||||
if (decoded == null || decoded.width <= 0 || decoded.height <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final ar = decoded.width / decoded.height;
|
|
||||||
_aspectRatioCache[key] = ar;
|
|
||||||
return ar;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final out = pw.Document(version: pdf.PdfVersion.pdf_1_4, compress: false);
|
final out = pw.Document(version: pdf.PdfVersion.pdf_1_4, compress: false);
|
||||||
int pageIndex = 0;
|
int pageIndex = 0;
|
||||||
bool anyPage = false;
|
bool anyPage = false;
|
||||||
|
|
@ -206,22 +123,51 @@ class ExportService {
|
||||||
final w = r.width * widthPts;
|
final w = r.width * widthPts;
|
||||||
final h = r.height * heightPts;
|
final h = r.height * heightPts;
|
||||||
|
|
||||||
// Get processed bytes (cached) and then embed as MemoryImage (cached)
|
// Process the signature asset with its graphic adjustments
|
||||||
Uint8List bytes = _getProcessedBytes(placement);
|
Uint8List bytes = placement.asset.bytes;
|
||||||
|
if (bytes.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// Decode the image
|
||||||
|
final decoded = img.decodeImage(bytes);
|
||||||
|
if (decoded != null) {
|
||||||
|
img.Image processed = decoded;
|
||||||
|
|
||||||
|
// Apply contrast and brightness first
|
||||||
|
if (placement.graphicAdjust.contrast != 1.0 ||
|
||||||
|
placement.graphicAdjust.brightness != 0.0) {
|
||||||
|
processed = img.adjustColor(
|
||||||
|
processed,
|
||||||
|
contrast: placement.graphicAdjust.contrast,
|
||||||
|
brightness: placement.graphicAdjust.brightness,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply background removal after color adjustments
|
||||||
|
if (placement.graphicAdjust.bgRemoval) {
|
||||||
|
processed = _removeBackground(processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode back to PNG to preserve transparency
|
||||||
|
bytes = Uint8List.fromList(img.encodePng(processed));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If processing fails, use original bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fallback if no bytes available
|
||||||
if (bytes.isEmpty && signatureImageBytes != null) {
|
if (bytes.isEmpty && signatureImageBytes != null) {
|
||||||
bytes = signatureImageBytes;
|
bytes = signatureImageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes.isNotEmpty) {
|
if (bytes.isNotEmpty) {
|
||||||
final imgObj = _getMemoryImage(bytes);
|
pw.MemoryImage? imgObj;
|
||||||
|
try {
|
||||||
|
imgObj = pw.MemoryImage(bytes);
|
||||||
|
} catch (_) {
|
||||||
|
imgObj = null;
|
||||||
|
}
|
||||||
if (imgObj != null) {
|
if (imgObj != null) {
|
||||||
// Align with RotatedSignatureImage: counterclockwise positive
|
|
||||||
final angle = rot.radians(placement.rotationDeg);
|
|
||||||
// Prefer AR from base bytes to avoid extra decode of processed
|
|
||||||
final baseBytes = _getBaseBytes(placement);
|
|
||||||
final ar = _getAspectRatioFromBytes(baseBytes);
|
|
||||||
final scaleToFit = rot.scaleToFitForAngle(angle, ar: ar);
|
|
||||||
|
|
||||||
children.add(
|
children.add(
|
||||||
pw.Positioned(
|
pw.Positioned(
|
||||||
left: left,
|
left: left,
|
||||||
|
|
@ -231,12 +177,12 @@ class ExportService {
|
||||||
height: h,
|
height: h,
|
||||||
child: pw.FittedBox(
|
child: pw.FittedBox(
|
||||||
fit: pw.BoxFit.contain,
|
fit: pw.BoxFit.contain,
|
||||||
child: pw.Transform.scale(
|
child: pw.Transform.rotate(
|
||||||
scale: scaleToFit,
|
angle:
|
||||||
child: pw.Transform.rotate(
|
placement.rotationDeg *
|
||||||
angle: angle,
|
3.1415926535 /
|
||||||
child: pw.Image(imgObj),
|
180.0,
|
||||||
),
|
child: pw.Image(imgObj),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -281,7 +227,7 @@ class ExportService {
|
||||||
color: pdf.PdfColors.white,
|
color: pdf.PdfColors.white,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
// Multi-placement stamping on fallback page
|
||||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
for (var i = 0; i < pagePlacements.length; i++) {
|
for (var i = 0; i < pagePlacements.length; i++) {
|
||||||
final placement = pagePlacements[i];
|
final placement = pagePlacements[i];
|
||||||
|
|
@ -292,19 +238,65 @@ class ExportService {
|
||||||
final w = r.width * widthPts;
|
final w = r.width * widthPts;
|
||||||
final h = r.height * heightPts;
|
final h = r.height * heightPts;
|
||||||
|
|
||||||
Uint8List bytes = _getProcessedBytes(placement);
|
// Process the signature asset with its graphic adjustments
|
||||||
|
Uint8List bytes = placement.asset.bytes;
|
||||||
|
if (bytes.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// Decode the image
|
||||||
|
final decoded = img.decodeImage(bytes);
|
||||||
|
if (decoded != null) {
|
||||||
|
img.Image processed = decoded;
|
||||||
|
|
||||||
|
// Apply contrast and brightness first
|
||||||
|
if (placement.graphicAdjust.contrast != 1.0 ||
|
||||||
|
placement.graphicAdjust.brightness != 0.0) {
|
||||||
|
processed = img.adjustColor(
|
||||||
|
processed,
|
||||||
|
contrast: placement.graphicAdjust.contrast,
|
||||||
|
brightness: placement.graphicAdjust.brightness,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply background removal after color adjustments
|
||||||
|
if (placement.graphicAdjust.bgRemoval) {
|
||||||
|
processed = _removeBackground(processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode back to PNG to preserve transparency
|
||||||
|
bytes = Uint8List.fromList(img.encodePng(processed));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If processing fails, use original bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fallback if no bytes available
|
||||||
if (bytes.isEmpty && signatureImageBytes != null) {
|
if (bytes.isEmpty && signatureImageBytes != null) {
|
||||||
bytes = signatureImageBytes;
|
bytes = signatureImageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes.isNotEmpty) {
|
if (bytes.isNotEmpty) {
|
||||||
final imgObj = _getMemoryImage(bytes);
|
pw.MemoryImage? imgObj;
|
||||||
|
try {
|
||||||
|
// Ensure PNG for transparency if not already
|
||||||
|
final asStr = String.fromCharCodes(bytes.take(8));
|
||||||
|
final isPng =
|
||||||
|
bytes.length > 8 &&
|
||||||
|
bytes[0] == 0x89 &&
|
||||||
|
asStr.startsWith('\u0089PNG');
|
||||||
|
if (isPng) {
|
||||||
|
imgObj = pw.MemoryImage(bytes);
|
||||||
|
} else {
|
||||||
|
final decoded = img.decodeImage(bytes);
|
||||||
|
if (decoded != null) {
|
||||||
|
final png = img.encodePng(decoded, level: 6);
|
||||||
|
imgObj = pw.MemoryImage(Uint8List.fromList(png));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
imgObj = null;
|
||||||
|
}
|
||||||
if (imgObj != null) {
|
if (imgObj != null) {
|
||||||
final angle = rot.radians(placement.rotationDeg);
|
|
||||||
final baseBytes = _getBaseBytes(placement);
|
|
||||||
final ar = _getAspectRatioFromBytes(baseBytes);
|
|
||||||
final scaleToFit = rot.scaleToFitForAngle(angle, ar: ar);
|
|
||||||
|
|
||||||
children.add(
|
children.add(
|
||||||
pw.Positioned(
|
pw.Positioned(
|
||||||
left: left,
|
left: left,
|
||||||
|
|
@ -314,12 +306,10 @@ class ExportService {
|
||||||
height: h,
|
height: h,
|
||||||
child: pw.FittedBox(
|
child: pw.FittedBox(
|
||||||
fit: pw.BoxFit.contain,
|
fit: pw.BoxFit.contain,
|
||||||
child: pw.Transform.scale(
|
child: pw.Transform.rotate(
|
||||||
scale: scaleToFit,
|
angle:
|
||||||
child: pw.Transform.rotate(
|
placement.rotationDeg * 3.1415926535 / 180.0,
|
||||||
angle: angle,
|
child: pw.Image(imgObj),
|
||||||
child: pw.Image(imgObj),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class SignatureCard {
|
||||||
});
|
});
|
||||||
|
|
||||||
SignatureCard copyWith({
|
SignatureCard copyWith({
|
||||||
double? rotationDeg, //z axis is out of the screen, positive is CCW
|
double? rotationDeg,
|
||||||
SignatureAsset? asset,
|
SignatureAsset? asset,
|
||||||
GraphicAdjust? graphicAdjust,
|
GraphicAdjust? graphicAdjust,
|
||||||
}) => SignatureCard(
|
}) => SignatureCard(
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ class _DrawCanvasState extends State<DrawCanvas> {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// Export signature to PNG bytes first
|
// Export signature to PNG bytes first
|
||||||
final byteData = await _control.toImage(
|
final byteData = await _control.toImage(
|
||||||
width: 512,
|
width: 1024,
|
||||||
height: 256,
|
height: 512,
|
||||||
fit: true,
|
fit: true,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
background: Colors.transparent,
|
background: Colors.transparent,
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,21 @@ class PdfPageArea extends ConsumerStatefulWidget {
|
||||||
const PdfPageArea({
|
const PdfPageArea({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pageSize,
|
required this.pageSize,
|
||||||
|
required this.onDragSignature,
|
||||||
|
required this.onResizeSignature,
|
||||||
|
required this.onConfirmSignature,
|
||||||
|
required this.onClearActiveOverlay,
|
||||||
|
required this.onSelectPlaced,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Size pageSize;
|
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;
|
final PdfViewerController controller;
|
||||||
@override
|
@override
|
||||||
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
|
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
|
||||||
|
|
@ -145,6 +156,11 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
if (isContinuous) {
|
if (isContinuous) {
|
||||||
return PdfViewerWidget(
|
return PdfViewerWidget(
|
||||||
pageSize: widget.pageSize,
|
pageSize: widget.pageSize,
|
||||||
|
onDragSignature: widget.onDragSignature,
|
||||||
|
onResizeSignature: widget.onResizeSignature,
|
||||||
|
onConfirmSignature: widget.onConfirmSignature,
|
||||||
|
onClearActiveOverlay: widget.onClearActiveOverlay,
|
||||||
|
onSelectPlaced: widget.onSelectPlaced,
|
||||||
pageKeyBuilder: _pageKey,
|
pageKeyBuilder: _pageKey,
|
||||||
scrollToPage: _scrollToPage,
|
scrollToPage: _scrollToPage,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,22 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
return bytes;
|
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 {
|
Future<Uint8List?> _openDrawCanvas() async {
|
||||||
final result = await showModalBottomSheet<Uint8List>(
|
final result = await showModalBottomSheet<Uint8List>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -307,6 +323,11 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
controller: _viewModel.controller,
|
controller: _viewModel.controller,
|
||||||
key: const ValueKey('pdf_page_area'),
|
key: const ValueKey('pdf_page_area'),
|
||||||
pageSize: _pageSize,
|
pageSize: _pageSize,
|
||||||
|
onDragSignature: _onDragSignature,
|
||||||
|
onResizeSignature: _onResizeSignature,
|
||||||
|
onConfirmSignature: _confirmSignature,
|
||||||
|
onClearActiveOverlay: () {},
|
||||||
|
onSelectPlaced: _onSelectPlaced,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,22 @@ class PdfViewerWidget extends ConsumerStatefulWidget {
|
||||||
const PdfViewerWidget({
|
const PdfViewerWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pageSize,
|
required this.pageSize,
|
||||||
|
required this.onDragSignature,
|
||||||
|
required this.onResizeSignature,
|
||||||
|
required this.onConfirmSignature,
|
||||||
|
required this.onClearActiveOverlay,
|
||||||
|
required this.onSelectPlaced,
|
||||||
this.pageKeyBuilder,
|
this.pageKeyBuilder,
|
||||||
this.scrollToPage,
|
this.scrollToPage,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Size pageSize;
|
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 GlobalKey Function(int page)? pageKeyBuilder;
|
||||||
final void Function(int page)? scrollToPage;
|
final void Function(int page)? scrollToPage;
|
||||||
final PdfViewerController controller;
|
final PdfViewerController controller;
|
||||||
|
|
@ -78,6 +88,11 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
widget.pageKeyBuilder ??
|
widget.pageKeyBuilder ??
|
||||||
(page) => GlobalKey(debugLabel: 'page_$page'),
|
(page) => GlobalKey(debugLabel: 'page_$page'),
|
||||||
scrollToPage: widget.scrollToPage ?? (page) {},
|
scrollToPage: widget.scrollToPage ?? (page) {},
|
||||||
|
onDragSignature: widget.onDragSignature,
|
||||||
|
onResizeSignature: widget.onResizeSignature,
|
||||||
|
onConfirmSignature: widget.onConfirmSignature,
|
||||||
|
onClearActiveOverlay: widget.onClearActiveOverlay,
|
||||||
|
onSelectPlaced: widget.onSelectPlaced,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,6 +163,11 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
PdfPageOverlays(
|
PdfPageOverlays(
|
||||||
pageSize: Size(pageRect.width, pageRect.height),
|
pageSize: Size(pageRect.width, pageRect.height),
|
||||||
pageNumber: page.pageNumber,
|
pageNumber: page.pageNumber,
|
||||||
|
onDragSignature: widget.onDragSignature,
|
||||||
|
onResizeSignature: widget.onResizeSignature,
|
||||||
|
onConfirmSignature: widget.onConfirmSignature,
|
||||||
|
onClearActiveOverlay: widget.onClearActiveOverlay,
|
||||||
|
onSelectPlaced: widget.onSelectPlaced,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||||
late bool _bgRemoval;
|
late bool _bgRemoval;
|
||||||
late double _contrast;
|
late double _contrast;
|
||||||
late double _brightness;
|
late double _brightness;
|
||||||
late final ValueNotifier<double> _rotation;
|
late double _rotation;
|
||||||
|
|
||||||
// Cached image data
|
// Cached image data
|
||||||
late Uint8List _originalBytes; // Original asset bytes (never mutated)
|
late Uint8List _originalBytes; // Original asset bytes (never mutated)
|
||||||
|
|
@ -59,7 +59,7 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||||
_bgRemoval = widget.initialGraphicAdjust.bgRemoval;
|
_bgRemoval = widget.initialGraphicAdjust.bgRemoval;
|
||||||
_contrast = widget.initialGraphicAdjust.contrast;
|
_contrast = widget.initialGraphicAdjust.contrast;
|
||||||
_brightness = widget.initialGraphicAdjust.brightness;
|
_brightness = widget.initialGraphicAdjust.brightness;
|
||||||
_rotation = ValueNotifier<double>(widget.initialRotation);
|
_rotation = widget.initialRotation;
|
||||||
_originalBytes = widget.asset.bytes;
|
_originalBytes = widget.asset.bytes;
|
||||||
// Decode lazily only if/when background removal is needed
|
// Decode lazily only if/when background removal is needed
|
||||||
if (_bgRemoval) {
|
if (_bgRemoval) {
|
||||||
|
|
@ -172,7 +172,6 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_rotation.dispose();
|
|
||||||
_bgRemovalDebounce?.cancel();
|
_bgRemovalDebounce?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -207,20 +206,19 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: ValueListenableBuilder<double>(
|
child:
|
||||||
valueListenable: _rotation,
|
_bgRemoval
|
||||||
builder: (context, rot, child) {
|
? RotatedSignatureImage(
|
||||||
final image = RotatedSignatureImage(
|
bytes: _displayBytes,
|
||||||
bytes: _displayBytes,
|
rotationDeg: _rotation,
|
||||||
rotationDeg: rot,
|
)
|
||||||
);
|
: ColorFiltered(
|
||||||
if (_bgRemoval) return image;
|
colorFilter: _currentColorFilter(),
|
||||||
return ColorFiltered(
|
child: RotatedSignatureImage(
|
||||||
colorFilter: _currentColorFilter(),
|
bytes: _displayBytes,
|
||||||
child: image,
|
rotationDeg: _rotation,
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -250,26 +248,16 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||||
children: [
|
children: [
|
||||||
Text(l10n.rotate),
|
Text(l10n.rotate),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ValueListenableBuilder<double>(
|
child: Slider(
|
||||||
valueListenable: _rotation,
|
key: const Key('sld_rotation'),
|
||||||
builder: (context, rot, _) {
|
min: -180,
|
||||||
return Slider(
|
max: 180,
|
||||||
key: const Key('sld_rotation'),
|
divisions: 72,
|
||||||
min: -180,
|
value: _rotation,
|
||||||
max: 180,
|
onChanged: (v) => setState(() => _rotation = v),
|
||||||
divisions: 72,
|
|
||||||
value: rot,
|
|
||||||
onChanged: (v) => _rotation.value = v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValueListenableBuilder<double>(
|
Text('${_rotation.toStringAsFixed(0)}°'),
|
||||||
valueListenable: _rotation,
|
|
||||||
builder:
|
|
||||||
(context, rot, _) =>
|
|
||||||
Text('${rot.toStringAsFixed(0)}°'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -281,7 +269,7 @@ class _ImageEditorDialogState extends State<ImageEditorDialog> {
|
||||||
onPressed:
|
onPressed:
|
||||||
() => Navigator.of(context).pop(
|
() => Navigator.of(context).pop(
|
||||||
ImageEditorResult(
|
ImageEditorResult(
|
||||||
rotation: _rotation.value,
|
rotation: _rotation,
|
||||||
graphicAdjust: domain.GraphicAdjust(
|
graphicAdjust: domain.GraphicAdjust(
|
||||||
contrast: _contrast,
|
contrast: _contrast,
|
||||||
brightness: _brightness,
|
brightness: _brightness,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../../utils/rotation_utils.dart' as rot;
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
/// A lightweight widget to render signature bytes with rotation and an
|
/// A lightweight widget to render signature bytes with rotation and an
|
||||||
/// angle-aware scale-to-fit so the rotated image stays within its bounds.
|
/// 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 {
|
class RotatedSignatureImage extends StatefulWidget {
|
||||||
const RotatedSignatureImage({
|
const RotatedSignatureImage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.bytes,
|
required this.bytes,
|
||||||
this.rotationDeg = 0.0, // counterclockwise as positive
|
this.rotationDeg = 0.0,
|
||||||
this.filterQuality = FilterQuality.low,
|
this.filterQuality = FilterQuality.low,
|
||||||
this.semanticLabel,
|
this.semanticLabel,
|
||||||
this.cacheWidth,
|
|
||||||
this.cacheHeight,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
|
|
@ -24,10 +22,6 @@ class RotatedSignatureImage extends StatefulWidget {
|
||||||
final Alignment alignment = Alignment.center;
|
final Alignment alignment = Alignment.center;
|
||||||
final bool wrapInRepaintBoundary = true;
|
final bool wrapInRepaintBoundary = true;
|
||||||
final String? semanticLabel;
|
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
|
@override
|
||||||
State<RotatedSignatureImage> createState() => _RotatedSignatureImageState();
|
State<RotatedSignatureImage> createState() => _RotatedSignatureImageState();
|
||||||
|
|
@ -51,9 +45,8 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant RotatedSignatureImage oldWidget) {
|
void didUpdateWidget(covariant RotatedSignatureImage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
// Only re-resolve when the bytes change. Rotation does not affect
|
if (!identical(oldWidget.bytes, widget.bytes) ||
|
||||||
// intrinsic aspect ratio, so avoid expensive decode/resolve on slider drags.
|
oldWidget.rotationDeg != widget.rotationDeg) {
|
||||||
if (!identical(oldWidget.bytes, widget.bytes)) {
|
|
||||||
_derivedAspectRatio = null;
|
_derivedAspectRatio = null;
|
||||||
_resolveImage();
|
_resolveImage();
|
||||||
}
|
}
|
||||||
|
|
@ -67,12 +60,25 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
|
|
||||||
void _resolveImage() {
|
void _resolveImage() {
|
||||||
_unlisten();
|
_unlisten();
|
||||||
// Resolve via ImageProvider; when first frame arrives, capture intrinsic size.
|
// Decode synchronously to get aspect ratio
|
||||||
// Avoid synchronous decode on UI thread to keep rotation smooth.
|
// Guard against empty / invalid bytes that some simplified tests may inject.
|
||||||
if (widget.bytes.isEmpty) {
|
if (widget.bytes.isEmpty) {
|
||||||
_setAspectRatio(1.0); // safe fallback
|
_setAspectRatio(1.0); // assume square to avoid layout exceptions
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
final decoded = img.decodePng(widget.bytes);
|
||||||
|
if (decoded != null) {
|
||||||
|
final w = decoded.width;
|
||||||
|
final h = decoded.height;
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
_setAspectRatio(w / h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Swallow decode errors for test-provided dummy data; assume square.
|
||||||
|
_setAspectRatio(1.0);
|
||||||
|
}
|
||||||
final stream = _provider.resolve(createLocalImageConfiguration(context));
|
final stream = _provider.resolve(createLocalImageConfiguration(context));
|
||||||
_stream = stream;
|
_stream = stream;
|
||||||
_listener = ImageStreamListener((ImageInfo info, bool sync) {
|
_listener = ImageStreamListener((ImageInfo info, bool sync) {
|
||||||
|
|
@ -101,7 +107,7 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final angle = rot.ccwRadians(widget.rotationDeg);
|
final angle = widget.rotationDeg * math.pi / 180.0;
|
||||||
Widget img = Image.memory(
|
Widget img = Image.memory(
|
||||||
widget.bytes,
|
widget.bytes,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
|
|
@ -109,10 +115,6 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
filterQuality: widget.filterQuality,
|
filterQuality: widget.filterQuality,
|
||||||
alignment: widget.alignment,
|
alignment: widget.alignment,
|
||||||
semanticLabel: widget.semanticLabel,
|
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) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
// Return a placeholder for invalid images
|
// Return a placeholder for invalid images
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -123,7 +125,16 @@ class _RotatedSignatureImageState extends State<RotatedSignatureImage> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (angle != 0.0) {
|
if (angle != 0.0) {
|
||||||
final scaleToFit = rot.scaleToFitForAngle(angle, ar: _derivedAspectRatio);
|
final double c = math.cos(angle).abs();
|
||||||
|
final double s = math.sin(angle).abs();
|
||||||
|
final ar = _derivedAspectRatio;
|
||||||
|
double scaleToFit;
|
||||||
|
if (ar != null && ar > 0) {
|
||||||
|
scaleToFit = math.min(ar / (ar * c + s), 1.0 / (ar * s + c));
|
||||||
|
} else {
|
||||||
|
// Fallback: square approximation
|
||||||
|
scaleToFit = 1.0 / (c + s).clamp(1.0, double.infinity);
|
||||||
|
}
|
||||||
img = Transform.scale(
|
img = Transform.scale(
|
||||||
scale: scaleToFit,
|
scale: scaleToFit,
|
||||||
child: Transform.rotate(angle: angle, child: img),
|
child: Transform.rotate(angle: angle, child: img),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
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/domain/models/model.dart' as domain;
|
||||||
|
|
@ -8,14 +7,15 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import '../view_model/signature_view_model.dart';
|
import '../view_model/signature_view_model.dart';
|
||||||
import '../view_model/dragging_signature_view_model.dart';
|
import '../view_model/dragging_signature_view_model.dart';
|
||||||
|
|
||||||
class SignatureCardView extends ConsumerStatefulWidget {
|
class SignatureCard extends ConsumerWidget {
|
||||||
const SignatureCardView({
|
const SignatureCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.disabled,
|
required this.disabled,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onAdjust,
|
this.onAdjust,
|
||||||
|
this.useCurrentBytesForDrag = false,
|
||||||
this.rotationDeg = 0.0,
|
this.rotationDeg = 0.0,
|
||||||
this.graphicAdjust = const domain.GraphicAdjust(),
|
this.graphicAdjust = const domain.GraphicAdjust(),
|
||||||
});
|
});
|
||||||
|
|
@ -24,14 +24,9 @@ class SignatureCardView extends ConsumerStatefulWidget {
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback? onAdjust;
|
final VoidCallback? onAdjust;
|
||||||
|
final bool useCurrentBytesForDrag;
|
||||||
final double rotationDeg;
|
final double rotationDeg;
|
||||||
final domain.GraphicAdjust graphicAdjust;
|
final domain.GraphicAdjust graphicAdjust;
|
||||||
@override
|
|
||||||
ConsumerState<SignatureCardView> createState() => _SignatureCardViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
|
||||||
Uint8List? _lastBytesRef;
|
|
||||||
Future<void> _showContextMenu(BuildContext context, Offset position) async {
|
Future<void> _showContextMenu(BuildContext context, Offset position) async {
|
||||||
final selected = await showMenu<String>(
|
final selected = await showMenu<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -55,40 +50,22 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (selected == 'adjust') {
|
if (selected == 'adjust') {
|
||||||
widget.onAdjust?.call();
|
onAdjust?.call();
|
||||||
} else if (selected == 'delete') {
|
} else if (selected == 'delete') {
|
||||||
widget.onDelete();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final displayData = ref
|
final displayData = ref
|
||||||
.watch(signatureViewModelProvider)
|
.watch(signatureViewModelProvider)
|
||||||
.getDisplaySignatureData(widget.asset, widget.graphicAdjust);
|
.getDisplaySignatureData(asset, graphicAdjust);
|
||||||
_maybePrecache(displayData.bytes);
|
|
||||||
// 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;
|
||||||
// Hint decoder with small target size to reduce decode cost.
|
|
||||||
// The card shows inside 96x64 with 6px padding; request ~128px max.
|
|
||||||
Widget coreImage = RotatedSignatureImage(
|
Widget coreImage = RotatedSignatureImage(
|
||||||
bytes: displayData.bytes,
|
bytes: displayData.bytes,
|
||||||
rotationDeg: widget.rotationDeg,
|
rotationDeg: rotationDeg,
|
||||||
// Only set one dimension to keep aspect ratio
|
|
||||||
cacheHeight: 128,
|
|
||||||
);
|
);
|
||||||
Widget img =
|
Widget img =
|
||||||
(displayData.colorMatrix != null)
|
(displayData.colorMatrix != null)
|
||||||
|
|
@ -125,7 +102,7 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
||||||
top: 0,
|
top: 0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.close, size: 16),
|
icon: const Icon(Icons.close, size: 16),
|
||||||
onPressed: widget.disabled ? null : widget.onDelete,
|
onPressed: disabled ? null : onDelete,
|
||||||
tooltip: 'Remove',
|
tooltip: 'Remove',
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
),
|
),
|
||||||
|
|
@ -134,31 +111,33 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Widget child =
|
Widget child = onTap != null ? InkWell(onTap: onTap, child: base) : base;
|
||||||
widget.onTap != null ? InkWell(onTap: widget.onTap, child: base) : base;
|
|
||||||
// Add context menu for adjust/delete on right-click or long-press
|
// Add context menu for adjust/delete on right-click or long-press
|
||||||
child = GestureDetector(
|
child = GestureDetector(
|
||||||
key: const Key('gd_signature_card_area'),
|
key: const Key('gd_signature_card_area'),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onSecondaryTapDown:
|
onSecondaryTapDown:
|
||||||
widget.disabled
|
disabled
|
||||||
? null
|
? null
|
||||||
: (details) => _showContextMenu(context, details.globalPosition),
|
: (details) => _showContextMenu(context, details.globalPosition),
|
||||||
onLongPressStart:
|
onLongPressStart:
|
||||||
widget.disabled
|
disabled
|
||||||
? null
|
? null
|
||||||
: (details) => _showContextMenu(context, details.globalPosition),
|
: (details) => _showContextMenu(context, details.globalPosition),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
if (widget.disabled) return child;
|
if (disabled) return child;
|
||||||
return Draggable<SignatureDragData>(
|
return Draggable<SignatureDragData>(
|
||||||
data: SignatureDragData(
|
data:
|
||||||
card: domain.SignatureCard(
|
useCurrentBytesForDrag
|
||||||
asset: widget.asset,
|
? const SignatureDragData()
|
||||||
rotationDeg: widget.rotationDeg,
|
: SignatureDragData(
|
||||||
graphicAdjust: widget.graphicAdjust,
|
card: domain.SignatureCard(
|
||||||
),
|
asset: asset,
|
||||||
),
|
rotationDeg: rotationDeg,
|
||||||
|
graphicAdjust: graphicAdjust,
|
||||||
|
),
|
||||||
|
),
|
||||||
onDragStarted: () {
|
onDragStarted: () {
|
||||||
ref.read(isDraggingSignatureViewModelProvider.notifier).state = true;
|
ref.read(isDraggingSignatureViewModelProvider.notifier).state = true;
|
||||||
},
|
},
|
||||||
|
|
@ -187,14 +166,12 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
||||||
),
|
),
|
||||||
child: RotatedSignatureImage(
|
child: RotatedSignatureImage(
|
||||||
bytes: displayData.bytes,
|
bytes: displayData.bytes,
|
||||||
rotationDeg: widget.rotationDeg,
|
rotationDeg: rotationDeg,
|
||||||
cacheHeight: 256,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: RotatedSignatureImage(
|
: RotatedSignatureImage(
|
||||||
bytes: displayData.bytes,
|
bytes: displayData.bytes,
|
||||||
rotationDeg: widget.rotationDeg,
|
rotationDeg: rotationDeg,
|
||||||
cacheHeight: 256,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:pdf_signature/domain/models/model.dart';
|
import 'package:pdf_signature/domain/models/model.dart';
|
||||||
|
|
||||||
class SignatureDragData {
|
class SignatureDragData {
|
||||||
final SignatureCard card; // null means use current processed signature
|
final SignatureCard? card; // null means use current processed signature
|
||||||
const SignatureDragData({required this.card});
|
const SignatureDragData({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_asset_repository.dart';
|
||||||
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
||||||
import 'package:pdf_signature/domain/models/signature_asset.dart';
|
import 'package:pdf_signature/domain/models/model.dart' hide SignatureCard;
|
||||||
import 'image_editor_dialog.dart';
|
import 'image_editor_dialog.dart';
|
||||||
import 'signature_card_view.dart';
|
import 'signature_card.dart';
|
||||||
import '../../pdf/view_model/pdf_view_model.dart';
|
import '../../pdf/view_model/pdf_view_model.dart';
|
||||||
|
|
||||||
/// Data for drag-and-drop is in signature_drag_data.dart
|
/// Data for drag-and-drop is in signature_drag_data.dart
|
||||||
|
|
@ -49,7 +49,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: SignatureCardView(
|
child: SignatureCard(
|
||||||
key: ValueKey('sig_card_${library.indexOf(card)}'),
|
key: ValueKey('sig_card_${library.indexOf(card)}'),
|
||||||
asset: card.asset,
|
asset: card.asset,
|
||||||
rotationDeg: card.rotationDeg,
|
rotationDeg: card.rotationDeg,
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Convert degrees to radians with counterclockwise as positive in screen space
|
|
||||||
/// by inverting the sign (because screen Y axis points downwards).
|
|
||||||
double ccwRadians(double degrees) => -degrees * math.pi / 180.0;
|
|
||||||
|
|
||||||
/// Classic math convention: positive degrees rotate counterclockwise.
|
|
||||||
/// No screen-space Y-inversion applied.
|
|
||||||
double radians(double degrees) => degrees * math.pi / 180.0;
|
|
||||||
|
|
||||||
/// Compute scale factor to keep a rotated rectangle of aspect ratio [ar]
|
|
||||||
/// within a unit 1x1 box. If [ar] is null or <= 0, fall back to a square.
|
|
||||||
/// Returns the scale to apply before rotation.
|
|
||||||
double scaleToFitForAngle(double angleRad, {double? ar}) {
|
|
||||||
final double c = angleRad == 0.0 ? 1.0 : math.cos(angleRad).abs();
|
|
||||||
final double s = angleRad == 0.0 ? 0.0 : math.sin(angleRad).abs();
|
|
||||||
if (ar != null && ar > 0) {
|
|
||||||
return math.min(ar / (ar * c + s), 1.0 / (ar * s + c));
|
|
||||||
}
|
|
||||||
return 1.0 / (c + s).clamp(1.0, double.infinity);
|
|
||||||
}
|
|
||||||
|
|
@ -42,6 +42,11 @@ void main() {
|
||||||
height: 520,
|
height: 520,
|
||||||
child: PdfPageArea(
|
child: PdfPageArea(
|
||||||
pageSize: Size(676, 400),
|
pageSize: Size(676, 400),
|
||||||
|
onDragSignature: _noopOffset,
|
||||||
|
onResizeSignature: _noopOffset,
|
||||||
|
onConfirmSignature: _noop,
|
||||||
|
onClearActiveOverlay: _noop,
|
||||||
|
onSelectPlaced: _noopInt,
|
||||||
controller: PdfViewerController(),
|
controller: PdfViewerController(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -70,4 +75,6 @@ void main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// No extra callbacks required in the new API
|
void _noop() {}
|
||||||
|
void _noopInt(int? _) {}
|
||||||
|
void _noopOffset(Offset _) {}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ void main() {
|
||||||
height: 520,
|
height: 520,
|
||||||
child: PdfPageArea(
|
child: PdfPageArea(
|
||||||
pageSize: Size(676, 400),
|
pageSize: Size(676, 400),
|
||||||
|
onDragSignature: _noopOffset,
|
||||||
|
onResizeSignature: _noopOffset,
|
||||||
|
onConfirmSignature: _noop,
|
||||||
|
onClearActiveOverlay: _noop,
|
||||||
|
onSelectPlaced: _noopInt,
|
||||||
controller: PdfViewerController(),
|
controller: PdfViewerController(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -101,4 +106,6 @@ void main() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No extra callbacks required in the new API
|
void _noop() {}
|
||||||
|
void _noopInt(int? _) {}
|
||||||
|
void _noopOffset(Offset _) {}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ void main() {
|
||||||
height: 520,
|
height: 520,
|
||||||
child: PdfPageArea(
|
child: PdfPageArea(
|
||||||
pageSize: const Size(676, 400),
|
pageSize: const Size(676, 400),
|
||||||
|
onDragSignature: _noopOffset,
|
||||||
|
onResizeSignature: _noopOffset,
|
||||||
|
onConfirmSignature: _noop,
|
||||||
|
onClearActiveOverlay: _noop,
|
||||||
|
onSelectPlaced: _noopInt,
|
||||||
controller: PdfViewerController(),
|
controller: PdfViewerController(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -86,6 +91,11 @@ void main() {
|
||||||
// Keep aspect ratio consistent with uiPageSize
|
// Keep aspect ratio consistent with uiPageSize
|
||||||
child: PdfPageArea(
|
child: PdfPageArea(
|
||||||
pageSize: uiPageSize,
|
pageSize: uiPageSize,
|
||||||
|
onDragSignature: _noopOffset,
|
||||||
|
onResizeSignature: _noopOffset,
|
||||||
|
onConfirmSignature: _noop,
|
||||||
|
onClearActiveOverlay: _noop,
|
||||||
|
onSelectPlaced: _noopInt,
|
||||||
controller: PdfViewerController(),
|
controller: PdfViewerController(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -160,4 +170,6 @@ void main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// No extra callbacks required in the new API
|
void _noop() {}
|
||||||
|
void _noopInt(int? _) {}
|
||||||
|
void _noopOffset(Offset _) {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue