178 lines
5.5 KiB
Lua
178 lines
5.5 KiB
Lua
--[[
|
|
NinePatchParser.lua - 9-patch PNG parser for FlexLove
|
|
Parses Android-style 9-patch images to extract stretch regions and content padding
|
|
]]
|
|
|
|
-- ====================
|
|
-- Error Handling Utilities
|
|
-- ====================
|
|
|
|
--- Standardized error message formatter
|
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
---@param message string -- Error message
|
|
---@return string -- Formatted error message
|
|
local function formatError(module, message)
|
|
return string.format("[FlexLove.%s] %s", module, message)
|
|
end
|
|
|
|
-- ====================
|
|
-- Dependencies
|
|
-- ====================
|
|
|
|
local ImageDataReader = require((...):match("(.-)[^%.]+$") .. "ImageDataReader")
|
|
|
|
-- ====================
|
|
-- NinePatchParser
|
|
-- ====================
|
|
|
|
local NinePatchParser = {}
|
|
|
|
--- Find all continuous runs of black pixels in a pixel array
|
|
---@param pixels table -- Array of {r, g, b, a} pixel values
|
|
---@return table -- Array of {start, end} pairs (1-based indices, inclusive)
|
|
local function findBlackPixelRuns(pixels)
|
|
local runs = {}
|
|
local inRun = false
|
|
local runStart = nil
|
|
|
|
for i = 1, #pixels do
|
|
local pixel = pixels[i]
|
|
local isBlack = ImageDataReader.isBlackPixel(pixel.r, pixel.g, pixel.b, pixel.a)
|
|
|
|
if isBlack and not inRun then
|
|
-- Start of a new run
|
|
inRun = true
|
|
runStart = i
|
|
elseif not isBlack and inRun then
|
|
-- End of current run
|
|
table.insert(runs, { start = runStart, ["end"] = i - 1 })
|
|
inRun = false
|
|
runStart = nil
|
|
end
|
|
end
|
|
|
|
-- Handle case where run extends to end of array
|
|
if inRun then
|
|
table.insert(runs, { start = runStart, ["end"] = #pixels })
|
|
end
|
|
|
|
return runs
|
|
end
|
|
|
|
--- Parse a 9-patch PNG image to extract stretch regions and content padding
|
|
---@param imagePath string -- Path to the 9-patch image file
|
|
---@return table|nil, string|nil -- Returns {insets, stretchX, stretchY} or nil, error message
|
|
function NinePatchParser.parse(imagePath)
|
|
if not imagePath then
|
|
return nil, "Image path cannot be nil"
|
|
end
|
|
|
|
local success, imageData = pcall(function()
|
|
return ImageDataReader.loadImageData(imagePath)
|
|
end)
|
|
|
|
if not success then
|
|
return nil, "Failed to load image data: " .. tostring(imageData)
|
|
end
|
|
|
|
local width = imageData:getWidth()
|
|
local height = imageData:getHeight()
|
|
|
|
-- Validate minimum size (must be at least 3x3 with 1px border)
|
|
if width < 3 or height < 3 then
|
|
return nil, string.format("Invalid 9-patch dimensions: %dx%d (minimum 3x3)", width, height)
|
|
end
|
|
|
|
-- Extract border pixels (0-based indexing, but we convert to 1-based for processing)
|
|
local topBorder = ImageDataReader.getRow(imageData, 0)
|
|
local leftBorder = ImageDataReader.getColumn(imageData, 0)
|
|
local bottomBorder = ImageDataReader.getRow(imageData, height - 1)
|
|
local rightBorder = ImageDataReader.getColumn(imageData, width - 1)
|
|
|
|
-- Remove corner pixels from borders (they're not part of the stretch/content markers)
|
|
-- Top and bottom borders: remove first and last pixel
|
|
local topStretchPixels = {}
|
|
local bottomContentPixels = {}
|
|
for i = 2, #topBorder - 1 do
|
|
table.insert(topStretchPixels, topBorder[i])
|
|
end
|
|
for i = 2, #bottomBorder - 1 do
|
|
table.insert(bottomContentPixels, bottomBorder[i])
|
|
end
|
|
|
|
-- Left and right borders: remove first and last pixel
|
|
local leftStretchPixels = {}
|
|
local rightContentPixels = {}
|
|
for i = 2, #leftBorder - 1 do
|
|
table.insert(leftStretchPixels, leftBorder[i])
|
|
end
|
|
for i = 2, #rightBorder - 1 do
|
|
table.insert(rightContentPixels, rightBorder[i])
|
|
end
|
|
|
|
-- Find stretch regions (top and left borders)
|
|
local stretchX = findBlackPixelRuns(topStretchPixels)
|
|
local stretchY = findBlackPixelRuns(leftStretchPixels)
|
|
|
|
-- Find content padding regions (bottom and right borders)
|
|
local contentX = findBlackPixelRuns(bottomContentPixels)
|
|
local contentY = findBlackPixelRuns(rightContentPixels)
|
|
|
|
-- Validate that we have at least one stretch region
|
|
if #stretchX == 0 or #stretchY == 0 then
|
|
return nil, "No stretch regions found (top or left border has no black pixels)"
|
|
end
|
|
|
|
-- Calculate stretch insets from stretch regions (top/left guides)
|
|
-- Use the first stretch region's start and last stretch region's end
|
|
local firstStretchX = stretchX[1]
|
|
local lastStretchX = stretchX[#stretchX]
|
|
local firstStretchY = stretchY[1]
|
|
local lastStretchY = stretchY[#stretchY]
|
|
|
|
-- Stretch insets define the 9-patch regions
|
|
local stretchLeft = firstStretchX.start
|
|
local stretchRight = #topStretchPixels - lastStretchX["end"]
|
|
local stretchTop = firstStretchY.start
|
|
local stretchBottom = #leftStretchPixels - lastStretchY["end"]
|
|
|
|
-- Calculate content padding from content guides (bottom/right guides)
|
|
-- If content padding is defined, use it; otherwise use stretch regions
|
|
local contentLeft, contentRight, contentTop, contentBottom
|
|
|
|
if #contentX > 0 then
|
|
contentLeft = contentX[1].start
|
|
contentRight = #topStretchPixels - contentX[#contentX]["end"]
|
|
else
|
|
contentLeft = stretchLeft
|
|
contentRight = stretchRight
|
|
end
|
|
|
|
if #contentY > 0 then
|
|
contentTop = contentY[1].start
|
|
contentBottom = #leftStretchPixels - contentY[#contentY]["end"]
|
|
else
|
|
contentTop = stretchTop
|
|
contentBottom = stretchBottom
|
|
end
|
|
|
|
return {
|
|
insets = {
|
|
left = stretchLeft,
|
|
top = stretchTop,
|
|
right = stretchRight,
|
|
bottom = stretchBottom,
|
|
},
|
|
contentPadding = {
|
|
left = contentLeft,
|
|
top = contentTop,
|
|
right = contentRight,
|
|
bottom = contentBottom,
|
|
},
|
|
stretchX = stretchX,
|
|
stretchY = stretchY,
|
|
}
|
|
end
|
|
|
|
return NinePatchParser
|