fix_xpm.lua 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/lua
  2. -------------------------------------------------------------------------------
  3. -- Name: fix_xpm.lua
  4. -- Purpose: Fix XPM files for use in Ribbon sample
  5. -- Author: Peter Cawley
  6. -- Modified by:
  7. -- Created: 2009-07-06
  8. -- Copyright: (C) Copyright 2009, Peter Cawley
  9. -- Licence: wxWindows Library Licence
  10. -------------------------------------------------------------------------------
  11. -- My preferred image editor (Paint Shop Pro 9) spits out XPM files, but with
  12. -- some deficiencies:
  13. -- 1) Specifies a 256 colour palette, even when less than 256 colours are used
  14. -- 2) Transparency is replaced by a non-transparent colour
  15. -- 3) Does not name the C array appropriately
  16. -- 4) Array and strings not marked const
  17. assert(_VERSION == "Lua 5.1", "Lua 5.1 is required")
  18. local lpeg = require "lpeg"
  19. -- Parse command line
  20. local args = {...}
  21. local filename = assert(...,"Expected filename as first command line argument")
  22. local arg_transparent
  23. local arg_name
  24. local arg_out
  25. for i = 2, select('#', ...) do
  26. local arg = args[i]
  27. if arg == "/?" or arg == "-?" or arg == "--help" then
  28. print("Usage: filename [transparent=<colour>|(x,y)] [name=<array_name>] "..
  29. "[out=<filename>]")
  30. print("In addition to the transparent colour and name changes, the "..
  31. "palette will be also be optimised")
  32. print "Examples:"
  33. print(" in.xpm transparent=Gray100 -- Modifies in.xpm, replacing "..
  34. "Gray100 with transparent")
  35. print(" in.xpm transparent=(0,0) -- Modifies in.xpm, replacing "..
  36. "whichever colour is at (0,0) with transparent")
  37. print(" in.xpm name=out_xpm out=out.xpm -- Copies in.xpm to out.xpm, "..
  38. "and changes the array name to out_xpm")
  39. return
  40. end
  41. arg_transparent = arg:match"transparent=(.*)" or arg_transparent
  42. arg_name = arg:match"name=(.*)" or arg_name
  43. arg_out = arg:match"out=(.*)" or arg_out
  44. end
  45. -- XPM parsing
  46. local grammars = {}
  47. do
  48. local C, P, R, S, V = lpeg.C, lpeg.P, lpeg.R, lpeg.S, lpeg.V
  49. local Ct = lpeg.Ct
  50. local comment = P"/*" * (1 - P"*/") ^ 0 * P"*/"
  51. local ws = (S" \r\n\t" + comment) ^ 0
  52. local function tokens(...)
  53. local result = ws
  54. for i, arg in ipairs{...} do
  55. if type(arg) == "table" then
  56. arg = P(arg[1]) ^ -1
  57. end
  58. result = result * P(arg) * ws
  59. end
  60. return result
  61. end
  62. grammars.file = P { "xpm";
  63. xpm = P"/* XPM */" * ws *
  64. tokens("static",{"const"},"char","*",{"const"}) * V"name" *
  65. tokens("[","]","=","{") * V"lines",
  66. name = C(R("az","AZ","__") * R("az","AZ","09","__") ^ 0),
  67. lines = Ct(V"line" ^ 1),
  68. line = ws * P'"' * C((1 - P'"') ^ 0) * P'"' * (tokens"," + V"eof"),
  69. eof = tokens("}",";") * P(1) ^ 0,
  70. }
  71. grammars.values = P { "values";
  72. values = Ct(V"value" * (S" \r\n\t" ^ 1 * V"value") ^ 3),
  73. value = C(R"09" ^ 1) / tonumber,
  74. }
  75. function make_remaining_grammars(cpp)
  76. local char = R"\32\126" - S[['"\]] -- Most of lower ASCII
  77. local colour = char
  78. for i = 2, cpp do
  79. colour = colour * char
  80. end
  81. grammars.colour = P { "line";
  82. line = C(colour) * Ct(Ct(ws * V"key" * ws * V"col") ^ 1),
  83. key = C(P"g4" + S"msgc"),
  84. col = V"name" + V"hex",
  85. name = C(R("az","AZ","__") * R("az","AZ","09","__") ^ 0),
  86. hex = C(P"#" * R("09","af","AF") ^ 3),
  87. }
  88. grammars.pixels = P { "line";
  89. line = Ct(C(colour) ^ 1),
  90. }
  91. end
  92. end
  93. -- Load file
  94. local file = assert(io.open(filename,"rt"))
  95. local filedata = file:read"*a"
  96. file:close()
  97. local xpm = {}
  98. xpm.name, xpm.lines = grammars.file:match(filedata)
  99. local values_table = assert(grammars.values:match(xpm.lines[1]))
  100. xpm.width, xpm.height, xpm.ncolours, xpm.cpp = unpack(values_table)
  101. make_remaining_grammars(xpm.cpp)
  102. xpm.colours = {}
  103. xpm.colours_full = {}
  104. for i = 1, xpm.ncolours do
  105. local name, data = grammars.colour:match(xpm.lines[1 + i])
  106. local colour = ""
  107. for _, datum in ipairs(data) do
  108. if datum[1] == "c" then
  109. colour = datum[2]
  110. break
  111. end
  112. end
  113. assert(colour, "No colour data for " .. name)
  114. xpm.colours[name] = colour
  115. xpm.colours_full[i] = {name = name, unpack(data)}
  116. end
  117. xpm.pixels = {}
  118. for y = 1, xpm.height do
  119. xpm.pixels[y] = grammars.pixels:match(xpm.lines[1 + xpm.ncolours + y])
  120. if not xpm.pixels[y] or #xpm.pixels[y] ~= xpm.width then
  121. error("Line " .. y .. " is invalid")
  122. end
  123. end
  124. -- Fix palette
  125. repeat
  126. local n_colours_used = 0
  127. local colours_used = setmetatable({}, {__newindex = function(t, k, v)
  128. n_colours_used = n_colours_used + 1
  129. rawset(t, k, v)
  130. end})
  131. for y = 1, xpm.height do
  132. for x = 1, xpm.width do
  133. colours_used[xpm.pixels[y][x]] = true
  134. end
  135. end
  136. if n_colours_used == xpm.ncolours then
  137. break
  138. end
  139. local chars =" .abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567"
  140. local cpp = (n_colours_used > #chars) and 2 or 1
  141. local nalloc = 0
  142. local colour_map = setmetatable({}, {__index = function(t, k)
  143. nalloc = nalloc + 1
  144. local v
  145. if cpp == 1 then
  146. v = chars:sub(nalloc, nalloc)
  147. else
  148. local a, b = math.floor(nalloc / #chars) + 1, (nalloc % #chars) + 1
  149. v = chars:sub(a, a) .. chars:sub(b, b)
  150. end
  151. t[k] = v
  152. return v
  153. end})
  154. for y = 1, xpm.height do
  155. for x = 1, xpm.width do
  156. xpm.pixels[y][x] = colour_map[xpm.pixels[y][x]]
  157. end
  158. end
  159. local new_colours_full = {}
  160. for i, colour in ipairs(xpm.colours_full) do
  161. if colours_used[colour.name] then
  162. colour.name = colour_map[colour.name]
  163. new_colours_full[#new_colours_full + 1] = colour
  164. end
  165. end
  166. xpm.colours_full = new_colours_full
  167. local new_colours = {}
  168. for name, value in pairs(xpm.colours) do
  169. if colours_used[name] then
  170. new_colours[colour_map[name]] = value
  171. end
  172. end
  173. xpm.colours = new_colours
  174. xpm.cpp = cpp
  175. xpm.ncolours = nalloc
  176. until true
  177. -- Fix transparency
  178. if arg_transparent then
  179. local name
  180. local x, y = arg_transparent:match"[(](%d+),(%d+)[)]"
  181. if x and y then
  182. name = xpm.pixels[y + 1][x + 1]
  183. else
  184. for n, c in pairs(xpm.colours) do
  185. if c == arg_transparent then
  186. name = n
  187. break
  188. end
  189. end
  190. end
  191. if not name then
  192. error("Cannot convert " .. arg_transparent .. " to transparent as the "..
  193. "colour is not present in the file")
  194. end
  195. xpm.colours[name] = "None"
  196. for i, colour in ipairs(xpm.colours_full) do
  197. if colour.name == name then
  198. for i, data in ipairs(colour) do
  199. if data[1] == "c" then
  200. data[2] = "None"
  201. break
  202. end
  203. end
  204. break
  205. end
  206. end
  207. end
  208. -- Fix name
  209. xpm.name = arg_name or xpm.name
  210. -- Save
  211. local file = assert(io.open(arg_out or filename, "wt"))
  212. file:write"/* XPM */\n"
  213. file:write("static const char *const " .. xpm.name .. "[] = {\n")
  214. file:write(('"%i %i %i %i",\n'):format(xpm.width, xpm.height, xpm.ncolours,
  215. xpm.cpp))
  216. for _, colour in ipairs(xpm.colours_full) do
  217. file:write('"' .. colour.name)
  218. for _, data in ipairs(colour) do
  219. file:write(" " .. data[1] .. " " .. data[2])
  220. end
  221. file:write('",\n')
  222. end
  223. for i, row in ipairs(xpm.pixels) do
  224. file:write('"' .. table.concat(row) .. (i == xpm.height and '"\n' or '",\n'))
  225. end
  226. file:write("};\n")