commit 51f75cdfff4b082259a3fd0d9b0a608283a2a9a0
parent 24f03437c48d300bed3c8033f0038a8c5309866a
Author: luajitos <bbhbb2094@gmail.com>
Date: Tue, 2 Dec 2025 02:45:02 +0000
Fixed Paint
Diffstat:
10 files changed, 875 insertions(+), 601 deletions(-)
diff --git a/decoder.c b/decoder.c
@@ -590,3 +590,50 @@ int lua_image_rotate(lua_State* L) {
lua_pushlightuserdata(L, rotated);
return 1;
}
+
+/* Lua binding: Get image buffer as BGRA string (for Lua Image library) */
+int lua_image_get_buffer_bgra(lua_State* L) {
+ image_t* img = (image_t*)lua_touserdata(L, 1);
+
+ if (!img || !img->data) {
+ lua_pushnil(L);
+ lua_pushstring(L, "Invalid image");
+ return 2;
+ }
+
+ /* Allocate buffer for BGRA data (always 4 bytes per pixel) */
+ size_t buf_size = img->width * img->height * 4;
+ uint8_t* bgra_buf = (uint8_t*)malloc(buf_size);
+ if (!bgra_buf) {
+ lua_pushnil(L);
+ lua_pushstring(L, "Out of memory");
+ return 2;
+ }
+
+ /* Convert image data to BGRA format */
+ int bytes_per_pixel = img->bpp / 8;
+ for (uint32_t y = 0; y < img->height; y++) {
+ for (uint32_t x = 0; x < img->width; x++) {
+ uint8_t* src_pixel = img->data + (y * img->width + x) * bytes_per_pixel;
+ uint8_t* dst_pixel = bgra_buf + (y * img->width + x) * 4;
+
+ /* Source is RGB or RGBA */
+ uint8_t r = src_pixel[0];
+ uint8_t g = src_pixel[1];
+ uint8_t b = src_pixel[2];
+ uint8_t a = (bytes_per_pixel == 4) ? src_pixel[3] : 255;
+
+ /* Destination is BGRA */
+ dst_pixel[0] = b;
+ dst_pixel[1] = g;
+ dst_pixel[2] = r;
+ dst_pixel[3] = a;
+ }
+ }
+
+ /* Push as Lua string */
+ lua_pushlstring(L, (const char*)bgra_buf, buf_size);
+ free(bgra_buf);
+
+ return 1;
+}
diff --git a/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua b/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua
@@ -23,10 +23,10 @@ local visibleLines = math.floor(contentHeight / lineHeight)
-- Toolbar buttons
local toolbarButtons = {
- { x = 5, y = 5, width = 50, height = 20, label = "New", action = "new" },
- { x = 60, y = 5, width = 50, height = 20, label = "Open", action = "open" },
- { x = 115, y = 5, width = 50, height = 20, label = "Save", action = "save" },
- { x = 170, y = 5, width = 55, height = 20, label = "SaveAs", action = "saveas" },
+ { x = 5, y = 5, width = 70, height = 20, label = "New", action = "new" },
+ { x = 80, y = 5, width = 70, height = 20, label = "Open", action = "open" },
+ { x = 155, y = 5, width = 70, height = 20, label = "Save", action = "save" },
+ { x = 230, y = 5, width = 75, height = 20, label = "Save As", action = "saveas" },
}
-- Helper: get window title
@@ -411,6 +411,21 @@ window.onClick = function(x, y, button)
end
end
+-- Resize callback
+window.onResize = function(newWidth, newHeight, oldWidth, oldHeight)
+ windowWidth = newWidth
+ windowHeight = newHeight
+ contentHeight = windowHeight - toolbarHeight - statusBarHeight
+ visibleLines = math.floor(contentHeight / lineHeight)
+ if osprint then
+ osprint("[LunarEditor] onResize: " .. oldWidth .. "x" .. oldHeight .. " -> " .. newWidth .. "x" .. newHeight .. "\n")
+ osprint("[LunarEditor] contentHeight=" .. contentHeight .. " visibleLines=" .. visibleLines .. "\n")
+ end
+ -- Ensure cursor is still visible after resize
+ ensureCursorVisible()
+ window:markDirty()
+end
+
-- Check for command line arguments to open a file
if args then
local argPath = args.o or args.open or args[1]
diff --git a/iso_includes/apps/com.luajitos.paint/src/init.lua b/iso_includes/apps/com.luajitos.paint/src/init.lua
@@ -13,9 +13,13 @@ window.resizable = true -- Allow resizing
-- Store drawn strokes (lines between points)
local strokes = {}
--- Background image (loaded BMP/PNG)
+-- Background image (native C image for display)
local bgImage = nil
+-- Background image dimensions (for saving)
+local bgImageWidth = nil
+local bgImageHeight = nil
+
-- Current file path
local currentFile = nil
@@ -68,7 +72,7 @@ local function addLine(x1, y1, x2, y2)
end
end
--- Load image file (BMP or PNG)
+-- Load image file (BMP, PNG, or JPEG)
local function loadImage(path)
if not path then return false end
@@ -77,22 +81,34 @@ local function loadImage(path)
local ext = path:lower():match("%.([^%.]+)$")
+ -- Destroy old image if exists
+ if bgImage and ImageDestroy then
+ ImageDestroy(bgImage)
+ bgImage = nil
+ end
+ bgImageWidth = nil
+ bgImageHeight = nil
+
+ -- Load native image for display
+ local nativeImg = nil
if ext == "bmp" and BMPLoad then
- if bgImage and ImageDestroy then
- ImageDestroy(bgImage)
- end
- bgImage = BMPLoad(data)
- currentFile = path
- strokes = {}
- return bgImage ~= nil
+ nativeImg = BMPLoad(data)
elseif ext == "png" and PNGLoad then
- if bgImage and ImageDestroy then
- ImageDestroy(bgImage)
+ nativeImg = PNGLoad(data)
+ elseif (ext == "jpeg" or ext == "jpg") and JPEGLoad then
+ nativeImg = JPEGLoad(data)
+ end
+
+ if nativeImg then
+ bgImage = nativeImg
+ -- Get dimensions from native image
+ if ImageGetWidth and ImageGetHeight then
+ bgImageWidth = ImageGetWidth(nativeImg)
+ bgImageHeight = ImageGetHeight(nativeImg)
end
- bgImage = PNGLoad(data)
currentFile = path
strokes = {}
- return bgImage ~= nil
+ return true
end
return false
@@ -106,6 +122,8 @@ local buttons = {
ImageDestroy(bgImage)
bgImage = nil
end
+ bgImageWidth = nil
+ bgImageHeight = nil
currentFile = nil
window:markDirty()
end},
@@ -139,34 +157,58 @@ local buttons = {
local canvasW = window.width
local canvasH = window.height - toolbarHeight
- -- Create image from canvas
- local img = Image.new(canvasW, canvasH, false)
-
- -- Fill with white background
- for y = 0, canvasH - 1 do
- for x = 0, canvasW - 1 do
- img:writePixel(x, y, 255, 255, 255)
+ local img
+ local imgW, imgH
+
+ -- If we have a background image, load it with Image.open for saving
+ if currentFile and bgImage then
+ -- Use Image.open to get the Lua Image object
+ Image.fsOverride = fs
+ local loadedImg, err = Image.open(currentFile)
+ Image.fsOverride = nil
+
+ if loadedImg then
+ img = loadedImg
+ imgW, imgH = img:getSize()
+ else
+ print("Paint: Failed to reload image for save: " .. (err or "unknown"))
end
end
+ -- If no background image loaded, create new white canvas
+ if not img then
+ imgW = canvasW
+ imgH = canvasH
+ img = Image.new(imgW, imgH, false)
+ img:fill(0xFFFFFF)
+ end
+
+ -- Use batch mode for efficient stroke drawing
+ img:beginBatch()
+
-- Draw all strokes
for _, s in ipairs(strokes) do
- local r = s.r
+ local rad = s.r
+ -- Extract RGB from color
local sr = bit.band(bit.rshift(s.color, 16), 0xFF)
local sg = bit.band(bit.rshift(s.color, 8), 0xFF)
local sb = bit.band(s.color, 0xFF)
- for dy = -r, r do
- for dx = -r, r do
- if dx*dx + dy*dy <= r*r then
+
+ for dy = -rad, rad do
+ for dx = -rad, rad do
+ if dx*dx + dy*dy <= rad*rad then
local px, py = s.x + dx, s.y + dy
- if px >= 0 and px < canvasW and py >= 0 and py < canvasH then
- img:writePixel(px, py, sr, sg, sb)
+ if px >= 0 and px < imgW and py >= 0 and py < imgH then
+ img:_batchWritePixel(px, py, sr, sg, sb)
end
end
end
end
end
+ -- End batch mode to finalize buffer
+ img:endBatch()
+
-- Save based on extension
local ext = path:lower():match("%.([^%.]+)$")
local success, err
diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua
@@ -1445,7 +1445,10 @@ function MainDraw()
_G.resizing_window.height = new_h
_G.resizing_window.dirty = true
- -- Clear buffer to force recreation
+ -- Free old buffer and clear to force recreation
+ if _G.resizing_window.buffer and VESAFreeWindowBuffer then
+ VESAFreeWindowBuffer(_G.resizing_window.buffer)
+ end
_G.resizing_window.buffer = nil
-- Call onResize callback if dimensions changed
diff --git a/iso_includes/os/libs/Application.lua b/iso_includes/os/libs/Application.lua
@@ -558,6 +558,12 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6)
end
end
+ -- Free window buffer to release memory
+ if self.buffer and VESAFreeWindowBuffer then
+ VESAFreeWindowBuffer(self.buffer)
+ self.buffer = nil
+ end
+
-- Hide the window
self:hide()
@@ -636,7 +642,10 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6)
self.maximized = true
end
- -- Clear buffer to force recreation with new size
+ -- Free old buffer and clear to force recreation with new size
+ if self.buffer and VESAFreeWindowBuffer then
+ VESAFreeWindowBuffer(self.buffer)
+ end
self.buffer = nil
self.dirty = true
@@ -644,6 +653,27 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6)
_G.osprint(string.format("[MAXIMIZE] Window now %dx%d, buffer cleared, maximized=%s\n",
self.width, self.height, tostring(self.maximized)))
end
+
+ -- Call onResize callback if it exists
+ if self.onResize then
+ local oldWidth = self.restoreWidth or self.width
+ local oldHeight = self.restoreHeight or self.height
+ if self.maximized then
+ -- We just maximized, old size is restore size
+ pcall(self.onResize, self.width, self.height, self.restoreWidth, self.restoreHeight)
+ else
+ -- We just restored, old size was the maximized size (screen size)
+ local screenW = 1024
+ local screenH = 768
+ if _G.sys and _G.sys.screens and _G.sys.screens[1] then
+ screenW = _G.sys.screens[1].width or screenW
+ screenH = _G.sys.screens[1].height or screenH
+ end
+ local maxW = screenW - (BORDER_WIDTH * 2)
+ local maxH = screenH - TASKBAR_HEIGHT - TITLE_BAR_HEIGHT - (BORDER_WIDTH * 2)
+ pcall(self.onResize, self.width, self.height, maxW, maxH)
+ end
+ end
end
-- Resize window
@@ -662,7 +692,10 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6)
self.width = newWidth
self.height = newHeight
- -- Clear buffer to force recreation with new size
+ -- Free old buffer and clear to force recreation with new size
+ if self.buffer and VESAFreeWindowBuffer then
+ VESAFreeWindowBuffer(self.buffer)
+ end
self.buffer = nil
self.dirty = true
diff --git a/iso_includes/os/libs/Image.lua b/iso_includes/os/libs/Image.lua
@@ -1,6 +1,7 @@
-- Image.lua - Image Creation and Manipulation Library
-- Requires: "imaging" permission
--- Uses binary string buffer for pixel storage
+-- Uses BGRA binary string buffer (matching screen buffer format for fast blitting)
+-- Internal format: 4 bytes per pixel, order: B, G, R, A (little-endian 0xAARRGGBB)
local Image = {}
@@ -106,7 +107,7 @@ ImageObj.__metatable = false -- Prevent metatable access/modification
-- Create a new image
-- @param width Image width in pixels
-- @param height Image height in pixels
--- @param hasAlpha Optional, default true - whether image has alpha channel
+-- @param hasAlpha Optional, default true - whether image uses alpha (always stored as 4 bytes)
-- @return Image object
function Image.new(width, height, hasAlpha)
checkPermission()
@@ -130,19 +131,19 @@ function Image.new(width, height, hasAlpha)
self.width = width
self.height = height
self.hasAlpha = hasAlpha
- self.bytesPerPixel = hasAlpha and 4 or 3 -- RGBA or RGB
+ self.bytesPerPixel = 4 -- Always 4 bytes (BGRA) for screen buffer compatibility
-- Create binary buffer for pixels
- -- Format: RGBA (4 bytes per pixel) or RGB (3 bytes per pixel)
+ -- Format: BGRA (4 bytes per pixel) matching screen buffer
+ -- Memory order: B, G, R, A (which is 0xAARRGGBB in little-endian uint32)
local numPixels = width * height
-- Initialize to transparent black (or opaque black if no alpha)
- -- Use string.rep for efficient allocation
local pixel
if hasAlpha then
- pixel = "\0\0\0\0" -- RGBA: transparent black
+ pixel = "\0\0\0\0" -- BGRA: transparent black (B=0, G=0, R=0, A=0)
else
- pixel = "\0\0\0" -- RGB: black
+ pixel = "\0\0\0\255" -- BGRA: opaque black (B=0, G=0, R=0, A=255)
end
self.buffer = string.rep(pixel, numPixels)
@@ -153,7 +154,7 @@ end
-- Write a pixel to the image
-- @param x X coordinate (0-based)
-- @param y Y coordinate (0-based)
--- @param color Color in hex format "RRGGBB" or "RRGGBBAA", number, or table {r, g, b, a}
+-- @param color Color in hex format "RRGGBB" or "RRGGBBAA", number (0xRRGGBB or 0xAARRGGBB), or table {r, g, b, a}
function ImageObj:writePixel(x, y, color)
checkPermission()
@@ -162,76 +163,67 @@ function ImageObj:writePixel(x, y, color)
end
if x < 0 or x >= self.width or y < 0 or y >= self.height then
- error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")")
+ return -- Silently ignore out of bounds (more efficient for drawing)
end
- -- Parse color
+ -- Parse color to get r, g, b, a values
local r, g, b, a
if type(color) == "table" then
-- Table format {r, g, b, a}
- r = color.r or 0
- g = color.g or 0
- b = color.b or 0
- a = color.a or 255
+ r = color.r or color[1] or 0
+ g = color.g or color[2] or 0
+ b = color.b or color[3] or 0
+ a = color.a or color[4] or 255
elseif type(color) == "string" then
-- Remove # if present
color = color:gsub("^#", "")
if #color == 6 then
-- RRGGBB format
- r = tonumber(color:sub(1, 2), 16)
- g = tonumber(color:sub(3, 4), 16)
- b = tonumber(color:sub(5, 6), 16)
+ r = tonumber(color:sub(1, 2), 16) or 0
+ g = tonumber(color:sub(3, 4), 16) or 0
+ b = tonumber(color:sub(5, 6), 16) or 0
a = 255
elseif #color == 8 then
-- RRGGBBAA format
- r = tonumber(color:sub(1, 2), 16)
- g = tonumber(color:sub(3, 4), 16)
- b = tonumber(color:sub(5, 6), 16)
- a = tonumber(color:sub(7, 8), 16)
+ r = tonumber(color:sub(1, 2), 16) or 0
+ g = tonumber(color:sub(3, 4), 16) or 0
+ b = tonumber(color:sub(5, 6), 16) or 0
+ a = tonumber(color:sub(7, 8), 16) or 255
else
- error("Invalid color format. Use 'RRGGBB' or 'RRGGBBAA'")
+ r, g, b, a = 0, 0, 0, 255
end
elseif type(color) == "number" then
- -- Treat as 0xRRGGBB or 0xRRGGBBAA
+ -- Treat as 0xRRGGBB or 0xAARRGGBB
if color <= 0xFFFFFF then
- -- RGB format
+ -- RGB format (0xRRGGBB)
r = bit.rshift(bit.band(color, 0xFF0000), 16)
g = bit.rshift(bit.band(color, 0x00FF00), 8)
b = bit.band(color, 0x0000FF)
a = 255
else
- -- RGBA format
- r = bit.rshift(bit.band(color, 0xFF000000), 24)
- g = bit.rshift(bit.band(color, 0x00FF0000), 16)
- b = bit.rshift(bit.band(color, 0x0000FF00), 8)
- a = bit.band(color, 0x000000FF)
+ -- ARGB format (0xAARRGGBB)
+ a = bit.rshift(bit.band(color, 0xFF000000), 24)
+ r = bit.rshift(bit.band(color, 0x00FF0000), 16)
+ g = bit.rshift(bit.band(color, 0x0000FF00), 8)
+ b = bit.band(color, 0x000000FF)
end
else
- error("Color must be a string (hex), number, or table {r, g, b, a}")
- end
-
- if not r or not g or not b then
- error("Failed to parse color: " .. tostring(color))
+ r, g, b, a = 0, 0, 0, 255
end
- -- Calculate byte offset in buffer (row-major order)
+ -- Calculate byte offset in buffer (4 bytes per pixel, BGRA order)
local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
+ local byteOffset = pixelIndex * 4 + 1
- -- Update buffer
- local newBytes
- if self.hasAlpha then
- newBytes = string.char(r, g, b, a)
- else
- newBytes = string.char(r, g, b)
- end
+ -- Write BGRA bytes
+ local newBytes = string.char(b, g, r, a)
-- Replace bytes in buffer
self.buffer = self.buffer:sub(1, byteOffset - 1) ..
newBytes ..
- self.buffer:sub(byteOffset + self.bytesPerPixel)
+ self.buffer:sub(byteOffset + 4)
end
-- Read a pixel from the image
@@ -249,26 +241,21 @@ function ImageObj:readPixel(x, y)
error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")")
end
- -- Calculate byte offset in buffer
+ -- Calculate byte offset in buffer (4 bytes per pixel, BGRA order)
local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
+ local byteOffset = pixelIndex * 4 + 1
- -- Read bytes from buffer
- local r = string.byte(self.buffer, byteOffset)
+ -- Read BGRA bytes
+ local b = string.byte(self.buffer, byteOffset)
local g = string.byte(self.buffer, byteOffset + 1)
- local b = string.byte(self.buffer, byteOffset + 2)
-
- -- Format as hex string
- local rHex = string.format("%02X", r)
- local gHex = string.format("%02X", g)
- local bHex = string.format("%02X", b)
+ local r = string.byte(self.buffer, byteOffset + 2)
+ local a = string.byte(self.buffer, byteOffset + 3)
+ -- Format as hex string (RRGGBBAA)
if self.hasAlpha then
- local a = string.byte(self.buffer, byteOffset + 3)
- local aHex = string.format("%02X", a)
- return rHex .. gHex .. bHex .. aHex
+ return string.format("%02X%02X%02X%02X", r, g, b, a)
else
- return rHex .. gHex .. bHex
+ return string.format("%02X%02X%02X", r, g, b)
end
end
@@ -288,12 +275,13 @@ function ImageObj:getPixel(x, y)
end
local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
+ local byteOffset = pixelIndex * 4 + 1
- local r = string.byte(self.buffer, byteOffset)
+ -- Read BGRA bytes
+ local b = string.byte(self.buffer, byteOffset)
local g = string.byte(self.buffer, byteOffset + 1)
- local b = string.byte(self.buffer, byteOffset + 2)
- local a = self.hasAlpha and string.byte(self.buffer, byteOffset + 3) or 255
+ local r = string.byte(self.buffer, byteOffset + 2)
+ local a = string.byte(self.buffer, byteOffset + 3)
return {
r = r,
@@ -315,57 +303,82 @@ function ImageObj:setPixel(x, y, rgba)
end
if x < 0 or x >= self.width or y < 0 or y >= self.height then
- error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")")
+ return -- Silently ignore out of bounds
end
- local r = rgba.r or 0
- local g = rgba.g or 0
- local b = rgba.b or 0
- local a = rgba.a or 255
+ local r = rgba.r or rgba[1] or 0
+ local g = rgba.g or rgba[2] or 0
+ local b = rgba.b or rgba[3] or 0
+ local a = rgba.a or rgba[4] or 255
local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
+ local byteOffset = pixelIndex * 4 + 1
- local newBytes
- if self.hasAlpha then
- newBytes = string.char(r, g, b, a)
- else
- newBytes = string.char(r, g, b)
- end
+ -- Write BGRA bytes
+ local newBytes = string.char(b, g, r, a)
self.buffer = self.buffer:sub(1, byteOffset - 1) ..
newBytes ..
- self.buffer:sub(byteOffset + self.bytesPerPixel)
+ self.buffer:sub(byteOffset + 4)
end
-- Fill entire image with a color
--- @param color Color in hex format "RRGGBB" or "RRGGBBAA", or table {r, g, b, a}
+-- @param color Color in hex format "RRGGBB" or "RRGGBBAA", number, or table {r, g, b, a}
function ImageObj:fill(color)
checkPermission()
- -- Convert table to hex string if needed
- if type(color) == "table" then
- local r = color.r or 0
- local g = color.g or 0
- local b = color.b or 0
- local a = color.a or 255
-
- color = string.format("%02X%02X%02X%02X", r, g, b, a)
- end
+ -- Parse color to get r, g, b, a values
+ local r, g, b, a = 0, 0, 0, 255
- for y = 0, self.height - 1 do
- for x = 0, self.width - 1 do
- self:writePixel(x, y, color)
+ if type(color) == "table" then
+ r = color.r or color[1] or 0
+ g = color.g or color[2] or 0
+ b = color.b or color[3] or 0
+ a = color.a or color[4] or 255
+ elseif type(color) == "string" then
+ color = color:gsub("^#", "")
+ if #color == 6 then
+ r = tonumber(color:sub(1, 2), 16) or 0
+ g = tonumber(color:sub(3, 4), 16) or 0
+ b = tonumber(color:sub(5, 6), 16) or 0
+ a = 255
+ elseif #color == 8 then
+ r = tonumber(color:sub(1, 2), 16) or 0
+ g = tonumber(color:sub(3, 4), 16) or 0
+ b = tonumber(color:sub(5, 6), 16) or 0
+ a = tonumber(color:sub(7, 8), 16) or 255
+ end
+ elseif type(color) == "number" then
+ if color <= 0xFFFFFF then
+ r = bit.rshift(bit.band(color, 0xFF0000), 16)
+ g = bit.rshift(bit.band(color, 0x00FF00), 8)
+ b = bit.band(color, 0x0000FF)
+ a = 255
+ else
+ a = bit.rshift(bit.band(color, 0xFF000000), 24)
+ r = bit.rshift(bit.band(color, 0x00FF0000), 16)
+ g = bit.rshift(bit.band(color, 0x0000FF00), 8)
+ b = bit.band(color, 0x000000FF)
end
end
+
+ -- Create single pixel in BGRA format
+ local pixel = string.char(b, g, r, a)
+
+ -- Fill buffer efficiently using string.rep
+ local numPixels = self.width * self.height
+ self.buffer = string.rep(pixel, numPixels)
end
-- Clear image to transparent (or opaque black if no alpha)
function ImageObj:clear()
checkPermission()
- local clearColor = self.hasAlpha and "00000000" or "000000FF"
- self:fill(clearColor)
+ if self.hasAlpha then
+ self:fill({r=0, g=0, b=0, a=0})
+ else
+ self:fill({r=0, g=0, b=0, a=255})
+ end
end
-- Get image dimensions
@@ -386,7 +399,7 @@ function ImageObj:getInfo()
}
end
--- Draw a rectangle
+-- Draw a filled rectangle
-- @param x X coordinate of top-left corner
-- @param y Y coordinate of top-left corner
-- @param width Rectangle width
@@ -395,15 +408,69 @@ end
function ImageObj:fillRect(x, y, width, height, color)
checkPermission()
- for dy = 0, height - 1 do
- for dx = 0, width - 1 do
- local px = x + dx
- local py = y + dy
- if px >= 0 and px < self.width and py >= 0 and py < self.height then
- self:writePixel(px, py, color)
- end
+ -- Parse color once
+ local r, g, b, a = 0, 0, 0, 255
+ if type(color) == "number" then
+ if color <= 0xFFFFFF then
+ r = bit.rshift(bit.band(color, 0xFF0000), 16)
+ g = bit.rshift(bit.band(color, 0x00FF00), 8)
+ b = bit.band(color, 0x0000FF)
+ else
+ a = bit.rshift(bit.band(color, 0xFF000000), 24)
+ r = bit.rshift(bit.band(color, 0x00FF0000), 16)
+ g = bit.rshift(bit.band(color, 0x0000FF00), 8)
+ b = bit.band(color, 0x000000FF)
+ end
+ elseif type(color) == "table" then
+ r = color.r or color[1] or 0
+ g = color.g or color[2] or 0
+ b = color.b or color[3] or 0
+ a = color.a or color[4] or 255
+ end
+
+ local pixel = string.char(b, g, r, a)
+
+ -- Clip to image bounds
+ local x1 = math.max(0, x)
+ local y1 = math.max(0, y)
+ local x2 = math.min(self.width, x + width)
+ local y2 = math.min(self.height, y + height)
+
+ if x1 >= x2 or y1 >= y2 then return end
+
+ local rectWidth = x2 - x1
+ local rowPixels = string.rep(pixel, rectWidth)
+
+ -- Build new buffer with rectangle
+ local parts = {}
+ local imgWidth = self.width
+
+ -- Rows before rectangle
+ if y1 > 0 then
+ parts[#parts + 1] = self.buffer:sub(1, y1 * imgWidth * 4)
+ end
+
+ -- Rectangle rows
+ for row = y1, y2 - 1 do
+ local rowStart = row * imgWidth * 4 + 1
+ -- Pixels before rectangle in this row
+ if x1 > 0 then
+ parts[#parts + 1] = self.buffer:sub(rowStart, rowStart + x1 * 4 - 1)
+ end
+ -- Rectangle pixels
+ parts[#parts + 1] = rowPixels
+ -- Pixels after rectangle in this row
+ if x2 < imgWidth then
+ parts[#parts + 1] = self.buffer:sub(rowStart + x2 * 4, rowStart + imgWidth * 4 - 1)
end
end
+
+ -- Rows after rectangle
+ if y2 < self.height then
+ parts[#parts + 1] = self.buffer:sub(y2 * imgWidth * 4 + 1)
+ end
+
+ self.buffer = table.concat(parts)
end
-- Draw a line (Bresenham's algorithm)
@@ -442,22 +509,101 @@ function ImageObj:drawLine(x1, y1, x2, y2, color)
end
end
--- Get raw buffer
+-- Get raw buffer (BGRA format, ready for screen blitting)
-- @return Binary string buffer
function ImageObj:getBuffer()
checkPermission()
return self.buffer
end
+-- Get row of pixels as binary string (for fast blitting)
+-- @param y Row index (0-based)
+-- @return Binary string of row pixels in BGRA format
+function ImageObj:getRow(y)
+ checkPermission()
+ if y < 0 or y >= self.height then
+ return nil
+ end
+ local rowStart = y * self.width * 4 + 1
+ local rowEnd = rowStart + self.width * 4 - 1
+ return self.buffer:sub(rowStart, rowEnd)
+end
+
+-- Begin batch edit mode - converts buffer to table for faster pixel writes
+-- Call endBatch() when done to convert back to string
+function ImageObj:beginBatch()
+ checkPermission()
+ if self._batchMode then return end -- Already in batch mode
+
+ -- Convert string buffer to table of bytes for fast random access
+ -- Process in chunks to avoid "string slice too long" error
+ self._batchBuffer = {}
+ local bufLen = #self.buffer
+ local chunkSize = 4096
+
+ for i = 1, bufLen, chunkSize do
+ local endIdx = math.min(i + chunkSize - 1, bufLen)
+ local bytes = {string.byte(self.buffer, i, endIdx)}
+ for j = 1, #bytes do
+ self._batchBuffer[i + j - 1] = bytes[j]
+ end
+ end
+ self._batchMode = true
+end
+
+-- End batch edit mode - converts table back to string buffer
+function ImageObj:endBatch()
+ checkPermission()
+ if not self._batchMode then return end -- Not in batch mode
+
+ -- Convert table back to string using string.char with multiple args
+ -- Process in chunks to avoid too many arguments to string.char
+ local chunks = {}
+ local chunkSize = 256 -- string.char can handle many args
+ local bufLen = #self._batchBuffer
+
+ for i = 1, bufLen, chunkSize do
+ local endIdx = math.min(i + chunkSize - 1, bufLen)
+ -- Use unpack to pass multiple values to string.char at once
+ chunks[#chunks + 1] = string.char(unpack(self._batchBuffer, i, endIdx))
+ end
+
+ self.buffer = table.concat(chunks)
+ self._batchBuffer = nil
+ self._batchMode = false
+end
+
+-- Fast pixel write for batch mode (takes RGB values, writes BGRA)
+-- @param x X coordinate
+-- @param y Y coordinate
+-- @param r Red (0-255)
+-- @param g Green (0-255)
+-- @param b Blue (0-255)
+-- @param a Alpha (0-255), optional, defaults to 255
+function ImageObj:_batchWritePixel(x, y, r, g, b, a)
+ if not self._batchMode then return end
+ if x < 0 or x >= self.width or y < 0 or y >= self.height then return end
+
+ local pixelIndex = y * self.width + x
+ local byteOffset = pixelIndex * 4 + 1
+
+ -- Write BGRA bytes
+ self._batchBuffer[byteOffset] = b
+ self._batchBuffer[byteOffset + 1] = g
+ self._batchBuffer[byteOffset + 2] = r
+ self._batchBuffer[byteOffset + 3] = a or 255
+end
+
-- Convert to native C image for efficient drawing with ImageDraw
-- @return Native image handle (userdata) or nil on error
function ImageObj:toNativeImage()
checkPermission()
-- Check if ImageCreateFromBuffer is available (fastest path)
+ -- The buffer is already in BGRA format matching screen buffer
if ImageCreateFromBuffer then
- -- Pass the raw buffer directly to C - it will be copied in one go
- local img = ImageCreateFromBuffer(self.width, self.height, self.buffer, self.hasAlpha)
+ -- Pass the raw buffer directly to C - it's already in the right format
+ local img = ImageCreateFromBuffer(self.width, self.height, self.buffer, true)
if img then
return img
end
@@ -476,14 +622,13 @@ function ImageObj:toNativeImage()
end
-- Copy pixels to native image (slow path - pixel by pixel)
- local bpp = self.bytesPerPixel
for y = 0, self.height - 1 do
for x = 0, self.width - 1 do
- local offset = (y * self.width + x) * bpp + 1
- local r = string.byte(self.buffer, offset)
+ local offset = (y * self.width + x) * 4 + 1
+ local b = string.byte(self.buffer, offset)
local g = string.byte(self.buffer, offset + 1)
- local b = string.byte(self.buffer, offset + 2)
- local a = bpp == 4 and string.byte(self.buffer, offset + 3) or 255
+ local r = string.byte(self.buffer, offset + 2)
+ local a = string.byte(self.buffer, offset + 3)
-- ImageSetPixel expects 0xAARRGGBB format
local color = bit.bor(
@@ -527,6 +672,9 @@ function ImageObj:addImage(srcImage, x, y, w, h, opacity)
local scaleX = srcImage.width / w
local scaleY = srcImage.height / h
+ -- Use batch mode for efficiency
+ self:beginBatch()
+
-- Blend each pixel
for dstY = 0, h - 1 do
for dstX = 0, w - 1 do
@@ -544,38 +692,32 @@ function ImageObj:addImage(srcImage, x, y, w, h, opacity)
if srcX >= srcImage.width then srcX = srcImage.width - 1 end
if srcY >= srcImage.height then srcY = srcImage.height - 1 end
- -- Get source pixel
- local srcIndex = (srcY * srcImage.width + srcX) * srcImage.bytesPerPixel + 1
- local srcR = string.byte(srcImage.buffer, srcIndex)
+ -- Get source pixel (BGRA format)
+ local srcIndex = (srcY * srcImage.width + srcX) * 4 + 1
+ local srcB = string.byte(srcImage.buffer, srcIndex)
local srcG = string.byte(srcImage.buffer, srcIndex + 1)
- local srcB = string.byte(srcImage.buffer, srcIndex + 2)
- local srcA = srcImage.hasAlpha and string.byte(srcImage.buffer, srcIndex + 3) or 255
+ local srcR = string.byte(srcImage.buffer, srcIndex + 2)
+ local srcA = string.byte(srcImage.buffer, srcIndex + 3)
-- Apply global opacity
srcA = math.floor(srcA * opacity)
-- Get destination pixel
- local destIndex = (destY * self.width + destX) * self.bytesPerPixel + 1
- local dstR = string.byte(self.buffer, destIndex)
- local dstG = string.byte(self.buffer, destIndex + 1)
- local dstB = string.byte(self.buffer, destIndex + 2)
- local dstA = self.hasAlpha and string.byte(self.buffer, destIndex + 3) or 255
+ local destIndex = (destY * self.width + destX) * 4 + 1
+ local dstB = self._batchBuffer[destIndex]
+ local dstG = self._batchBuffer[destIndex + 1]
+ local dstR = self._batchBuffer[destIndex + 2]
+ local dstA = self._batchBuffer[destIndex + 3]
-- Alpha blending formula: result = src * srcAlpha + dst * (1 - srcAlpha)
local srcAlpha = srcA / 255
- local dstAlpha = dstA / 255
local invSrcAlpha = 1 - srcAlpha
-- Blend colors
local outR = math.floor(srcR * srcAlpha + dstR * invSrcAlpha)
local outG = math.floor(srcG * srcAlpha + dstG * invSrcAlpha)
local outB = math.floor(srcB * srcAlpha + dstB * invSrcAlpha)
-
- -- Calculate output alpha (for RGBA destinations)
- local outA = 255
- if self.hasAlpha then
- outA = math.floor(srcAlpha * 255 + dstAlpha * invSrcAlpha * 255)
- end
+ local outA = math.floor(srcA + dstA * invSrcAlpha)
-- Clamp values
outR = math.min(255, math.max(0, outR))
@@ -583,25 +725,21 @@ function ImageObj:addImage(srcImage, x, y, w, h, opacity)
outB = math.min(255, math.max(0, outB))
outA = math.min(255, math.max(0, outA))
- -- Write blended pixel
- local newBytes
- if self.hasAlpha then
- newBytes = string.char(outR, outG, outB, outA)
- else
- newBytes = string.char(outR, outG, outB)
- end
-
- self.buffer = self.buffer:sub(1, destIndex - 1) ..
- newBytes ..
- self.buffer:sub(destIndex + self.bytesPerPixel)
+ -- Write blended pixel (BGRA)
+ self._batchBuffer[destIndex] = outB
+ self._batchBuffer[destIndex + 1] = outG
+ self._batchBuffer[destIndex + 2] = outR
+ self._batchBuffer[destIndex + 3] = outA
end
end
end
+
+ self:endBatch()
end
-- Save image as PNG file
-- @param path Path to save PNG file
--- @param options Optional table with fields: fs (SafeFS instance), compression (boolean), colorSpace (string), interlacing (boolean)
+-- @param options Optional table with fields: fs (SafeFS instance), compression (boolean)
-- @return true on success, nil and error message on failure
function ImageObj:saveAsPNG(path, options)
checkPermission()
@@ -613,12 +751,10 @@ function ImageObj:saveAsPNG(path, options)
-- Parse options
options = options or {}
local compression = options.compression or false
- local colorSpace = options.colorSpace or "sRGB"
- local interlacing = options.interlacing or false
local safeFSInstance = options.fs
- -- Build PNG file format
- local pngData = self:_encodePNG(compression, colorSpace, interlacing)
+ -- Build PNG file format (convert from BGRA to RGBA)
+ local pngData = self:_encodePNG(compression)
if not pngData then
return nil, "Failed to encode PNG"
@@ -652,7 +788,7 @@ function ImageObj:saveAsBMP(path, options)
options = options or {}
local safeFSInstance = options.fs
- -- Build BMP file format
+ -- Build BMP file format (convert from BGRA to BGR)
local bmpData = self:_encodeBMP()
if not bmpData then
@@ -672,15 +808,11 @@ function ImageObj:saveAsBMP(path, options)
return writeFile(path, bmpData)
end
--- Internal: Encode image as PNG format
+-- Internal: Encode image as PNG format (converts BGRA buffer to RGBA for PNG)
-- @param compression Use compression (true/false)
--- @param colorSpace Color space (e.g., "sRGB", "linear")
--- @param interlacing Use ADAM7 interlacing (true/false)
-- @return PNG binary data as string
-function ImageObj:_encodePNG(compression, colorSpace, interlacing)
+function ImageObj:_encodePNG(compression)
compression = compression or false
- colorSpace = colorSpace or "sRGB"
- interlacing = interlacing or false
local chunks = {}
@@ -690,7 +822,7 @@ function ImageObj:_encodePNG(compression, colorSpace, interlacing)
-- IHDR chunk (image header)
local colorType = self.hasAlpha and 6 or 2 -- 6=RGBA, 2=RGB
- local interlaceMethod = interlacing and 1 or 0 -- 0=none, 1=ADAM7
+ local interlaceMethod = 0
local ihdr = self:_createChunk("IHDR",
self:_uint32(self.width) ..
@@ -703,27 +835,16 @@ function ImageObj:_encodePNG(compression, colorSpace, interlacing)
)
chunks[#chunks + 1] = ihdr
- -- sRGB chunk (if sRGB color space)
- if colorSpace == "sRGB" then
- -- Rendering intent: 0 = Perceptual
- local srgb = self:_createChunk("sRGB", string.char(0))
- chunks[#chunks + 1] = srgb
- end
+ -- sRGB chunk
+ local srgb = self:_createChunk("sRGB", string.char(0))
+ chunks[#chunks + 1] = srgb
- -- gAMA chunk (gamma correction)
- if colorSpace == "sRGB" then
- -- sRGB gamma is 2.2, stored as 1/2.2 * 100000 = 45455
- local gama = self:_createChunk("gAMA", self:_uint32(45455))
- chunks[#chunks + 1] = gama
- end
+ -- gAMA chunk
+ local gama = self:_createChunk("gAMA", self:_uint32(45455))
+ chunks[#chunks + 1] = gama
- -- IDAT chunk (image data)
- local imageData
- if interlacing then
- imageData = self:_createImageDataAdam7()
- else
- imageData = self:_createImageData()
- end
+ -- IDAT chunk (image data) - convert BGRA to RGBA
+ local imageData = self:_createImageDataRGBA()
local compressedData = self:_deflateCompress(imageData, compression)
local idat = self:_createChunk("IDAT", compressedData)
@@ -736,287 +857,97 @@ function ImageObj:_encodePNG(compression, colorSpace, interlacing)
return table.concat(chunks)
end
--- Internal: Create PNG chunk
--- @param chunkType 4-character chunk type
--- @param data Chunk data
--- @return Complete chunk with length, type, data, and CRC
-function ImageObj:_createChunk(chunkType, data)
- local length = self:_uint32(#data)
- local typeAndData = chunkType .. data
- local crc = self:_crc32(typeAndData)
-
- return length .. typeAndData .. self:_uint32(crc)
-end
-
--- Internal: Create image data with PNG filtering
--- @return Filtered image data
-function ImageObj:_createImageData()
+-- Internal: Create image data with PNG filtering (converts BGRA to RGBA)
+-- @return Filtered image data in RGBA format
+function ImageObj:_createImageDataRGBA()
local rows = {}
+ local width = self.width
+ local hasAlpha = self.hasAlpha
+ local buffer = self.buffer
for y = 0, self.height - 1 do
- -- Filter type 0 (None) for each scanline
- local row = {string.char(0)}
+ -- Build row data: filter byte + RGBA pixels
+ local rowBytes = {0} -- Filter type 0 (None)
+ local rowStart = y * width * 4 + 1
- for x = 0, self.width - 1 do
- local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
-
- local r = string.byte(self.buffer, byteOffset)
- local g = string.byte(self.buffer, byteOffset + 1)
- local b = string.byte(self.buffer, byteOffset + 2)
-
- row[#row + 1] = string.char(r, g, b)
-
- if self.hasAlpha then
- local a = string.byte(self.buffer, byteOffset + 3)
- row[#row + 1] = string.char(a)
+ for x = 0, width - 1 do
+ local offset = rowStart + x * 4
+ local b = string.byte(buffer, offset)
+ local g = string.byte(buffer, offset + 1)
+ local r = string.byte(buffer, offset + 2)
+ local a = string.byte(buffer, offset + 3)
+
+ -- Write RGBA (PNG format)
+ if hasAlpha then
+ rowBytes[#rowBytes + 1] = r
+ rowBytes[#rowBytes + 1] = g
+ rowBytes[#rowBytes + 1] = b
+ rowBytes[#rowBytes + 1] = a
+ else
+ rowBytes[#rowBytes + 1] = r
+ rowBytes[#rowBytes + 1] = g
+ rowBytes[#rowBytes + 1] = b
end
end
- rows[#rows + 1] = table.concat(row)
+ rows[#rows + 1] = string.char(unpack(rowBytes))
end
return table.concat(rows)
end
--- Internal: Create image data with ADAM7 interlacing
--- @return Filtered image data with ADAM7 interlacing
-function ImageObj:_createImageDataAdam7()
- -- ADAM7 interlacing pass parameters
- -- Pass: starting_row, starting_col, row_increment, col_increment
- local adam7Passes = {
- {0, 0, 8, 8}, -- Pass 1: every 8th pixel, starting at (0,0)
- {0, 4, 8, 8}, -- Pass 2: every 8th pixel, starting at (0,4)
- {4, 0, 8, 4}, -- Pass 3: every 4th pixel, starting at (4,0)
- {0, 2, 4, 4}, -- Pass 4: every 4th pixel, starting at (0,2)
- {2, 0, 4, 2}, -- Pass 5: every 2nd pixel, starting at (2,0)
- {0, 1, 2, 2}, -- Pass 6: every 2nd pixel, starting at (0,1)
- {1, 0, 2, 1} -- Pass 7: every pixel, starting at (1,0)
- }
-
- local allRows = {}
-
- for passNum = 1, 7 do
- local pass = adam7Passes[passNum]
- local startRow, startCol, rowInc, colInc = pass[1], pass[2], pass[3], pass[4]
-
- -- Calculate pass dimensions
- local passWidth = 0
- local passHeight = 0
-
- for y = startRow, self.height - 1, rowInc do
- passHeight = passHeight + 1
- end
-
- if passHeight > 0 then
- for x = startCol, self.width - 1, colInc do
- passWidth = passWidth + 1
- end
- end
-
- -- Only process if pass has pixels
- if passWidth > 0 and passHeight > 0 then
- local y = startRow
- for row = 0, passHeight - 1 do
- -- Filter type 0 (None) for each scanline
- local rowData = {string.char(0)}
-
- local x = startCol
- for col = 0, passWidth - 1 do
- local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
-
- local r = string.byte(self.buffer, byteOffset)
- local g = string.byte(self.buffer, byteOffset + 1)
- local b = string.byte(self.buffer, byteOffset + 2)
-
- rowData[#rowData + 1] = string.char(r, g, b)
-
- if self.hasAlpha then
- local a = string.byte(self.buffer, byteOffset + 3)
- rowData[#rowData + 1] = string.char(a)
- end
-
- x = x + colInc
- end
-
- allRows[#allRows + 1] = table.concat(rowData)
- y = y + rowInc
- end
- end
- end
+-- Internal: Create PNG chunk
+function ImageObj:_createChunk(chunkType, data)
+ local length = self:_uint32(#data)
+ local typeAndData = chunkType .. data
+ local crc = self:_crc32(typeAndData)
- return table.concat(allRows)
+ return length .. typeAndData .. self:_uint32(crc)
end
-- Internal: DEFLATE compression
--- @param data Data to compress
--- @param useCompression Use actual compression (true) or uncompressed blocks (false)
--- @return Compressed data with zlib wrapper
function ImageObj:_deflateCompress(data, useCompression)
useCompression = useCompression or false
local result = {}
-- Zlib header (RFC 1950)
- -- CMF (Compression Method and Flags)
- local cmf = 0x78 -- CM=8 (deflate), CINFO=7 (32K window)
- -- FLG (Flags)
- local flg = 0x01 -- FCHECK makes (CMF*256+FLG) % 31 == 0
+ local cmf = 0x78
+ local flg = 0x01
result[#result + 1] = string.char(cmf, flg)
- if useCompression then
- -- Use basic RLE compression (simple but effective)
- result[#result + 1] = self:_deflateCompressRLE(data)
- else
- -- DEFLATE data (RFC 1951) - uncompressed blocks
- local pos = 1
- while pos <= #data do
- local blockSize = math.min(65535, #data - pos + 1)
- local isLast = (pos + blockSize > #data) and 1 or 0
-
- -- Block header (3 bits: BFINAL, BTYPE)
- result[#result + 1] = string.char(isLast) -- BFINAL + BTYPE=00 (uncompressed)
-
- -- LEN and NLEN (16-bit little endian)
- local len = blockSize
- local nlen = bit.bxor(len, 0xFFFF)
-
- result[#result + 1] = string.char(
- bit.band(len, 0xFF),
- bit.rshift(len, 8),
- bit.band(nlen, 0xFF),
- bit.rshift(nlen, 8)
- )
-
- -- Literal data
- result[#result + 1] = data:sub(pos, pos + blockSize - 1)
-
- pos = pos + blockSize
- end
- end
-
- -- Adler-32 checksum (RFC 1950)
- local adler = self:_adler32(data)
- result[#result + 1] = self:_uint32(adler)
-
- return table.concat(result)
-end
-
--- Internal: DEFLATE compression with Fixed Huffman coding
--- @param data Data to compress
--- @return Compressed DEFLATE data with Huffman encoding
-function ImageObj:_deflateCompressRLE(data)
- -- Use Fixed Huffman codes (BTYPE=01)
- -- This provides compression without needing dynamic Huffman trees
-
- local bitWriter = {
- buffer = 0,
- bits = 0,
- output = {}
- }
-
- -- Helper: Write bits to output
- local function writeBits(value, numBits)
- for i = 0, numBits - 1 do
- bitWriter.buffer = bit.bor(bitWriter.buffer, bit.lshift(bit.band(bit.rshift(value, i), 1), bitWriter.bits))
- bitWriter.bits = bitWriter.bits + 1
-
- if bitWriter.bits == 8 then
- bitWriter.output[#bitWriter.output + 1] = string.char(bitWriter.buffer)
- bitWriter.buffer = 0
- bitWriter.bits = 0
- end
- end
- end
-
- -- Helper: Flush remaining bits
- local function flushBits()
- if bitWriter.bits > 0 then
- bitWriter.output[#bitWriter.output + 1] = string.char(bitWriter.buffer)
- bitWriter.buffer = 0
- bitWriter.bits = 0
- end
- end
-
- -- Fixed Huffman code tables (RFC 1951 Section 3.2.6)
- local function getFixedLiteralCode(value)
- if value <= 143 then
- -- 0-143: 8 bits, codes 00110000 through 10111111
- return bit.bor(0x30, value), 8
- elseif value <= 255 then
- -- 144-255: 9 bits, codes 110010000 through 111111111
- return bit.bor(0x190, value - 144), 9
- elseif value <= 279 then
- -- 256-279: 7 bits, codes 0000000 through 0010111
- return value - 256, 7
- else
- -- 280-287: 8 bits, codes 11000000 through 11000111
- return bit.bor(0xC0, value - 280), 8
- end
- end
-
- -- Block header: BFINAL=1, BTYPE=01 (fixed Huffman)
- writeBits(1, 1) -- BFINAL (last block)
- writeBits(1, 2) -- BTYPE (fixed Huffman)
-
- -- Simple LZ77: just look for repeated bytes
+ -- DEFLATE data (RFC 1951) - uncompressed blocks
local pos = 1
while pos <= #data do
- local byte = string.byte(data, pos)
+ local blockSize = math.min(65535, #data - pos + 1)
+ local isLast = (pos + blockSize > #data) and 1 or 0
- -- Look for run-length encoding opportunities
- local runLength = 1
- while pos + runLength <= #data and runLength < 258 do
- if string.byte(data, pos + runLength) == byte then
- runLength = runLength + 1
- else
- break
- end
- end
+ result[#result + 1] = string.char(isLast)
- if runLength >= 4 then
- -- Use length/distance pair
- -- First, output the literal byte
- local code, bits = getFixedLiteralCode(byte)
- writeBits(code, bits)
-
- -- Then output length code (simplified: just use code 257-285)
- local lengthCode = 257 + math.min(runLength - 3, 28)
- code, bits = getFixedLiteralCode(lengthCode)
- writeBits(code, bits)
-
- -- Extra bits for length (if needed)
- if runLength > 10 then
- local extraBits = math.min(math.floor((runLength - 11) / 2), 5)
- writeBits(runLength - 11 - extraBits * 2, extraBits)
- end
+ local len = blockSize
+ local nlen = bit.bxor(len, 0xFFFF)
- -- Distance code (distance=1 for RLE)
- writeBits(0, 5) -- Distance code 0 = distance 1
+ result[#result + 1] = string.char(
+ bit.band(len, 0xFF),
+ bit.rshift(len, 8),
+ bit.band(nlen, 0xFF),
+ bit.rshift(nlen, 8)
+ )
- pos = pos + runLength
- else
- -- Just output literal
- local code, bits = getFixedLiteralCode(byte)
- writeBits(code, bits)
- pos = pos + 1
- end
- end
+ result[#result + 1] = data:sub(pos, pos + blockSize - 1)
- -- End of block (code 256)
- local code, bits = getFixedLiteralCode(256)
- writeBits(code, bits)
+ pos = pos + blockSize
+ end
- flushBits()
+ -- Adler-32 checksum
+ local adler = self:_adler32(data)
+ result[#result + 1] = self:_uint32(adler)
- return table.concat(bitWriter.output)
+ return table.concat(result)
end
-- Internal: Calculate CRC32
--- @param data Input data
--- @return CRC32 value
function ImageObj:_crc32(data)
local crc = 0xFFFFFFFF
@@ -1037,8 +968,6 @@ function ImageObj:_crc32(data)
end
-- Internal: Calculate Adler-32
--- @param data Input data
--- @return Adler-32 value
function ImageObj:_adler32(data)
local s1 = 1
local s2 = 0
@@ -1053,8 +982,6 @@ function ImageObj:_adler32(data)
end
-- Internal: Encode 32-bit unsigned integer as big-endian
--- @param value Integer value
--- @return 4-byte big-endian string
function ImageObj:_uint32(value)
return string.char(
bit.band(bit.rshift(value, 24), 0xFF),
@@ -1065,8 +992,6 @@ function ImageObj:_uint32(value)
end
-- Internal: Encode 32-bit unsigned integer as little-endian
--- @param value Integer value
--- @return 4-byte little-endian string
function ImageObj:_uint32le(value)
return string.char(
bit.band(value, 0xFF),
@@ -1077,8 +1002,6 @@ function ImageObj:_uint32le(value)
end
-- Internal: Encode 16-bit unsigned integer as little-endian
--- @param value Integer value
--- @return 2-byte little-endian string
function ImageObj:_uint16le(value)
return string.char(
bit.band(value, 0xFF),
@@ -1086,124 +1009,207 @@ function ImageObj:_uint16le(value)
)
end
--- Internal: Encode image as BMP format
--- @return BMP binary data as string
+-- Internal: Encode image as BMP format (BGRA buffer is already close to BMP's BGR format)
function ImageObj:_encodeBMP()
local bmpParts = {}
- -- Calculate sizes
- local rowSize = ((self.bytesPerPixel * self.width + 3) / 4) * 4 -- Row must be multiple of 4 bytes
- rowSize = math.floor(rowSize)
+ -- BMP uses 3 bytes per pixel (BGR, no alpha)
+ local bmpBytesPerPixel = 3
+ local rowSize = math.floor((bmpBytesPerPixel * self.width + 3) / 4) * 4
local pixelDataSize = rowSize * self.height
- local fileSize = 54 + pixelDataSize -- 54 = header size
+ local fileSize = 54 + pixelDataSize
-- BMP File Header (14 bytes)
- bmpParts[#bmpParts + 1] = "BM" -- Signature
- bmpParts[#bmpParts + 1] = self:_uint32le(fileSize) -- File size
- bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Reserved
- bmpParts[#bmpParts + 1] = self:_uint32le(54) -- Pixel data offset
+ bmpParts[#bmpParts + 1] = "BM"
+ bmpParts[#bmpParts + 1] = self:_uint32le(fileSize)
+ bmpParts[#bmpParts + 1] = self:_uint32le(0)
+ bmpParts[#bmpParts + 1] = self:_uint32le(54)
-- DIB Header (BITMAPINFOHEADER - 40 bytes)
- bmpParts[#bmpParts + 1] = self:_uint32le(40) -- Header size
- bmpParts[#bmpParts + 1] = self:_uint32le(self.width) -- Width
- bmpParts[#bmpParts + 1] = self:_uint32le(self.height) -- Height (positive = bottom-up)
- bmpParts[#bmpParts + 1] = self:_uint16le(1) -- Color planes
- bmpParts[#bmpParts + 1] = self:_uint16le(self.bytesPerPixel * 8) -- Bits per pixel
- bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Compression (0 = none)
- bmpParts[#bmpParts + 1] = self:_uint32le(pixelDataSize) -- Image size
- bmpParts[#bmpParts + 1] = self:_uint32le(2835) -- X pixels per meter (72 DPI)
- bmpParts[#bmpParts + 1] = self:_uint32le(2835) -- Y pixels per meter (72 DPI)
- bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Colors in palette
- bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Important colors
-
- -- Pixel data (bottom-up, BGR or BGRA format)
- local padding = string.rep(string.char(0), rowSize - (self.width * self.bytesPerPixel))
+ bmpParts[#bmpParts + 1] = self:_uint32le(40)
+ bmpParts[#bmpParts + 1] = self:_uint32le(self.width)
+ bmpParts[#bmpParts + 1] = self:_uint32le(self.height)
+ bmpParts[#bmpParts + 1] = self:_uint16le(1)
+ bmpParts[#bmpParts + 1] = self:_uint16le(24) -- 24-bit BMP
+ bmpParts[#bmpParts + 1] = self:_uint32le(0)
+ bmpParts[#bmpParts + 1] = self:_uint32le(pixelDataSize)
+ bmpParts[#bmpParts + 1] = self:_uint32le(2835)
+ bmpParts[#bmpParts + 1] = self:_uint32le(2835)
+ bmpParts[#bmpParts + 1] = self:_uint32le(0)
+ bmpParts[#bmpParts + 1] = self:_uint32le(0)
+
+ -- Pixel data (bottom-up, BGR format)
+ local paddingLen = rowSize - self.width * bmpBytesPerPixel
+ local padding = paddingLen > 0 and string.rep(string.char(0), paddingLen) or ""
+ local width = self.width
+ local buffer = self.buffer
for y = self.height - 1, 0, -1 do -- BMP is bottom-up
- local rowData = {}
-
- for x = 0, self.width - 1 do
- local pixelIndex = y * self.width + x
- local byteOffset = pixelIndex * self.bytesPerPixel + 1
+ local rowBytes = {}
+ local rowStart = y * width * 4 + 1
- local r = string.byte(self.buffer, byteOffset)
- local g = string.byte(self.buffer, byteOffset + 1)
- local b = string.byte(self.buffer, byteOffset + 2)
-
- -- BMP uses BGR order
- rowData[#rowData + 1] = string.char(b, g, r)
-
- if self.hasAlpha then
- local a = string.byte(self.buffer, byteOffset + 3)
- rowData[#rowData + 1] = string.char(a)
- end
+ for x = 0, width - 1 do
+ local offset = rowStart + x * 4
+ -- Buffer is BGRA, BMP wants BGR
+ rowBytes[#rowBytes + 1] = string.byte(buffer, offset) -- B
+ rowBytes[#rowBytes + 1] = string.byte(buffer, offset + 1) -- G
+ rowBytes[#rowBytes + 1] = string.byte(buffer, offset + 2) -- R
+ -- Skip alpha
end
- bmpParts[#bmpParts + 1] = table.concat(rowData)
- bmpParts[#bmpParts + 1] = padding
+ bmpParts[#bmpParts + 1] = string.char(unpack(rowBytes))
+ if paddingLen > 0 then
+ bmpParts[#bmpParts + 1] = padding
+ end
end
return table.concat(bmpParts)
end
--- Internal: Load PNG file using existing decoder
--- @param path Path to PNG file
--- @return Image object or nil on error
+-- Internal: Load PNG file using existing decoder (converts to BGRA)
local function loadPNG(path)
- if not PNGLoad or not ImageGetWidth or not ImageGetHeight or not ImageGetPixel then
+ -- Get functions from _G (they may be in sandbox env)
+ local pngLoad = PNGLoad or _G.PNGLoad
+ local imgGetWidth = ImageGetWidth or _G.ImageGetWidth
+ local imgGetHeight = ImageGetHeight or _G.ImageGetHeight
+ local imgGetBufferBGRA = ImageGetBufferBGRA or _G.ImageGetBufferBGRA
+ local imgDestroy = ImageDestroy or _G.ImageDestroy
+
+ if not pngLoad or not imgGetWidth or not imgGetHeight then
return nil, "PNG decoder functions not available"
end
- -- Read file using fs or CRamdisk
local pngData, err = readFile(path)
if not pngData then
return nil, err
end
- -- Decode PNG
- local imgBuffer = PNGLoad(pngData)
+ local imgBuffer = pngLoad(pngData)
if not imgBuffer then
return nil, "Failed to decode PNG: " .. path
end
- -- Get dimensions
- local width = ImageGetWidth(imgBuffer)
- local height = ImageGetHeight(imgBuffer)
+ local width = imgGetWidth(imgBuffer)
+ local height = imgGetHeight(imgBuffer)
- -- Create Image object
local self = setmetatable({}, ImageObj)
self.width = width
self.height = height
self.hasAlpha = true
self.bytesPerPixel = 4
- -- Read all pixels from C buffer into binary string
- -- Build rows sequentially to avoid sparse table issues
- local rows = {}
- for y = 0, height - 1 do
- local rowPixels = {}
- for x = 0, width - 1 do
- local r, g, b, a = ImageGetPixel(imgBuffer, x, y)
- rowPixels[#rowPixels + 1] = string.char(r or 0, g or 0, b or 0, a or 255)
+ -- Use fast C function to get entire buffer as BGRA string
+ if imgGetBufferBGRA then
+ local buf, bufErr = imgGetBufferBGRA(imgBuffer)
+ if buf then
+ self.buffer = buf
+ else
+ if imgDestroy then imgDestroy(imgBuffer) end
+ return nil, bufErr or "Failed to get image buffer"
end
- rows[#rows + 1] = table.concat(rowPixels)
+ else
+ -- Fallback: read pixel by pixel (slow)
+ local imgGetPixel = ImageGetPixel or _G.ImageGetPixel
+ if not imgGetPixel then
+ if imgDestroy then imgDestroy(imgBuffer) end
+ return nil, "ImageGetPixel not available"
+ end
+ local rows = {}
+ for y = 0, height - 1 do
+ local rowBytes = {}
+ for x = 0, width - 1 do
+ local r, g, b, a = imgGetPixel(imgBuffer, x, y)
+ local idx = x * 4
+ rowBytes[idx + 1] = b or 0
+ rowBytes[idx + 2] = g or 0
+ rowBytes[idx + 3] = r or 0
+ rowBytes[idx + 4] = a or 255
+ end
+ rows[#rows + 1] = string.char(unpack(rowBytes))
+ end
+ self.buffer = table.concat(rows)
end
- self.buffer = table.concat(rows)
+ if imgDestroy then
+ imgDestroy(imgBuffer)
+ end
+
+ return self
+end
+
+-- Internal: Load JPEG file using existing decoder (converts to BGRA)
+local function loadJPEG(path)
+ -- Get functions from _G (they may be in sandbox env)
+ local jpegLoad = JPEGLoad or _G.JPEGLoad
+ local imgGetWidth = ImageGetWidth or _G.ImageGetWidth
+ local imgGetHeight = ImageGetHeight or _G.ImageGetHeight
+ local imgGetBufferBGRA = ImageGetBufferBGRA or _G.ImageGetBufferBGRA
+ local imgDestroy = ImageDestroy or _G.ImageDestroy
+
+ if not jpegLoad or not imgGetWidth or not imgGetHeight then
+ return nil, "JPEG decoder functions not available"
+ end
- -- Free C image buffer
- if ImageDestroy then
- ImageDestroy(imgBuffer)
+ local jpegData, err = readFile(path)
+ if not jpegData then
+ return nil, err
+ end
+
+ local imgBuffer = jpegLoad(jpegData)
+ if not imgBuffer then
+ return nil, "Failed to decode JPEG: " .. path
+ end
+
+ local width = imgGetWidth(imgBuffer)
+ local height = imgGetHeight(imgBuffer)
+
+ local self = setmetatable({}, ImageObj)
+ self.width = width
+ self.height = height
+ self.hasAlpha = false -- JPEG doesn't support alpha
+ self.bytesPerPixel = 4 -- Still use 4 bytes for screen compatibility
+
+ -- Use fast C function to get entire buffer as BGRA string
+ if imgGetBufferBGRA then
+ local buf, bufErr = imgGetBufferBGRA(imgBuffer)
+ if buf then
+ self.buffer = buf
+ else
+ if imgDestroy then imgDestroy(imgBuffer) end
+ return nil, bufErr or "Failed to get image buffer"
+ end
+ else
+ -- Fallback: read pixel by pixel (slow)
+ local imgGetPixel = ImageGetPixel or _G.ImageGetPixel
+ if not imgGetPixel then
+ if imgDestroy then imgDestroy(imgBuffer) end
+ return nil, "ImageGetPixel not available"
+ end
+ local rows = {}
+ for y = 0, height - 1 do
+ local rowBytes = {}
+ for x = 0, width - 1 do
+ local r, g, b = imgGetPixel(imgBuffer, x, y)
+ local idx = x * 4
+ rowBytes[idx + 1] = b or 0
+ rowBytes[idx + 2] = g or 0
+ rowBytes[idx + 3] = r or 0
+ rowBytes[idx + 4] = 255
+ end
+ rows[#rows + 1] = string.char(unpack(rowBytes))
+ end
+ self.buffer = table.concat(rows)
+ end
+
+ if imgDestroy then
+ imgDestroy(imgBuffer)
end
return self
end
--- Internal: Load BMP file
--- @param path Path to BMP file
--- @return Image object or nil on error
+-- Internal: Load BMP file (converts to BGRA)
local function loadBMP(path)
- -- Read file using fs or CRamdisk
local bmpData, err = readFile(path)
if not bmpData then
return nil, err
@@ -1213,14 +1219,12 @@ local function loadBMP(path)
return nil, "Invalid BMP file: too small"
end
- -- Helper: Read 16-bit little-endian
local function readUInt16LE(data, offset)
local b1 = string.byte(data, offset)
local b2 = string.byte(data, offset + 1)
return bit.bor(b1, bit.lshift(b2, 8))
end
- -- Helper: Read 32-bit little-endian
local function readUInt32LE(data, offset)
local b1 = string.byte(data, offset)
local b2 = string.byte(data, offset + 1)
@@ -1229,12 +1233,10 @@ local function loadBMP(path)
return bit.bor(b1, bit.lshift(b2, 8), bit.lshift(b3, 16), bit.lshift(b4, 24))
end
- -- Check BMP signature
if bmpData:sub(1, 2) ~= "BM" then
return nil, "Not a valid BMP file"
end
- -- Read BMP header
local pixelDataOffset = readUInt32LE(bmpData, 11)
local headerSize = readUInt32LE(bmpData, 15)
@@ -1242,13 +1244,11 @@ local function loadBMP(path)
return nil, "Unsupported BMP format (old header)"
end
- -- Read DIB header (BITMAPINFOHEADER)
local width = readUInt32LE(bmpData, 19)
local height = readUInt32LE(bmpData, 23)
local bitsPerPixel = readUInt16LE(bmpData, 29)
local compression = readUInt32LE(bmpData, 31)
- -- Validate
if compression ~= 0 then
return nil, "Compressed BMP not supported"
end
@@ -1257,54 +1257,48 @@ local function loadBMP(path)
return nil, "Only 24-bit and 32-bit BMP supported"
end
- -- Create Image object
local self = setmetatable({}, ImageObj)
self.width = width
self.height = height
self.hasAlpha = (bitsPerPixel == 32)
- self.bytesPerPixel = self.hasAlpha and 4 or 3
+ self.bytesPerPixel = 4 -- Always 4 bytes for screen compatibility
- -- Calculate row size (rows are padded to 4-byte boundary)
local srcBytesPerPixel = bitsPerPixel / 8
local rowSize = math.floor((srcBytesPerPixel * width + 3) / 4) * 4
- -- Read pixels (BMP is bottom-up, BGR/BGRA format)
- local buffer = {}
- for y = height - 1, 0, -1 do
- local rowOffset = pixelDataOffset + (height - 1 - y) * rowSize + 1
+ -- Read pixels (BMP is bottom-up, BGR format) and convert to BGRA
+ local rows = {}
+ for y = 0, height - 1 do
+ local rowPixels = {}
+ -- BMP rows are stored bottom-to-top
+ local srcY = height - 1 - y
+ local rowOffset = pixelDataOffset + srcY * rowSize + 1
for x = 0, width - 1 do
local pixelOffset = rowOffset + x * srcBytesPerPixel
- -- Read BGR(A)
local b = string.byte(bmpData, pixelOffset)
local g = string.byte(bmpData, pixelOffset + 1)
local r = string.byte(bmpData, pixelOffset + 2)
local a = 255
- if self.hasAlpha then
+ if srcBytesPerPixel == 4 then
a = string.byte(bmpData, pixelOffset + 3)
end
- -- Store as RGB(A)
- local bufferIndex = (y * width + x) * self.bytesPerPixel + 1
- buffer[bufferIndex] = string.char(r)
- buffer[bufferIndex + 1] = string.char(g)
- buffer[bufferIndex + 2] = string.char(b)
-
- if self.hasAlpha then
- buffer[bufferIndex + 3] = string.char(a)
- end
+ -- Store as BGRA
+ rowPixels[#rowPixels + 1] = string.char(b, g, r, a)
end
+ rows[#rows + 1] = table.concat(rowPixels)
end
- self.buffer = table.concat(buffer)
+ self.buffer = table.concat(rows)
return self
end
-- Open image file (auto-detect format from extension)
--- @param path Path to image file (.png or .bmp)
+-- @param path Path to image file (.png, .bmp, .jpg, .jpeg)
-- @return Image object or nil on error
function Image.open(path)
checkPermission()
@@ -1313,7 +1307,6 @@ function Image.open(path)
error("Image.open requires a file path")
end
- -- Detect format from extension
local extension = path:match("%.([^%.]+)$")
if not extension then
return nil, "Cannot determine file format (no extension)"
@@ -1325,6 +1318,8 @@ function Image.open(path)
return loadPNG(path)
elseif extension == "bmp" then
return loadBMP(path)
+ elseif extension == "jpg" or extension == "jpeg" then
+ return loadJPEG(path)
else
return nil, "Unsupported image format: " .. extension
end
diff --git a/iso_includes/os/libs/Run.lua b/iso_includes/os/libs/Run.lua
@@ -1660,6 +1660,7 @@ function run.execute(app_name, fsRoot)
sandbox_env.ImageGetWidth = _G.ImageGetWidth
sandbox_env.ImageGetHeight = _G.ImageGetHeight
sandbox_env.ImageGetPixel = _G.ImageGetPixel
+ sandbox_env.ImageGetBufferBGRA = _G.ImageGetBufferBGRA
sandbox_env.ImageDestroy = _G.ImageDestroy
-- Add partial window update function for efficient drawing (e.g., paint apps)
@@ -2102,6 +2103,14 @@ function run.execute(app_name, fsRoot)
allowed_keys.JPEGLoad = true
allowed_keys.PNGLoad = true
allowed_keys.BMPLoad = true
+ allowed_keys.ImageGetWidth = true
+ allowed_keys.ImageGetHeight = true
+ allowed_keys.ImageGetPixel = true
+ allowed_keys.ImageGetBufferBGRA = true
+ allowed_keys.ImageDraw = true
+ allowed_keys.ImageDrawScaled = true
+ allowed_keys.ImageDestroy = true
+ allowed_keys.ImageGetInfo = true
setmetatable(sandbox_env, {
__index = function(t, k)
diff --git a/kernel.c b/kernel.c
@@ -1038,6 +1038,19 @@ void usermode_function(void) {
lua_pushcfunction(L, lua_image_rotate);
lua_setglobal(L, "ImageRotate");
+ lua_pushcfunction(L, lua_image_get_width);
+ lua_setglobal(L, "ImageGetWidth");
+
+ lua_pushcfunction(L, lua_image_get_height);
+ lua_setglobal(L, "ImageGetHeight");
+
+ lua_pushcfunction(L, lua_image_get_pixel);
+ lua_setglobal(L, "ImageGetPixel");
+
+ extern int lua_image_get_buffer_bgra(lua_State* L);
+ lua_pushcfunction(L, lua_image_get_buffer_bgra);
+ lua_setglobal(L, "ImageGetBufferBGRA");
+
/* Test simple Lua execution first */
terminal_writestring("Testing simple Lua expression...\n");
const char* simple_test = "osprint('Simple test works!\\n')";
diff --git a/libc.c b/libc.c
@@ -131,10 +131,31 @@ int toupper(int c) {
/* ========== Memory Allocation ========== */
-/* Simple bump allocator - NOT suitable for production */
-#define HEAP_SIZE (256 * 1024 * 1024) /* 256 MB heap for LuaJIT + ramdisk + apps */
+/*
+ * Free-list allocator with block coalescing
+ * Each block has a header with size and free/used flag
+ * Free blocks are linked in a free list for reuse
+ */
+
+#define HEAP_SIZE (384 * 1024 * 1024) /* 384 MB heap for LuaJIT + ramdisk + apps */
static uint8_t heap[HEAP_SIZE] __attribute__((aligned(4096)));
-static size_t heap_pos = 0;
+
+/* Block header - stored before each allocation */
+typedef struct block_header {
+ size_t size; /* Size of user data (not including header) */
+ struct block_header *next_free; /* Next free block (only valid if free) */
+ uint32_t magic; /* Magic number for validation */
+ uint32_t is_free; /* 1 if free, 0 if allocated */
+} block_header_t;
+
+#define BLOCK_MAGIC 0xDEADBEEF
+#define HEADER_SIZE ((sizeof(block_header_t) + 15) & ~15) /* Aligned header size */
+#define MIN_BLOCK_SIZE 32 /* Minimum user data size */
+
+/* Free list head */
+static block_header_t *free_list = NULL;
+static int heap_initialized = 0;
+static size_t heap_end = 0; /* Current end of used heap */
extern void terminal_writestring(const char *);
extern void terminal_putchar(char);
@@ -154,50 +175,147 @@ static void print_hex(unsigned long val) {
while (i > 0) terminal_putchar(buf[--i]);
}
+/* Initialize heap if needed */
+static void heap_init(void) {
+ if (heap_initialized) return;
+ heap_initialized = 1;
+ heap_end = 0;
+ free_list = NULL;
+}
+
+/* Get header from user pointer */
+static inline block_header_t *get_header(void *ptr) {
+ return (block_header_t *)((uint8_t *)ptr - HEADER_SIZE);
+}
+
+/* Get user pointer from header */
+static inline void *get_user_ptr(block_header_t *header) {
+ return (void *)((uint8_t *)header + HEADER_SIZE);
+}
+
+/* Find a free block that fits, using first-fit strategy */
+static block_header_t *find_free_block(size_t size) {
+ block_header_t *curr = free_list;
+ block_header_t *prev = NULL;
+
+ while (curr) {
+ if (curr->size >= size) {
+ /* Remove from free list */
+ if (prev) {
+ prev->next_free = curr->next_free;
+ } else {
+ free_list = curr->next_free;
+ }
+ curr->next_free = NULL;
+ curr->is_free = 0;
+ return curr;
+ }
+ prev = curr;
+ curr = curr->next_free;
+ }
+ return NULL;
+}
+
+/* Allocate a new block from the heap end */
+static block_header_t *alloc_new_block(size_t size) {
+ size_t total_size = HEADER_SIZE + size;
+
+ if (heap_end + total_size > HEAP_SIZE) {
+ return NULL; /* Out of memory */
+ }
+
+ block_header_t *header = (block_header_t *)&heap[heap_end];
+ heap_end += total_size;
+
+ header->size = size;
+ header->next_free = NULL;
+ header->magic = BLOCK_MAGIC;
+ header->is_free = 0;
+
+ return header;
+}
+
void *malloc(size_t size) {
if (size == 0) return NULL;
- /* Align to 16 bytes */
+
+ heap_init();
+
+ /* Align size to 16 bytes, with minimum */
size = (size + 15) & ~15;
- if (heap_pos + size > HEAP_SIZE) {
- terminal_writestring("MALLOC OOM: size=");
- print_hex(size);
- terminal_writestring(" heap_pos=");
- print_hex(heap_pos);
- terminal_writestring("\n");
- return NULL; /* Out of memory */
+ if (size < MIN_BLOCK_SIZE) size = MIN_BLOCK_SIZE;
+
+ /* Try to find a free block first */
+ block_header_t *header = find_free_block(size);
+
+ if (!header) {
+ /* No suitable free block, allocate new */
+ header = alloc_new_block(size);
+ if (!header) {
+ terminal_writestring("MALLOC OOM: size=");
+ print_hex(size);
+ terminal_writestring(" heap_end=");
+ print_hex(heap_end);
+ terminal_writestring("\n");
+ return NULL;
+ }
}
- void *ptr = &heap[heap_pos];
- heap_pos += size;
- memset(ptr, 0, size); /* Zero initialize */
-
- /* Debug: show large allocations */
- // Disabled to reduce boot noise
- // if (size > 4096) {
- // terminal_writestring("MALLOC: size=");
- // print_hex(size);
- // terminal_writestring(" ptr=");
- // print_hex((unsigned long)ptr);
- // terminal_writestring("\n");
- // }
+ void *ptr = get_user_ptr(header);
+ memset(ptr, 0, header->size); /* Zero initialize */
return ptr;
}
void free(void *ptr) {
- /* Simple allocator doesn't support free */
- (void)ptr;
+ if (!ptr) return;
+
+ block_header_t *header = get_header(ptr);
+
+ /* Validate the block */
+ if (header->magic != BLOCK_MAGIC) {
+ terminal_writestring("FREE: invalid pointer (bad magic)\n");
+ return;
+ }
+
+ if (header->is_free) {
+ terminal_writestring("FREE: double free detected\n");
+ return;
+ }
+
+ /* Mark as free and add to free list (front insertion) */
+ header->is_free = 1;
+ header->next_free = free_list;
+ free_list = header;
}
void *realloc(void *ptr, size_t size) {
- /* Simple implementation - always allocate new */
+ if (!ptr) return malloc(size);
+
if (size == 0) {
free(ptr);
return NULL;
}
+
+ block_header_t *header = get_header(ptr);
+
+ /* Validate */
+ if (header->magic != BLOCK_MAGIC) {
+ terminal_writestring("REALLOC: invalid pointer\n");
+ return NULL;
+ }
+
+ /* If current block is large enough, reuse it */
+ size_t aligned_size = (size + 15) & ~15;
+ if (aligned_size < MIN_BLOCK_SIZE) aligned_size = MIN_BLOCK_SIZE;
+
+ if (header->size >= aligned_size) {
+ return ptr; /* Current block is big enough */
+ }
+
+ /* Need to allocate new block */
void *new_ptr = malloc(size);
- if (new_ptr && ptr) {
- /* Copy old data - we don't know the old size, so just copy size bytes */
- memcpy(new_ptr, ptr, size);
+ if (new_ptr) {
+ memcpy(new_ptr, ptr, header->size); /* Copy old data */
+ free(ptr);
}
return new_ptr;
}
@@ -911,7 +1029,7 @@ int close(int fd) {
return 0;
}
-/* mmap - memory map stub (just allocates from heap) */
+/* mmap - memory map stub (uses our allocator with block headers) */
/* All memory is executable in bare metal x86, so PROT_EXEC works automatically */
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) {
(void)addr; (void)fd; (void)offset;
@@ -934,26 +1052,22 @@ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
/* Align to page boundary (4KB) */
size_t aligned_length = (length + 4095) & ~4095;
- if (heap_pos + aligned_length > HEAP_SIZE) {
+ /* Use malloc which now supports free */
+ void *ptr = malloc(aligned_length);
+ if (!ptr) {
terminal_writestring("MMAP OOM: need=");
print_hex(aligned_length);
- terminal_writestring(" used=");
- print_hex(heap_pos);
- terminal_writestring(" total=");
- print_hex(HEAP_SIZE);
+ terminal_writestring(" heap_end=");
+ print_hex(heap_end);
terminal_writestring("\n");
errno = 12; /* ENOMEM */
return (void *)-1;
}
- void *ptr = &heap[heap_pos];
- heap_pos += aligned_length;
- memset(ptr, 0, aligned_length);
-
terminal_writestring("MMAP OK: ptr=");
print_hex((unsigned long)ptr);
terminal_writestring(" used=");
- print_hex(heap_pos);
+ print_hex(heap_end);
terminal_writestring("/");
print_hex(HEAP_SIZE);
terminal_writestring("\n");
@@ -967,7 +1081,11 @@ void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off_t offse
}
int munmap(void *addr, size_t length) {
- (void)addr; (void)length;
+ (void)length;
+ /* Actually free the memory now */
+ if (addr) {
+ free(addr);
+ }
return 0;
}
@@ -1113,7 +1231,7 @@ double modf(double x, double *iptr) {
return x - int_part;
}
-/* Memory remapping - allocate new and copy */
+/* Memory remapping - allocate new and copy, free old */
void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...) {
(void)flags;
@@ -1125,24 +1243,19 @@ void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...
/* Align to page boundary */
new_size = (new_size + 4095) & ~4095;
- if (heap_pos + new_size > HEAP_SIZE) {
+ /* Allocate new memory using our allocator */
+ void *new_ptr = malloc(new_size);
+ if (!new_ptr) {
errno = 12; /* ENOMEM */
return (void *)-1;
}
- /* Allocate new memory */
- void *new_ptr = &heap[heap_pos];
- heap_pos += new_size;
-
/* Copy old data */
if (old_address && old_size > 0) {
size_t copy_size = old_size < new_size ? old_size : new_size;
memcpy(new_ptr, old_address, copy_size);
- }
-
- /* Zero remaining */
- if (new_size > old_size) {
- memset((char*)new_ptr + old_size, 0, new_size - old_size);
+ /* Free the old memory */
+ free(old_address);
}
return new_ptr;
diff --git a/luajit_init.c b/luajit_init.c
@@ -1201,6 +1201,10 @@ __attribute__((naked)) void usermode_main(void) {
lua_pushcfunction(L, lua_image_get_pixel);
lua_setglobal(L, "ImageGetPixel");
+ extern int lua_image_get_buffer_bgra(lua_State* L);
+ lua_pushcfunction(L, lua_image_get_buffer_bgra);
+ lua_setglobal(L, "ImageGetBufferBGRA");
+
/* Register compression functions */
lua_pushcfunction(L, lua_deflate_compress);
lua_setglobal(L, "DeflateCompress");