Skip to content

Commit 5905423

Browse files
committed
feat(demos): rewrote ppm loading function
1 parent f8b1e2b commit 5905423

File tree

3 files changed

+213
-52
lines changed

3 files changed

+213
-52
lines changed

demos/image.lua

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,35 @@
11
local fenster = require('fenster')
22

3-
---Load an image from a PPM file
4-
---@param path string
5-
---@return integer[]
6-
---@return integer
7-
---@return integer
8-
local function load_image(path)
9-
local image = assert(io.open(path, 'rb'))
10-
11-
local image_type = image:read(2)
12-
assert(image_type == 'P6', 'Invalid image type: ' .. tostring(image_type))
13-
assert(image:read(1), 'Invalid image header') -- Whitespace
14-
local image_width = image:read('*number')
15-
assert(image_width, 'Invalid image width: ' .. tostring(image_width))
16-
assert(image:read(1), 'Invalid image header') -- Whitespace
17-
local image_height = image:read('*number')
18-
assert(image_height, 'Invalid image height: ' .. tostring(image_height))
19-
assert(image:read(1), 'Invalid image header') -- Whitespace
20-
local image_max_color = image:read('*number')
21-
assert(
22-
image_max_color == 255,
23-
'Invalid image maximum color: ' .. tostring(image_max_color)
24-
)
25-
assert(image:read(1), 'Invalid image header') -- Whitespace
26-
27-
local image_buffer = {} ---@type integer[]
28-
while true do
29-
local r_raw = image:read(1)
30-
local g_raw = image:read(1)
31-
local b_raw = image:read(1)
32-
if not r_raw or not g_raw or not b_raw then
33-
break
34-
end
35-
36-
local r = string.byte(r_raw)
37-
local g = string.byte(g_raw)
38-
local b = string.byte(b_raw)
39-
image_buffer[#image_buffer + 1] = fenster.rgb(r, g, b)
40-
end
3+
-- Hack to get the current script directory
4+
local dirname = './' .. (debug.getinfo(1, 'S').source:match('^@?(.*[/\\])') or '') ---@type string
5+
-- Add the project root directory to the package path
6+
package.path = dirname .. '../?.lua;' .. package.path
417

42-
return image_buffer, image_width, image_height
43-
end
8+
local ppm = require('demos.lib.ppm')
449

45-
---Draw an image from load_image()
10+
---Draw an image from a buffer.
4611
---@param window window*
4712
---@param x integer
4813
---@param y integer
49-
---@param image_buffer integer[]
14+
---@param image_pixels integer[][]
5015
---@param image_width integer
5116
---@param image_height integer
52-
local function draw_image(window, x, y, image_buffer, image_width, image_height)
53-
local ix_end = image_width - 1
54-
for iy = 0, image_height - 1 do
55-
local dy = y + iy
56-
local iy_index = iy * image_width + 1
57-
for ix = 0, ix_end do
58-
window:set(x + ix, dy, image_buffer[iy_index + ix])
17+
local function draw_image(window, x, y, image_pixels, image_width, image_height)
18+
for iy = 1, image_height do
19+
local dy = y + (iy - 1)
20+
for ix = 1, image_width do
21+
window:set(x + (ix - 1), dy, image_pixels[iy][ix])
5922
end
6023
end
6124
end
6225

6326
-- Load the image
64-
local dirname = './' .. (debug.getinfo(1, 'S').source:match('^@?(.*[/\\])') or '') ---@type string
6527
local image_path = dirname .. 'assets/uv.ppm'
66-
local image_buffer, image_width, image_height = load_image(image_path)
28+
local image_pixels, image_width, image_height, image_err = ppm.load(image_path)
29+
if not image_pixels or not image_width or not image_height then
30+
print('Failed to load image: ' .. tostring(image_err))
31+
return
32+
end
6733

6834
-- Open a window
6935
local window = fenster.open(
@@ -77,7 +43,7 @@ draw_image(
7743
window,
7844
0,
7945
0,
80-
image_buffer,
46+
image_pixels,
8147
image_width,
8248
image_height
8349
)

demos/lib/ppm.lua

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
local tostring = tostring
2+
local io = io
3+
local string = string
4+
local math = math
5+
local fenster = require('fenster')
6+
7+
local ppm = {}
8+
9+
---Load a PPM file.
10+
---@param path string
11+
---@return integer[][]|nil, integer|nil, integer|nil, string|nil
12+
---@nodiscard
13+
function ppm.load(path)
14+
local ppm_file, ppm_file_err = io.open(path, 'rb')
15+
if not ppm_file then
16+
return nil, nil, nil, 'Failed to open PPM file: ' .. tostring(ppm_file_err)
17+
end
18+
19+
---Read the next line and trim whitespace.
20+
---@return string|nil, string|nil
21+
---@nodiscard
22+
local function read_line()
23+
local line = ppm_file:read('*line') ---@type string|nil
24+
if not line then
25+
return nil, 'Failed to read line, possibly early EOF'
26+
end
27+
line = string.match(line, '^%s*(.-)%s*$') ---@type string|nil
28+
if not line then
29+
return nil, 'Failed to trim line'
30+
end
31+
return line, nil
32+
end
33+
34+
---Parse a string as an integer, while making sure it is positive, not NaN/Inf and not a float.
35+
---@param raw string|nil
36+
---@param name string
37+
---@return integer|nil, string|nil
38+
---@nodiscard
39+
local function parse_int(raw, name)
40+
local number = tonumber(raw)
41+
if not number or number <= 0 or number ~= number
42+
or number == math.huge or number == -math.huge or number ~= math.floor(number) then
43+
local number_str = tostring(number)
44+
if #number_str > 20 then
45+
number_str = string.sub(number_str, 1, 20) .. '...'
46+
end
47+
return nil, 'Invalid ' .. tostring(name) .. ': ' .. number_str
48+
end
49+
return number --[[ @as integer ]], nil
50+
end
51+
52+
--[[ Read header ]]
53+
local line, line_err = read_line()
54+
if not line then return nil, nil, nil, line_err end
55+
-- Read the magic number
56+
local magic_number ---@type string|nil
57+
magic_number, line = string.match(line, '^(%S+)%s*(.*)') ---@type string|nil
58+
if magic_number ~= 'P6' then
59+
if #magic_number > 20 then
60+
magic_number = string.sub(magic_number, 1, 20) .. '...'
61+
end
62+
return nil, nil, nil, 'Unsupported magic number, expected P6 (PPM Binary/Raw): ' .. magic_number
63+
end
64+
-- Skip comments, if line has ended
65+
if not line or line == '' then
66+
repeat
67+
line, line_err = read_line()
68+
if not line then return nil, nil, nil, line_err end
69+
until string.sub(line, 1, 1) ~= '#'
70+
end
71+
-- Read the width
72+
local raw_width ---@type string|nil
73+
raw_width, line = string.match(line, '^(%d+)%s*(.*)') ---@type string|nil, string|nil
74+
local width, width_err = parse_int(raw_width, 'width')
75+
if not width then return nil, nil, nil, width_err end
76+
-- Skip comments, if line has ended
77+
if not line or line == '' then
78+
repeat
79+
line, line_err = read_line()
80+
if not line then return nil, nil, nil, line_err end
81+
until string.sub(line, 1, 1) ~= '#'
82+
end
83+
-- Read the height
84+
local raw_height ---@type string|nil
85+
raw_height, line = string.match(line, '^(%d+)%s*(.*)') ---@type string|nil, string|nil
86+
local height, height_err = parse_int(raw_height, 'height')
87+
if not height then return nil, nil, nil, height_err end
88+
-- Skip comments, if line has ended
89+
if not line or line == '' then
90+
repeat
91+
line, line_err = read_line()
92+
if not line then return nil, nil, nil, line_err end
93+
until string.sub(line, 1, 1) ~= '#'
94+
end
95+
-- Read the maximum color
96+
local raw_maximum_color = string.match(line, '^(%d+)') ---@type string|nil
97+
local maximum_color, maximum_color_err = parse_int(raw_maximum_color, 'maximum color')
98+
if not maximum_color then return nil, nil, nil, maximum_color_err end
99+
-- TODO: Support up to 65535
100+
if maximum_color > 255 then
101+
return nil, nil, nil, 'Unsupported maximum color, expected below or equal 255: ' .. tostring(maximum_color)
102+
end
103+
104+
--[[ Read pixel data ]]
105+
local pixels = {} ---@type integer[][]
106+
for y = 1, height do
107+
pixels[y] = {} ---@type integer[]
108+
for x = 1, width do
109+
local r_raw = ppm_file:read(1) ---@type string|nil
110+
local g_raw = ppm_file:read(1) ---@type string|nil
111+
local b_raw = ppm_file:read(1) ---@type string|nil
112+
if not r_raw or not g_raw or not b_raw then
113+
return nil, nil, nil, 'Failed to read pixel data, possibly early EOF'
114+
end
115+
116+
local r = string.byte(r_raw)
117+
if r > maximum_color then
118+
return nil, nil, nil, 'Invalid pixel data, red value out of range: ' .. tostring(r)
119+
end
120+
local g = string.byte(g_raw)
121+
if g > maximum_color then
122+
return nil, nil, nil, 'Invalid pixel data, green value out of range: ' .. tostring(g)
123+
end
124+
local b = string.byte(b_raw)
125+
if b > maximum_color then
126+
return nil, nil, nil, 'Invalid pixel data, blue value out of range: ' .. tostring(b)
127+
end
128+
if maximum_color ~= 255 then
129+
r = math.floor(r * 255 / maximum_color)
130+
g = math.floor(g * 255 / maximum_color)
131+
b = math.floor(b * 255 / maximum_color)
132+
end
133+
pixels[y][x] = fenster.rgb(r, g, b)
134+
end
135+
end
136+
137+
ppm_file:close()
138+
return pixels, width, height, nil
139+
end
140+
141+
return ppm

demos/lib/ppmold.lua

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
local assert = assert
2+
local tostring = tostring
3+
local io = io
4+
local string = string
5+
local table = table
6+
local fenster = require('fenster')
7+
8+
local ppm = {}
9+
10+
---Load a PPM image.
11+
---@param path string
12+
---@return integer[]
13+
---@return integer
14+
---@return integer
15+
---@nodiscard
16+
function ppm.load(path)
17+
local image = assert(io.open(path, 'rb'))
18+
19+
local image_type = image:read(2)
20+
assert(image_type == 'P6', 'Invalid image type: ' .. tostring(image_type))
21+
assert(image:read(1), 'Invalid image header') -- Whitespace
22+
local image_width = image:read('*number')
23+
assert(image_width, 'Invalid image width: ' .. tostring(image_width))
24+
assert(image:read(1), 'Invalid image header') -- Whitespace
25+
local image_height = image:read('*number')
26+
assert(image_height, 'Invalid image height: ' .. tostring(image_height))
27+
assert(image:read(1), 'Invalid image header') -- Whitespace
28+
local image_max_color = image:read('*number')
29+
assert(
30+
image_max_color == 255,
31+
'Invalid image maximum color: ' .. tostring(image_max_color)
32+
)
33+
assert(image:read(1), 'Invalid image header') -- Whitespace
34+
35+
local image_buffer = {} ---@type integer[]
36+
while true do
37+
local r_raw = image:read(1)
38+
local g_raw = image:read(1)
39+
local b_raw = image:read(1)
40+
if not r_raw or not g_raw or not b_raw then
41+
break
42+
end
43+
44+
local r = string.byte(r_raw)
45+
local g = string.byte(g_raw)
46+
local b = string.byte(b_raw)
47+
table.insert(image_buffer, fenster.rgb(r, g, b))
48+
end
49+
50+
image:close()
51+
return image_buffer, image_width, image_height
52+
end
53+
54+
return ppm

0 commit comments

Comments
 (0)