PNG.lua (12006B)
1 -- PNG.lua - PNG Image Creation and Manipulation Library 2 -- Requires: "imaging" permission 3 4 local PNG = {} 5 6 -- Check for imaging permission 7 local function checkPermission() 8 if not ADMIN_HasPermission then 9 error("PNG library requires permission system, but ADMIN_HasPermission is not available") 10 end 11 12 if not ADMIN_HasPermission("imaging") then 13 error("Permission denied: 'imaging' permission required to use PNG library") 14 end 15 end 16 17 -- PNG image object 18 local PNGImage = {} 19 PNGImage.__index = PNGImage 20 PNGImage.__metatable = false -- Prevent metatable access/modification 21 22 -- Create a new PNG image 23 -- @param width Image width in pixels 24 -- @param height Image height in pixels 25 -- @param hasAlpha Optional, default true - whether image has alpha channel 26 -- @return PNG image object 27 function PNG.new(width, height, hasAlpha) 28 checkPermission() 29 30 if not width or not height then 31 error("PNG.new requires width and height") 32 end 33 34 if width <= 0 or width > 4096 then 35 error("PNG width must be between 1 and 4096") 36 end 37 38 if height <= 0 or height > 4096 then 39 error("PNG height must be between 1 and 4096") 40 end 41 42 hasAlpha = (hasAlpha == nil) and true or hasAlpha 43 44 local self = setmetatable({}, PNGImage) 45 46 -- Create image buffer using C API 47 -- We'll create a simple RGBA buffer in memory 48 self.width = width 49 self.height = height 50 self.hasAlpha = hasAlpha 51 self.bpp = hasAlpha and 32 or 24 52 53 -- Allocate pixel data (RGBA format) 54 -- Each pixel is 4 bytes (R, G, B, A) 55 local pixelCount = width * height 56 self.pixels = {} 57 58 -- Initialize to transparent black (or opaque black if no alpha) 59 local defaultAlpha = hasAlpha and 0 or 255 60 for i = 1, pixelCount do 61 self.pixels[i] = { 62 r = 0, 63 g = 0, 64 b = 0, 65 a = defaultAlpha 66 } 67 end 68 69 -- Create C image buffer if available 70 if ImageCreate then 71 self.imageBuffer = ImageCreate(width, height, hasAlpha) 72 end 73 74 return self 75 end 76 77 -- Write a pixel to the image 78 -- @param x X coordinate (0-based) 79 -- @param y Y coordinate (0-based) 80 -- @param color Color in hex format "RRGGBB" or "RRGGBBAA" 81 function PNGImage:writePixel(x, y, color) 82 checkPermission() 83 84 if not x or not y or not color then 85 error("writePixel requires x, y, and color") 86 end 87 88 if x < 0 or x >= self.width or y < 0 or y >= self.height then 89 error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") 90 end 91 92 -- Parse color string 93 local r, g, b, a 94 95 if type(color) == "string" then 96 -- Remove # if present 97 color = color:gsub("^#", "") 98 99 if #color == 6 then 100 -- RRGGBB format 101 r = tonumber(color:sub(1, 2), 16) 102 g = tonumber(color:sub(3, 4), 16) 103 b = tonumber(color:sub(5, 6), 16) 104 a = 255 105 elseif #color == 8 then 106 -- RRGGBBAA format 107 r = tonumber(color:sub(1, 2), 16) 108 g = tonumber(color:sub(3, 4), 16) 109 b = tonumber(color:sub(5, 6), 16) 110 a = tonumber(color:sub(7, 8), 16) 111 else 112 error("Invalid color format. Use 'RRGGBB' or 'RRGGBBAA'") 113 end 114 elseif type(color) == "number" then 115 -- Treat as 0xRRGGBB or 0xRRGGBBAA 116 if color <= 0xFFFFFF then 117 -- RGB format 118 r = bit.rshift(bit.band(color, 0xFF0000), 16) 119 g = bit.rshift(bit.band(color, 0x00FF00), 8) 120 b = bit.band(color, 0x0000FF) 121 a = 255 122 else 123 -- RGBA format 124 r = bit.rshift(bit.band(color, 0xFF000000), 24) 125 g = bit.rshift(bit.band(color, 0x00FF0000), 16) 126 b = bit.rshift(bit.band(color, 0x0000FF00), 8) 127 a = bit.band(color, 0x000000FF) 128 end 129 else 130 error("Color must be a string (hex) or number") 131 end 132 133 if not r or not g or not b then 134 error("Failed to parse color: " .. tostring(color)) 135 end 136 137 -- Calculate pixel index (row-major order) 138 local index = y * self.width + x + 1 139 140 -- Store pixel 141 self.pixels[index] = { 142 r = r, 143 g = g, 144 b = b, 145 a = a or 255 146 } 147 148 -- Update C image buffer if available 149 if self.imageBuffer and ImageSetPixel then 150 ImageSetPixel(self.imageBuffer, x, y, r, g, b, a or 255) 151 end 152 end 153 154 -- Read a pixel from the image 155 -- @param x X coordinate (0-based) 156 -- @param y Y coordinate (0-based) 157 -- @return Color string in "RRGGBB" or "RRGGBBAA" format 158 function PNGImage:readPixel(x, y) 159 checkPermission() 160 161 if not x or not y then 162 error("readPixel requires x and y coordinates") 163 end 164 165 if x < 0 or x >= self.width or y < 0 or y >= self.height then 166 error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") 167 end 168 169 -- Calculate pixel index 170 local index = y * self.width + x + 1 171 172 local pixel = self.pixels[index] 173 174 if not pixel then 175 return "00000000" 176 end 177 178 -- Format as hex string 179 local r = string.format("%02X", pixel.r) 180 local g = string.format("%02X", pixel.g) 181 local b = string.format("%02X", pixel.b) 182 183 if self.hasAlpha then 184 local a = string.format("%02X", pixel.a) 185 return r .. g .. b .. a 186 else 187 return r .. g .. b 188 end 189 end 190 191 -- Get pixel as RGBA table 192 -- @param x X coordinate (0-based) 193 -- @param y Y coordinate (0-based) 194 -- @return Table with r, g, b, a fields (0-255) 195 function PNGImage:getPixel(x, y) 196 checkPermission() 197 198 if not x or not y then 199 error("getPixel requires x and y coordinates") 200 end 201 202 if x < 0 or x >= self.width or y < 0 or y >= self.height then 203 error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") 204 end 205 206 local index = y * self.width + x + 1 207 local pixel = self.pixels[index] 208 209 if not pixel then 210 return { r = 0, g = 0, b = 0, a = 0 } 211 end 212 213 return { 214 r = pixel.r, 215 g = pixel.g, 216 b = pixel.b, 217 a = pixel.a 218 } 219 end 220 221 -- Set pixel from RGBA table 222 -- @param x X coordinate (0-based) 223 -- @param y Y coordinate (0-based) 224 -- @param rgba Table with r, g, b, a fields 225 function PNGImage:setPixel(x, y, rgba) 226 checkPermission() 227 228 if not x or not y or not rgba then 229 error("setPixel requires x, y, and rgba table") 230 end 231 232 if x < 0 or x >= self.width or y < 0 or y >= self.height then 233 error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") 234 end 235 236 local index = y * self.width + x + 1 237 238 self.pixels[index] = { 239 r = rgba.r or 0, 240 g = rgba.g or 0, 241 b = rgba.b or 0, 242 a = rgba.a or 255 243 } 244 245 if self.imageBuffer and ImageSetPixel then 246 ImageSetPixel(self.imageBuffer, x, y, rgba.r or 0, rgba.g or 0, rgba.b or 0, rgba.a or 255) 247 end 248 end 249 250 -- Fill entire image with a color 251 -- @param color Color in hex format "RRGGBB" or "RRGGBBAA" 252 function PNGImage:fill(color) 253 checkPermission() 254 255 for y = 0, self.height - 1 do 256 for x = 0, self.width - 1 do 257 self:writePixel(x, y, color) 258 end 259 end 260 end 261 262 -- Clear image to transparent (or opaque black if no alpha) 263 function PNGImage:clear() 264 checkPermission() 265 266 local clearColor = self.hasAlpha and "00000000" or "000000FF" 267 self:fill(clearColor) 268 end 269 270 -- Get image dimensions 271 -- @return width, height 272 function PNGImage:getSize() 273 return self.width, self.height 274 end 275 276 -- Get image info 277 -- @return Table with width, height, hasAlpha, bpp 278 function PNGImage:getInfo() 279 return { 280 width = self.width, 281 height = self.height, 282 hasAlpha = self.hasAlpha, 283 bpp = self.bpp 284 } 285 end 286 287 -- Draw a rectangle 288 -- @param x X coordinate of top-left corner 289 -- @param y Y coordinate of top-left corner 290 -- @param width Rectangle width 291 -- @param height Rectangle height 292 -- @param color Fill color 293 function PNGImage:fillRect(x, y, width, height, color) 294 checkPermission() 295 296 for dy = 0, height - 1 do 297 for dx = 0, width - 1 do 298 local px = x + dx 299 local py = y + dy 300 if px >= 0 and px < self.width and py >= 0 and py < self.height then 301 self:writePixel(px, py, color) 302 end 303 end 304 end 305 end 306 307 -- Draw a line (Bresenham's algorithm) 308 -- @param x1 Start X 309 -- @param y1 Start Y 310 -- @param x2 End X 311 -- @param y2 End Y 312 -- @param color Line color 313 function PNGImage:drawLine(x1, y1, x2, y2, color) 314 checkPermission() 315 316 local dx = math.abs(x2 - x1) 317 local dy = math.abs(y2 - y1) 318 local sx = x1 < x2 and 1 or -1 319 local sy = y1 < y2 and 1 or -1 320 local err = dx - dy 321 322 while true do 323 if x1 >= 0 and x1 < self.width and y1 >= 0 and y1 < self.height then 324 self:writePixel(x1, y1, color) 325 end 326 327 if x1 == x2 and y1 == y2 then 328 break 329 end 330 331 local e2 = 2 * err 332 if e2 > -dy then 333 err = err - dy 334 x1 = x1 + sx 335 end 336 if e2 < dx then 337 err = err + dx 338 y1 = y1 + sy 339 end 340 end 341 end 342 343 -- Convert to C image buffer (if not already) 344 -- @return C image_t userdata or nil 345 function PNGImage:toImageBuffer() 346 checkPermission() 347 348 if self.imageBuffer then 349 return self.imageBuffer 350 end 351 352 if not ImageCreate then 353 return nil 354 end 355 356 -- Create C image buffer 357 local img = ImageCreate(self.width, self.height, self.hasAlpha) 358 359 if not img then 360 return nil 361 end 362 363 -- Copy all pixels 364 if ImageSetPixel then 365 for y = 0, self.height - 1 do 366 for x = 0, self.width - 1 do 367 local index = y * self.width + x + 1 368 local p = self.pixels[index] 369 ImageSetPixel(img, x, y, p.r, p.g, p.b, p.a) 370 end 371 end 372 end 373 374 self.imageBuffer = img 375 return img 376 end 377 378 -- Load PNG from file 379 -- @param path Path to PNG file 380 -- @return PNG image object or nil on error 381 function PNG.load(path) 382 checkPermission() 383 384 if not CRamdiskExists or not CRamdiskOpen or not CRamdiskRead or not CRamdiskClose then 385 error("PNG.load requires ramdisk functions") 386 end 387 388 if not PNGLoad or not ImageGetWidth or not ImageGetHeight or not ImageGetPixel then 389 error("PNG.load requires PNG decoder functions") 390 end 391 392 if not CRamdiskExists(path) then 393 return nil, "File not found: " .. path 394 end 395 396 local handle = CRamdiskOpen(path, "r") 397 if not handle then 398 return nil, "Failed to open file: " .. path 399 end 400 401 local pngData = CRamdiskRead(handle) 402 CRamdiskClose(handle) 403 404 if not pngData then 405 return nil, "Failed to read file: " .. path 406 end 407 408 -- Decode PNG 409 local imgBuffer = PNGLoad(pngData) 410 if not imgBuffer then 411 return nil, "Failed to decode PNG: " .. path 412 end 413 414 -- Get dimensions 415 local width = ImageGetWidth(imgBuffer) 416 local height = ImageGetHeight(imgBuffer) 417 418 -- Create PNG object 419 local self = setmetatable({}, PNGImage) 420 self.width = width 421 self.height = height 422 self.hasAlpha = true 423 self.bpp = 32 424 self.pixels = {} 425 self.imageBuffer = imgBuffer 426 427 -- Read all pixels from C buffer 428 for y = 0, height - 1 do 429 for x = 0, width - 1 do 430 local r, g, b, a = ImageGetPixel(imgBuffer, x, y) 431 local index = y * width + x + 1 432 self.pixels[index] = { 433 r = r or 0, 434 g = g or 0, 435 b = b or 0, 436 a = a or 255 437 } 438 end 439 end 440 441 return self 442 end 443 444 -- Clone the image 445 -- @return New PNG image object with copied pixels 446 function PNGImage:clone() 447 checkPermission() 448 449 local newImg = PNG.new(self.width, self.height, self.hasAlpha) 450 451 -- Copy all pixels 452 for i = 1, #self.pixels do 453 local p = self.pixels[i] 454 newImg.pixels[i] = { 455 r = p.r, 456 g = p.g, 457 b = p.b, 458 a = p.a 459 } 460 end 461 462 return newImg 463 end 464 465 return PNG