Skip to content

Commit 24138cc

Browse files
committed
font mapper code complete
1 parent 026d289 commit 24138cc

File tree

6 files changed

+212
-62
lines changed

6 files changed

+212
-62
lines changed

src/font_map_legacy.lua

Lines changed: 126 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ end
1818
-- luacheck: ignore 11./global_dialog
1919

2020
local utils = require("library.utils")
21+
local client = require("library.client")
2122
local library = require("library.general_library")
2223
local mixin = require("library.mixin")
2324
local smufl_glyphs = require("library.smufl_glyphs")
@@ -31,19 +32,14 @@ context = {
3132
current_directory = finenv.RunningLuaFolderPath()
3233
}
3334

34-
local function format_codepoint(cp)
35-
local codepoint_desc = "[" .. string.format("U+%04X", cp) .. "]"
36-
local glyph_name = smufl_glyphs.get_glyph_info(cp)
37-
if glyph_name then
38-
return "'" .. glyph_name .. "' " .. codepoint_desc
35+
local function format_mapping(mapping)
36+
local codepoint_desc = "[" .. utils.format_codepoint(mapping.codepoint) .. "]"
37+
if mapping.glyph then
38+
return "'" .. mapping.glyph .. "' " .. codepoint_desc
3939
end
4040
return codepoint_desc
4141
end
4242

43-
local function parse_codepoint(codepoint_string)
44-
return tonumber(codepoint_string:match("U%+(%x+)"), 16)
45-
end
46-
4743
local function enable_disable(dialog)
4844
local delable = #(dialog:GetControl("legacy_box"):GetText()) > 0
4945
local addable = delable and #(dialog:GetControl("smufl_box"):GetText()) > 0
@@ -90,7 +86,8 @@ end
9086

9187
local function on_popup(popup)
9288
local legacy_codepoint = context.popup_keys[popup:GetSelectedItem() + 1] or 0
93-
local smufl_codepoint = legacy_codepoint > 0 and context.current_mapping[legacy_codepoint] or 0
89+
local current_mapping = legacy_codepoint > 0 and context.current_mapping[legacy_codepoint]
90+
local smufl_codepoint = current_mapping and current_mapping.codepoint or 0
9491
local dialog = popup:GetParent()
9592
set_codepoint(dialog:GetControl("legacy_box"), legacy_codepoint)
9693
set_codepoint(dialog:GetControl("smufl_box"), smufl_codepoint)
@@ -105,7 +102,7 @@ local function update_popup(popup, current_codepoint)
105102
popup:Clear()
106103
local current_index
107104
for k, v in ipairs(context.popup_keys) do
108-
popup:AddString(tostring(v) .. " maps to " .. format_codepoint(context.current_mapping[v]))
105+
popup:AddString(tostring(v) .. " maps to " .. format_mapping(context.current_mapping[v]))
109106
if v == current_codepoint then
110107
current_index = k - 1
111108
end
@@ -138,7 +135,7 @@ local function on_select_file(control)
138135
local open_dialog = mixin.FCMFileOpenDialog(dialog:CreateChildUI())
139136
:SetWindowTitle(finale.FCString("Select existing JSON file"))
140137
:SetInitFolder(finale.FCString(context.current_directory))
141-
:AddFilter(finale.FCString("*.json"), finale.FCString("Font Mapping"))
138+
:AddFilter(finale.FCString("*.json"), finale.FCString("Legacy Font Mapping"))
142139
if not open_dialog:Execute() then
143140
return
144141
end
@@ -154,16 +151,27 @@ local function on_select_file(control)
154151
dialog:CreateChildUI():AlertError("Font " .. name .. " is a SMuFL font.", "SMuFL Font")
155152
return
156153
end
157-
local file = io.open(selected_file.LuaString)
154+
local file = io.open(client.encode_with_client_codepage(selected_file.LuaString))
158155
if file then
159156
local json_contents = file:read("*a")
160157
file:close()
161158
local json = cjson.decode(json_contents)
162159
context.current_directory = path
163160
change_font(dialog, font_info)
164161
context.current_mapping = {}
165-
for _, v in pairs(json) do
166-
context.current_mapping[tonumber(v.legacyCodepoint)] = parse_codepoint(v.codepoint)
162+
for glyph, v in pairs(json) do
163+
local t = {}
164+
t.glyph = glyph
165+
t.codepoint = utils.parse_codepoint(v.codepoint)
166+
t.nameIsMakeMusic = v.nameIsMakeMusic
167+
if t.codepoint == 0xFFFD then
168+
local smufl_box = dialog:GetControl("smufl_box")
169+
local _, info = smufl_glyphs.get_glyph_info(glyph, smufl_box:CreateFontInfo())
170+
if info then
171+
t.codepoint = info.codepoint
172+
end
173+
end
174+
context.current_mapping[tonumber(v.legacyCodepoint)] = t
167175
end
168176
update_popup(dialog:GetControl("mappings"))
169177
end
@@ -206,13 +214,21 @@ local function on_add_mapping(control)
206214
local legacy_point = get_codepoint(dialog:GetControl("legacy_box"))
207215
if legacy_point == 0 then return end
208216
local smufl_point = get_codepoint(dialog:GetControl("smufl_box"))
209-
if (smufl_point == 0) then return end
210-
if context.current_mapping[legacy_point] then
211-
if finale.YESRETURN ~= dialog:CreateChildUI():AlertYesNo("Symbol " .. legacy_point .. " is already mapped to " .. format_codepoint(smufl_point) .. ". Continue?", "Already Mapped") then
217+
if smufl_point == 0 then return end
218+
local current_mapping = context.current_mapping[legacy_point]
219+
if current_mapping then
220+
if finale.YESRETURN ~= dialog:CreateChildUI():AlertYesNo("Symbol " .. legacy_point .. " is already mapped to " .. format_mapping(current_mapping) .. ". Continue?", "Already Mapped") then
212221
return
213222
end
214223
end
215-
context.current_mapping[legacy_point] = smufl_point
224+
current_mapping = {codepoint = smufl_point}
225+
local glyph, info = smufl_glyphs.get_glyph_info(smufl_point, dialog:GetControl("smufl_box"):CreateFontInfo())
226+
if info then
227+
current_mapping.glyph = glyph
228+
else
229+
current_mapping.glyph = utils.format_codepoint(smufl_point)
230+
end
231+
context.current_mapping[legacy_point] = current_mapping
216232
update_popup(popup, legacy_point)
217233
end
218234

@@ -228,6 +244,84 @@ local function on_delete_mapping(control)
228244
end
229245
end
230246

247+
-- use hand-crafted json encoder to control order of elements
248+
local function emit_json(mapping, reverse_lookup)
249+
local function quote(str)
250+
return '"' .. tostring(str):gsub('\\', '\\\\'):gsub('"', '\\"') .. '"'
251+
end
252+
253+
local function emit_entry(entry, legacy_codepoint)
254+
local parts = {}
255+
if type(entry.nameIsMakeMusic) == "boolean" then
256+
table.insert(parts, '\t\t"nameIsMakeMusic": ' .. tostring(entry.nameIsMakeMusic))
257+
end
258+
if entry.codepoint then
259+
table.insert(parts, '\t\t"codepoint": ' .. quote(utils.format_codepoint(entry.codepoint)))
260+
end
261+
table.insert(parts, '\t\t"legacyCodepoint": ' .. quote(tostring(legacy_codepoint)))
262+
if entry.description then
263+
table.insert(parts, '\t\t"description": ' .. quote(entry.description))
264+
else
265+
table.insert(parts, '\t\t"description": ' .. quote(""))
266+
end
267+
return "{\n" .. table.concat(parts, ",\n") .. "\n\t}"
268+
end
269+
270+
local lines = { "{" }
271+
local first = true
272+
for key, entry in pairsbykeys(mapping) do
273+
if reverse_lookup[entry.glyph] then
274+
if not first then
275+
lines[#lines] = lines[#lines] .. "," -- ← append comma to previous line
276+
end
277+
table.insert(lines, "\t" .. quote(entry.glyph) .. ": " .. emit_entry(entry, key))
278+
first = false
279+
end
280+
end
281+
table.insert(lines, "}")
282+
return table.concat(lines, "\n")
283+
end
284+
285+
local function on_save(control)
286+
local dialog = control:GetParent()
287+
if type(context.current_mapping) ~= "table" or not next(context.current_mapping) then
288+
dialog:CreateChildUI():AlertInfo("Nothing has been mapped.", "No Mapping")
289+
return
290+
end
291+
local save_dialog = finale.FCFileSaveAsDialog(dialog:CreateChildUI())
292+
save_dialog:SetWindowTitle(finale.FCString("Save mapping as"))
293+
save_dialog:AddFilter(finale.FCString("*.json"), finale.FCString("Legacy Font Mapping"))
294+
save_dialog:SetInitFolder(finale.FCString(context.current_directory))
295+
save_dialog:SetFileName(finale.FCString(context.current_font.Name .. ".json"))
296+
save_dialog:AssureFileExtension("json")
297+
if not save_dialog:Execute() then
298+
return
299+
end
300+
local path_fstr = finale.FCString()
301+
save_dialog:GetFileName(path_fstr)
302+
local reverse_lookup = {}
303+
for legacy_codepoint, info in pairs(context.current_mapping) do
304+
if type(info.glyph) ~= "string" then
305+
dialog:CreateChildUI():AlertError("No glyph name found for legacy codepoint " .. legacy_codepoint .. ".",
306+
"No Glyph Name")
307+
return
308+
elseif reverse_lookup[info.glyph] then
309+
if finale.YESRETURN ~= dialog:CreateChildUI():AlertYesNo("Glyph name " .. info.glyph .. " is mapped more than once. Continue?", "Duplicate Glyph Name") then
310+
return
311+
end
312+
end
313+
reverse_lookup[info.glyph] = true
314+
end
315+
local result = emit_json(context.current_mapping, reverse_lookup)
316+
local file = io.open(client.encode_with_client_codepage(path_fstr.LuaString), "w")
317+
if not file then
318+
dialog:CreateChildUI():AlertError("Unable to write to file " .. path_fstr.LuaString .. ".", "File Error")
319+
return
320+
end
321+
file:write(result)
322+
file:close()
323+
end
324+
231325
function font_map_legacy()
232326
local dialog = mixin.FCXCustomLuaWindow()
233327
:SetTitle("Map Legacy Fonts to SMuFL")
@@ -283,14 +377,14 @@ function font_map_legacy()
283377
on_symbol_select(control:GetParent():GetControl("legacy_box"))
284378
end)
285379
dialog:CreateButton(0, current_y + editor_height / 2 - button_height, "add_mapping")
286-
:SetText("Add Mapping")
287-
:SetWidth(120)
380+
:SetText("Add/Update Mapping")
381+
:SetWidth(140)
288382
:SetEnable(false)
289383
:AssureNoHorizontalOverlap(dialog:GetControl("legacy_box"), editor_width / 2)
290384
:AddHandleCommand(on_add_mapping)
291385
dialog:CreateButton(0, current_y + editor_height / 2 + y_increment, "delete_mapping")
292386
:SetText("Delete Mapping")
293-
:SetWidth(120)
387+
:SetWidth(140)
294388
:SetEnable(false)
295389
:AssureNoHorizontalOverlap(dialog:GetControl("legacy_box"), editor_width / 2)
296390
:AddHandleCommand(on_delete_mapping)
@@ -311,8 +405,16 @@ function font_map_legacy()
311405
dialog:CreatePopup(0, current_y, "mappings")
312406
:StretchToAlignWithRight()
313407
:AddHandleCommand(on_popup)
314-
-- close button
315-
dialog:CreateCancelButton("cancel"):SetText("Close")
408+
current_y = current_y + button_height + y_increment
409+
-- save and close buttons
410+
dialog:CreateButton(0, current_y, "save")
411+
:SetText("Save...")
412+
:DoAutoResizeWidth(0)
413+
:AddHandleCommand(on_save)
414+
dialog:CreateCloseButton(0, current_y, "close")
415+
:SetText("Close")
416+
:DoAutoResizeWidth(0)
417+
:HorizontallyAlignRightWithFurthest()
316418
-- registrations
317419
dialog:RegisterInitWindow(function(self)
318420
on_smufl_popup(self:GetControl("smufl_list"))

src/library/client.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ local features = {
111111
luaosutils = {
112112
test = finenv.EmbeddedLuaOSUtils,
113113
error = requires_later_plugin_version("the embedded luaosutils library")
114+
},
115+
cjson = {
116+
test = client.get_lua_plugin_version() >= 0.67,
117+
error = requires_plugin_version("0.67", "the embedded cjson library"),
114118
}
115119
}
116120

src/library/general_library.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,31 @@ function library.get_smufl_metadata_file(font_info_or_name)
397397
return try_prefix(calc_smufl_directory(false))
398398
end
399399

400+
--[[
401+
% get_smufl_metadata_json
402+
403+
@ [font_info_or_name] (FCFontInfo|string) if non-nil, the font to search for; if nil, search for the Default Music Font; if string, search for the font by name
404+
@ [string] the key of the subtable to return from the json
405+
: (table|nil)
406+
]]
407+
function library.get_smufl_metadata_table(font_info_or_name, subkey)
408+
if not client.assert_supports("cjson") then
409+
return
410+
end
411+
local cjson = require("cjson")
412+
local json_file = library.get_smufl_metadata_file(font_info_or_name)
413+
if not json_file then
414+
return nil
415+
end
416+
local contents = json_file:read("*a")
417+
json_file:close()
418+
local json_table = cjson.decode(contents)
419+
if json_table and subkey then
420+
return json_table[subkey]
421+
end
422+
return json_table
423+
end
424+
400425
--[[
401426
% is_font_smufl_font
402427

src/library/smufl_glyphs.lua

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ local smufl_glyphs = {}
88

99
local utils = require("library.utils")
1010
local library = require("library.general_library")
11-
local cjson = require("cjson")
1211

1312
local glyphs = {
1413
["articStaccatissimoWedgeBelow"] = {codepoint = 0xE4A9, description = "Staccatissimo wedge below"},
@@ -5880,55 +5879,55 @@ local by_codepoint = {
58805879
[0xEAF5] = "beamAccelRit2",
58815880
}
58825881

5883-
local function parse_codepoint(codepoint)
5884-
return tonumber(codepoint:match("U%+(%x+)"), 16)
5885-
end
5886-
5887-
-- Add Finale Maestro optional glyphs.
5888-
local font_metadata_file = library.get_smufl_metadata_file("Finale Maestro")
5889-
if font_metadata_file then
5890-
local content = font_metadata_file:read("*a")
5891-
font_metadata_file:close()
5892-
local font_metadata = cjson.decode(content)
5893-
local optional_glyphs = font_metadata["optionalGlyphs"]
5894-
if optional_glyphs then
5895-
for name, info in pairs(optional_glyphs) do
5896-
if info.codepoint then
5897-
local codepoint = parse_codepoint(info.codepoint)
5898-
if codepoint and not glyphs[name] then
5899-
glyphs[name] = {codepoint = codepoint, description = ""}
5900-
by_codepoint[codepoint] = name
5901-
end
5902-
end
5903-
end
5904-
end
5905-
end
5906-
59075882
--[[
59085883
% get_glyph_info
59095884
59105885
Returns the SMuFL glyph name and a new table containing the `codepoint` and its `description`.
59115886
5912-
@ codepoint_or_name (string) or (number) the name or codepoint for a SMuFL glyph
5887+
@ codepoint_or_name (string|number) the name or codepoint for a SMuFL glyph
5888+
@ [font_info_or_name] (string) or (font_info) the SMuFL font to search for optional glyphs
59135889
: (string) The glyph name
59145890
: (table) The glyph information
59155891
]]
5916-
function smufl_glyphs.get_glyph_info(codepoint_or_name)
5917-
local name, info
5892+
function smufl_glyphs.get_glyph_info(codepoint_or_name, font_info_or_name)
5893+
local name
59185894
if type(codepoint_or_name) == "number" then
59195895
name = by_codepoint[codepoint_or_name]
5920-
info = name and glyphs[name]
59215896
elseif type(codepoint_or_name) == "string" then
59225897
name = codepoint_or_name
5923-
info = glyphs[codepoint_or_name]
5898+
end
5899+
local info = name and glyphs[name]
5900+
if not info and font_info_or_name then
5901+
local optional_glyphs = library.get_smufl_metadata_table(font_info_or_name, "optionalGlyphs")
5902+
if optional_glyphs then
5903+
if type(codepoint_or_name) == "number" then
5904+
for k, v in pairs(optional_glyphs) do
5905+
if v.codepoint then
5906+
local codepoint = utils.parse_codepoint(v.codepoint)
5907+
if codepoint == codepoint_or_name then
5908+
name = k
5909+
info = { codepoint = codepoint, description = "", optional = true }
5910+
break
5911+
end
5912+
end
5913+
end
5914+
elseif type(codepoint_or_name) == "string" then
5915+
name = codepoint_or_name
5916+
local optinfo = optional_glyphs[name]
5917+
if optinfo and optinfo.codepoint then
5918+
local codepoint = utils.parse_codepoint(optinfo.codepoint)
5919+
info = codepoint and {codepoint = codepoint, description = "", optional = true}
5920+
end
5921+
end
5922+
end
59245923
end
59255924
return name, info and utils.copy_table(info) or nil
59265925
end
59275926

59285927
--[[
59295928
% iterate_glyphs
59305929
5931-
Returns an iterator over all SMuFL glyphs.
5930+
Returns an iterator over the standard SMuFL glyphs as defined in glyphnames.json.
59325931
59335932
Each iteration returns:
59345933
1. The glyph name (string)

0 commit comments

Comments
 (0)