@@ -496,6 +496,8 @@ async function refreshDeviceFiles() {
496496 refreshDeviceFiles ( ) ;
497497 } else if ( _isImageFile ( entry . name ) ) {
498498 previewDeviceImage ( item . dataset . path , entry . name ) ;
499+ } else if ( _isLvglBinFile ( entry . name ) ) {
500+ previewDeviceImage ( item . dataset . path , entry . name , true ) ;
499501 } else if ( _isTextFile ( entry . name ) ) {
500502 openDeviceTextFile ( item . dataset . path , entry . name ) ;
501503 }
@@ -776,6 +778,55 @@ function _isImageFile(name) {
776778 return _IMAGE_EXTENSIONS . some ( ( ext ) => lower . endsWith ( ext ) ) ;
777779}
778780
781+ /** LVGL binary image extensions */
782+ const _LVGL_BIN_EXTENSIONS = [ '.bin' ] ;
783+
784+ /**
785+ * Check if a filename is an LVGL binary image file.
786+ */
787+ function _isLvglBinFile ( name ) {
788+ const lower = name . toLowerCase ( ) ;
789+ return _LVGL_BIN_EXTENSIONS . some ( ( ext ) => lower . endsWith ( ext ) ) ;
790+ }
791+
792+ /**
793+ * Convert LVGL binary data to PNG via backend icu tool.
794+ * @param {Blob } blob - LVGL binary data
795+ * @returns {Promise<{success: boolean, blob?: Blob, error?: string}> }
796+ */
797+ async function convertLvglToPng ( blob ) {
798+ try {
799+ const arrayBuffer = await blob . arrayBuffer ( ) ;
800+ const bytes = new Uint8Array ( arrayBuffer ) ;
801+ let binary = '' ;
802+ for ( let i = 0 ; i < bytes . length ; i ++ ) {
803+ binary += String . fromCharCode ( bytes [ i ] ) ;
804+ }
805+ const b64 = btoa ( binary ) ;
806+
807+ const res = await fetch ( '/api/convert/lvgl-to-png' , {
808+ method : 'POST' ,
809+ headers : { 'Content-Type' : 'application/json' } ,
810+ body : JSON . stringify ( { data : b64 } ) ,
811+ } ) ;
812+ const data = await res . json ( ) ;
813+
814+ if ( ! data . success ) {
815+ return { success : false , error : data . error || 'Conversion failed' } ;
816+ }
817+
818+ const pngBinary = atob ( data . data ) ;
819+ const pngBytes = new Uint8Array ( pngBinary . length ) ;
820+ for ( let i = 0 ; i < pngBinary . length ; i ++ ) {
821+ pngBytes [ i ] = pngBinary . charCodeAt ( i ) ;
822+ }
823+ const pngBlob = new Blob ( [ pngBytes ] , { type : 'image/png' } ) ;
824+ return { success : true , blob : pngBlob } ;
825+ } catch ( e ) {
826+ return { success : false , error : e . message || String ( e ) } ;
827+ }
828+ }
829+
779830/* ===========================
780831 TEXT FILE SUPPORT
781832 =========================== */
@@ -1335,8 +1386,11 @@ async function refreshPreviewTab(tabId) {
13351386/**
13361387 * Preview an image file from the device in an editor tab.
13371388 * Downloads the file, creates a blob URL, and displays it in a new tab.
1389+ * @param {string } remotePath - File path on device
1390+ * @param {string } fileName - File name for display
1391+ * @param {boolean } isLvgl - If true, convert LVGL binary to PNG before display
13381392 */
1339- async function previewDeviceImage ( remotePath , fileName ) {
1393+ async function previewDeviceImage ( remotePath , fileName , isLvgl = false ) {
13401394 const state = window . FPBState ;
13411395 if ( ! state . isConnected ) {
13421396 log . error ( 'Not connected' ) ;
@@ -1376,20 +1430,48 @@ async function previewDeviceImage(remotePath, fileName) {
13761430 return ;
13771431 }
13781432
1379- // Create blob URL from downloaded data
1380- const ext = fileName . split ( '.' ) . pop ( ) . toLowerCase ( ) ;
1381- const mimeMap = {
1382- png : 'image/png' ,
1383- jpg : 'image/jpeg' ,
1384- jpeg : 'image/jpeg' ,
1385- gif : 'image/gif' ,
1386- bmp : 'image/bmp' ,
1387- svg : 'image/svg+xml' ,
1388- webp : 'image/webp' ,
1389- ico : 'image/x-icon' ,
1390- } ;
1391- const mime = mimeMap [ ext ] || 'image/png' ;
1392- const blob = result . blob || new Blob ( [ result . data ] , { type : mime } ) ;
1433+ let blob ;
1434+ if ( isLvgl ) {
1435+ // Convert LVGL binary to PNG via backend
1436+ const rawBlob = result . blob || new Blob ( [ result . data ] ) ;
1437+ log . info ( 'Converting LVGL image...' ) ;
1438+ const convResult = await convertLvglToPng ( rawBlob ) ;
1439+ if ( ! convResult . success ) {
1440+ if (
1441+ convResult . error &&
1442+ convResult . error . includes ( 'icu tool not installed' )
1443+ ) {
1444+ const install = confirm (
1445+ t (
1446+ 'transfer.icu_not_installed' ,
1447+ 'The "icu" tool is required to preview LVGL images but is not installed.\nOpen the installation page?' ,
1448+ ) ,
1449+ ) ;
1450+ if ( install ) {
1451+ window . open ( 'https://i.to01.icu' , '_blank' ) ;
1452+ }
1453+ } else {
1454+ log . error ( `LVGL convert failed: ${ convResult . error } ` ) ;
1455+ }
1456+ return ;
1457+ }
1458+ blob = convResult . blob ;
1459+ } else {
1460+ // Standard image
1461+ const ext = fileName . split ( '.' ) . pop ( ) . toLowerCase ( ) ;
1462+ const mimeMap = {
1463+ png : 'image/png' ,
1464+ jpg : 'image/jpeg' ,
1465+ jpeg : 'image/jpeg' ,
1466+ gif : 'image/gif' ,
1467+ bmp : 'image/bmp' ,
1468+ svg : 'image/svg+xml' ,
1469+ webp : 'image/webp' ,
1470+ ico : 'image/x-icon' ,
1471+ } ;
1472+ const mime = mimeMap [ ext ] || 'image/png' ;
1473+ blob = result . blob || new Blob ( [ result . data ] , { type : mime } ) ;
1474+ }
13931475 const blobUrl = URL . createObjectURL ( blob ) ;
13941476
13951477 // Create editor tab
@@ -2336,6 +2418,8 @@ window.hideTransferContextMenu = hideTransferContextMenu;
23362418window . transferContextAction = transferContextAction ;
23372419window . previewDeviceImage = previewDeviceImage ;
23382420window . _isImageFile = _isImageFile ;
2421+ window . _isLvglBinFile = _isLvglBinFile ;
2422+ window . convertLvglToPng = convertLvglToPng ;
23392423window . _isTextFile = _isTextFile ;
23402424window . _getAceMode = _getAceMode ;
23412425window . openDeviceTextFile = openDeviceTextFile ;
0 commit comments