lens.lua (12357B)
1 -- Lens - LuajitOS Image Viewer 2 -- Displays BMP, PNG, and JPEG images with a toolbar 3 4 if osprint then 5 osprint("Lens: Starting...\n") 6 end 7 8 -- Check if required functions are available 9 if not CRamdiskOpen or not BMPLoad or not ImageDraw then 10 if osprint then 11 osprint("Lens: ERROR - Required image functions not available\n") 12 end 13 return 14 end 15 16 -- Constants 17 local TOOLBAR_HEIGHT = 30 18 local BUTTON_HEIGHT = 24 19 local BUTTON_WIDTH = 80 20 local BUTTON_MARGIN = 3 21 22 -- State variables (stored in window for proper scope) 23 local state = { 24 currentImage = nil, 25 currentImageInfo = nil, 26 currentImagePath = "" 27 } 28 29 -- Create main window 30 local screenWidth = (sys and sys.screen and sys.screen[1] and sys.screen[1].width) or 1024 31 local screenHeight = (sys and sys.screen and sys.screen[1] and sys.screen[1].height) or 768 32 local windowWidth = 640 33 local windowHeight = 480 34 35 local window = app:newWindow( 36 math.floor((screenWidth - windowWidth) / 2), 37 math.floor((screenHeight - windowHeight) / 2), 38 windowWidth, 39 windowHeight 40 ) 41 window.title = "Lens Viewer" 42 window.resizable = true 43 44 -- Helper: Load image from file path 45 local function loadImage(path) 46 if osprint then 47 osprint("Lens: Loading image from: " .. path .. "\n") 48 end 49 50 -- Clean up previous image 51 if state.currentImage and ImageDestroy then 52 ImageDestroy(state.currentImage) 53 state.currentImage = nil 54 end 55 56 -- Read file using SafeFS (fs) if available, otherwise CRamdisk 57 local imageData = nil 58 59 -- Try SafeFS first - it handles both ramdisk and diskfs paths 60 if fs and fs.read then 61 if osprint then 62 osprint("Lens: Using SafeFS for path: " .. path .. "\n") 63 end 64 local content, err = fs:read(path) 65 if content then 66 imageData = content 67 elseif osprint then 68 osprint("Lens: SafeFS read failed: " .. tostring(err) .. "\n") 69 end 70 end 71 72 -- Fall back to CRamdisk if SafeFS didn't work 73 if not imageData and CRamdiskOpen then 74 local handle, err = CRamdiskOpen(path, "r") 75 if not handle then 76 if osprint then 77 osprint("Lens: ERROR - Failed to open file: " .. tostring(err) .. "\n") 78 end 79 return false, "Failed to open file: " .. tostring(err) 80 end 81 imageData = CRamdiskRead(handle) 82 CRamdiskClose(handle) 83 end 84 85 if not imageData then 86 if osprint then 87 osprint("Lens: ERROR - Failed to read file data\n") 88 end 89 return false, "Failed to read file" 90 end 91 92 if osprint then 93 osprint("Lens: Read " .. #imageData .. " bytes\n") 94 95 -- Show first 16 bytes as hex to identify file type 96 local header = "" 97 for i = 1, math.min(16, #imageData) do 98 header = header .. string.format("%02X ", string.byte(imageData, i)) 99 end 100 osprint("Lens: File header: " .. header .. "\n") 101 102 -- Check for PNG magic number (89 50 4E 47) = "\x89PNG" 103 if #imageData >= 4 then 104 local b1, b2, b3, b4 = string.byte(imageData, 1, 4) 105 if b1 == 0x89 and b2 == 0x50 and b3 == 0x4E and b4 == 0x47 then 106 osprint("Lens: File appears to be PNG format (valid magic number)\n") 107 elseif b1 == 0x42 and b2 == 0x4D then 108 osprint("Lens: File appears to be BMP format\n") 109 else 110 osprint("Lens: Unknown file format (invalid magic number)\n") 111 end 112 end 113 end 114 115 -- Check what decoders are available 116 if osprint then 117 osprint("Lens: PNGLoad available: " .. tostring(PNGLoad ~= nil) .. "\n") 118 osprint("Lens: BMPLoad available: " .. tostring(BMPLoad ~= nil) .. "\n") 119 osprint("Lens: JPEGLoad available: " .. tostring(JPEGLoad ~= nil) .. "\n") 120 end 121 122 -- Detect file type from magic bytes 123 local fileType = nil 124 if #imageData >= 4 then 125 local b1, b2, b3, b4 = string.byte(imageData, 1, 4) 126 if b1 == 0x89 and b2 == 0x50 and b3 == 0x4E and b4 == 0x47 then 127 fileType = "PNG" 128 elseif b1 == 0x42 and b2 == 0x4D then 129 fileType = "BMP" 130 elseif b1 == 0xFF and b2 == 0xD8 then 131 fileType = "JPEG" 132 end 133 end 134 135 if osprint then 136 osprint("Lens: Detected file type: " .. tostring(fileType) .. "\n") 137 end 138 139 -- Try to decode based on detected type first, then try others 140 local image = nil 141 local decoder_used = nil 142 143 -- Try detected type first 144 if fileType == "PNG" and PNGLoad then 145 if osprint then 146 osprint("Lens: Trying PNG decoder...\n") 147 end 148 local result, err = PNGLoad(imageData) 149 if result then 150 image = result 151 decoder_used = "PNG" 152 if osprint then 153 osprint("Lens: Successfully decoded as PNG\n") 154 end 155 elseif osprint then 156 osprint("Lens: PNG decoder failed: " .. tostring(err) .. "\n") 157 end 158 elseif fileType == "BMP" and BMPLoad then 159 if osprint then 160 osprint("Lens: Trying BMP decoder...\n") 161 end 162 local result, err = BMPLoad(imageData) 163 if result then 164 image = result 165 decoder_used = "BMP" 166 if osprint then 167 osprint("Lens: Successfully decoded as BMP\n") 168 end 169 elseif osprint then 170 osprint("Lens: BMP decoder failed: " .. tostring(err) .. "\n") 171 end 172 elseif fileType == "JPEG" and JPEGLoad then 173 if osprint then 174 osprint("Lens: Trying JPEG decoder...\n") 175 end 176 local result, err = JPEGLoad(imageData) 177 if result then 178 image = result 179 decoder_used = "JPEG" 180 if osprint then 181 osprint("Lens: Successfully decoded as JPEG\n") 182 end 183 elseif osprint then 184 osprint("Lens: JPEG decoder failed: " .. tostring(err) .. "\n") 185 end 186 end 187 188 -- If detected type failed or unknown, try all decoders 189 if not image and PNGLoad then 190 local result, err = PNGLoad(imageData) 191 if result then 192 image = result 193 decoder_used = "PNG" 194 end 195 end 196 197 if not image and JPEGLoad then 198 local result, err = JPEGLoad(imageData) 199 if result then 200 image = result 201 decoder_used = "JPEG" 202 end 203 end 204 205 if not image and BMPLoad then 206 local result, err = BMPLoad(imageData) 207 if result then 208 image = result 209 decoder_used = "BMP" 210 end 211 end 212 213 if not image then 214 if osprint then 215 osprint("Lens: ERROR - Failed to decode image with any decoder\n") 216 end 217 return false, "Failed to decode image (not a valid BMP, PNG, or JPEG)" 218 end 219 220 -- Get image info 221 local info = nil 222 if ImageGetInfo then 223 info = ImageGetInfo(image) 224 if osprint and info then 225 osprint("Lens: Dimensions: " .. info.width .. "x" .. info.height .. "\n") 226 end 227 end 228 229 state.currentImage = image 230 state.currentImageInfo = info 231 state.currentImagePath = path 232 233 window:markDirty() 234 return true 235 end 236 237 -- Helper: Show open file dialog 238 local function openFileDialog() 239 if osprint then 240 osprint("Lens: Opening file dialog...\n") 241 end 242 243 if not Dialog then 244 if osprint then 245 osprint("Lens: ERROR - Dialog library not available\n") 246 end 247 return 248 end 249 250 local dialog = Dialog.fileOpen("/", { 251 app = app, 252 fs = fs, -- Use sandbox's fs (SafeFS instance) 253 title = "Open Image", 254 width = 500, 255 height = 400 256 }) 257 258 dialog:openDialog(function(path) 259 if path then 260 if osprint then 261 osprint("Lens: User selected: " .. path .. "\n") 262 end 263 local success, err = loadImage(path) 264 if success then 265 if osprint then 266 osprint("Lens: Image loaded successfully\n") 267 osprint("Lens: state.currentImage = " .. tostring(state.currentImage) .. "\n") 268 osprint("Lens: state.currentImageInfo = " .. tostring(state.currentImageInfo) .. "\n") 269 if state.currentImageInfo then 270 osprint("Lens: Image dimensions: " .. state.currentImageInfo.width .. "x" .. state.currentImageInfo.height .. "\n") 271 end 272 end 273 -- Explicitly mark window dirty to force redraw 274 window:markDirty() 275 else 276 if osprint then 277 osprint("Lens: Failed to load: " .. tostring(err) .. "\n") 278 end 279 end 280 else 281 if osprint then 282 osprint("Lens: User cancelled dialog\n") 283 end 284 end 285 end) 286 end 287 288 -- Draw callback 289 window.onDraw = function(gfx) 290 local winW = window:getWidth() 291 local winH = window:getHeight() 292 293 -- Draw toolbar background 294 gfx:fillRect(0, 0, winW, TOOLBAR_HEIGHT, 0x3C3C3C) 295 296 -- Draw "Open" button 297 local btnX = BUTTON_MARGIN 298 local btnY = BUTTON_MARGIN 299 gfx:fillRect(btnX, btnY, BUTTON_WIDTH, BUTTON_HEIGHT, 0x0066CC) 300 gfx:drawRect(btnX, btnY, BUTTON_WIDTH, BUTTON_HEIGHT, 0x0088EE) 301 gfx:drawText(btnX + 24, btnY + 7, "Open", 0xFFFFFF) 302 303 -- Draw separator line 304 gfx:fillRect(0, TOOLBAR_HEIGHT, winW, 1, 0x555555) 305 306 -- Image area 307 local imageAreaY = TOOLBAR_HEIGHT + 1 308 local imageAreaH = winH - imageAreaY 309 310 -- Fill background with dark gray 311 gfx:fillRect(0, imageAreaY, winW, imageAreaH, 0x2C2C2C) 312 313 if state.currentImage and state.currentImageInfo then 314 -- Calculate scaled dimensions to fit while maintaining aspect ratio 315 local imgW = state.currentImageInfo.width 316 local imgH = state.currentImageInfo.height 317 318 local scaleX = winW / imgW 319 local scaleY = imageAreaH / imgH 320 local scale = math.min(scaleX, scaleY) 321 322 -- Don't scale up, only scale down 323 if scale > 1.0 then 324 scale = 1.0 325 end 326 327 local scaledW = math.floor(imgW * scale) 328 local scaledH = math.floor(imgH * scale) 329 330 -- Center the image 331 local x = math.floor((winW - scaledW) / 2) 332 local y = imageAreaY + math.floor((imageAreaH - scaledH) / 2) 333 334 -- Draw the image using gfx:drawImage (window-relative coordinates) 335 if gfx.drawImage then 336 gfx:drawImage(state.currentImage, x, y, scaledW, scaledH) 337 end 338 339 -- Draw image info 340 local filename = state.currentImagePath:match("([^/]+)$") or state.currentImagePath 341 local infoText = filename .. " (" .. imgW .. "x" .. imgH .. ")" 342 gfx:drawText(5, winH - 15, infoText, 0xAAAAAA) 343 else 344 -- No image loaded 345 local msg1 = "No image loaded" 346 local msg2 = "Click 'Open' to select an image" 347 gfx:drawText(winW / 2 - 60, winH / 2 - 10, msg1, 0x888888) 348 gfx:drawText(winW / 2 - 100, winH / 2 + 10, msg2, 0x888888) 349 end 350 end 351 352 -- Click handler 353 window.onClick = function(mx, my) 354 -- Check if click is on "Open" button 355 local btnX = BUTTON_MARGIN 356 local btnY = BUTTON_MARGIN 357 358 if mx >= btnX and mx < btnX + BUTTON_WIDTH and 359 my >= btnY and my < btnY + BUTTON_HEIGHT then 360 openFileDialog() 361 return 362 end 363 end 364 365 -- Try to load image from command line argument or default 366 local default_image = "/os/public/res/background.bmp" 367 if args and #args > 0 and args[1] then 368 local success, err = loadImage(args[1]) 369 if success then 370 if osprint then 371 osprint("Lens: Loaded initial image: " .. args[1] .. "\n") 372 end 373 else 374 if osprint then 375 osprint("Lens: Failed to load initial image: " .. tostring(err) .. "\n") 376 end 377 end 378 else 379 -- Load default image 380 if osprint then 381 osprint("Lens: No initial image specified, loading default: " .. default_image .. "\n") 382 end 383 local success, err = loadImage(default_image) 384 if success then 385 if osprint then 386 osprint("Lens: Loaded default image successfully\n") 387 end 388 else 389 if osprint then 390 osprint("Lens: Failed to load default image: " .. tostring(err) .. "\n") 391 end 392 end 393 end 394 395 window:markDirty()