import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:syncfusion_pdfviewer_platform_interface/pdfviewer_platform_interface.dart'; /// Service for generating, caching, and retrieving page thumbnails. class ThumbnailService { static Future _thumbnailFile(String documentId, int pageIndex) async { final appDir = await getApplicationDocumentsDirectory(); final dir = Directory('${appDir.path}/thumbnails/$documentId'); if (!await dir.exists()) await dir.create(recursive: true); return File('${dir.path}/$pageIndex.png'); } static Future _rgbaToPng( Uint8List rgba, int width, int height, ) async { final completer = Completer(); ui.decodeImageFromPixels( rgba, width, height, ui.PixelFormat.bgra8888, completer.complete, rowBytes: width * 4, ); final image = await completer.future; final byteData = await image.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } /// Render a single PDF page to PNG bytes at [maxWidth] pixel width. /// Returns null on failure. static Future generate( String filePath, int pageIndex, { int maxWidth = 160, }) async { // Stable, low-collision renderer handle key for this file. Plain // `filePath.hashCode` can collide between different paths; combining it // with the path length and basename (no extra deps beyond `path`) // drastically reduces the chance two distinct files share a handle. final documentId = 'thumb-${filePath.hashCode}-${filePath.length}-${p.basename(filePath)}'; try { final bytes = await File(filePath).readAsBytes(); final pageCountStr = await PdfViewerPlatform.instance .initializePdfRenderer(bytes, documentId); if (pageCountStr == null) return null; final pageCount = int.tryParse(pageCountStr); if (pageCount == null || pageIndex < 0 || pageIndex >= pageCount) { await PdfViewerPlatform.instance.closeDocument(documentId); return null; } final pagesHeight = await PdfViewerPlatform.instance.getPagesHeight( documentId, ); final pagesWidth = await PdfViewerPlatform.instance.getPagesWidth( documentId, ); if (pagesHeight == null || pagesWidth == null) { await PdfViewerPlatform.instance.closeDocument(documentId); return null; } final pageHeight = pagesHeight[pageIndex] as double; final pageWidth = pagesWidth[pageIndex] as double; final thumbnailHeight = (maxWidth * pageHeight / pageWidth).round(); final rgba = await PdfViewerPlatform.instance.getPage( pageIndex + 1, maxWidth, thumbnailHeight, documentId, ); await PdfViewerPlatform.instance.closeDocument(documentId); if (rgba == null) return null; return _rgbaToPng(rgba, maxWidth, thumbnailHeight); } catch (_) { try { await PdfViewerPlatform.instance.closeDocument(documentId); } catch (_) {} return null; } } /// Persist thumbnail bytes to disk and return the file. static Future cacheThumbnail( String documentId, int pageIndex, Uint8List data, ) async { final file = await _thumbnailFile(documentId, pageIndex); await file.writeAsBytes(data); return file; } /// Whether a cached thumbnail exists on disk. static Future hasCached(String documentId, int pageIndex) async { return (await _thumbnailFile(documentId, pageIndex)).exists(); } /// Return the cached file if it exists, otherwise null. static Future getCached(String documentId, int pageIndex) async { final file = await _thumbnailFile(documentId, pageIndex); return (await file.exists()) ? file : null; } /// Delete all cached thumbnails for [documentId]. static Future invalidateAll(String documentId) async { final appDir = await getApplicationDocumentsDirectory(); final dir = Directory('${appDir.path}/thumbnails/$documentId'); if (await dir.exists()) await dir.delete(recursive: true); } /// Invalidate a single page thumbnail. static Future invalidatePage(String documentId, int pageIndex) async { final file = await _thumbnailFile(documentId, pageIndex); if (await file.exists()) await file.delete(); } }