Files
xteink-plugins/plugins/bookerly/patch.py
T

426 lines
15 KiB
Python

#!/usr/bin/env python3
import os
import sys
import glob
import shutil
import hashlib
import subprocess
def read_file(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()
def write_file(path, content):
with open(path, "w", encoding="utf-8") as f:
f.write(content)
def find_first(filename, repo_dir):
results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True)
if not results:
sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}")
return results[0]
def font_id_from_files(paths):
total = 0
for p in paths:
h = hashlib.sha256(open(p, "rb").read()).hexdigest()
total += int(h, 16)
return total % (2 ** 32) - (2 ** 31)
def generate_fonts(plugin_dir, repo_dir):
scripts_dir = os.path.join(repo_dir, "lib", "EpdFont", "scripts")
fonts_dir = os.path.join(repo_dir, "lib", "EpdFont", "builtinFonts")
source_dir = os.path.join(plugin_dir, "fonts")
fontconvert = os.path.join(scripts_dir, "fontconvert.py")
subprocess.run(
[sys.executable, "-m", "pip", "install", "freetype-py==2.5.1", "fonttools", "--break-system-packages", "-q"],
check=True
)
sizes = [12, 14, 16, 18]
styles = ["Regular"]
for size in sizes:
for style in styles:
fname = f"bookerly_{size}_{style.lower()}"
out_path = os.path.join(fonts_dir, f"{fname}.h")
if os.path.exists(out_path):
print(f" already exists: {fname}.h")
continue
src_path = os.path.join(source_dir, f"Bookerly-{style}.ttf")
if not os.path.exists(src_path):
sys.exit(f"ERROR: Missing font file: {src_path}")
with open(out_path, "w") as f:
result = subprocess.run(
[sys.executable, fontconvert, fname, str(size), src_path, "--2bit", "--compress"],
capture_output=True, text=True
)
if result.returncode != 0 or not result.stdout.strip():
print(f" ERROR generating {fname}: {result.stderr[:200]}")
if os.path.exists(out_path):
os.remove(out_path)
continue
f.write(result.stdout)
print(f" ✓ generated {fname}.h")
return fonts_dir
def compute_font_ids(fonts_dir):
ids = {}
sizes = [12, 14, 16, 18]
for size in sizes:
path = os.path.join(fonts_dir, f"bookerly_{size}_regular.h")
if os.path.exists(path):
h = hashlib.sha256(open(path, "rb").read()).hexdigest()
ids[f"BOOKERLY_{size}_FONT_ID"] = int(h, 16) % (2 ** 32) - (2 ** 31)
return ids
def patch_font_ids_h(repo_dir, new_ids):
path = find_first("fontIds.h", repo_dir)
content = read_file(path)
lines = []
for name, val in new_ids.items():
if name not in content:
lines.append(f"#define {name} ({val})")
if not lines:
print(" fontIds.h already patched, skipping.")
return
write_file(path, content.rstrip() + "\n" + "\n".join(lines) + "\n")
print(f" fontIds.h: added {len(lines)} new font IDs.")
def patch_all_h(repo_dir, new_ids):
path = find_first("all.h", repo_dir)
content = read_file(path)
added = 0
sizes = [12, 14, 16, 18]
for size in sizes:
key = f"BOOKERLY_{size}_FONT_ID"
if key not in new_ids:
continue
fname = f"bookerly_{size}_regular"
inc = f'#include <builtinFonts/{fname}.h>'
if inc not in content:
content += inc + "\n"
added += 1
if added:
write_file(path, content)
print(f" all.h: added {added} includes.")
else:
print(" all.h already patched, skipping.")
def patch_main_cpp(repo_dir, new_ids):
path = find_first("main.cpp", repo_dir)
content = read_file(path)
sizes = [8, 10, 12, 14, 16, 18]
decl_lines = []
insert_lines = []
for size in sizes:
key = f"BOOKERLY_{size}_FONT_ID"
varbase = f"bookerly{size}"
if key not in new_ids:
continue
if f"EpdFontFamily {varbase}FontFamily" in content:
continue
decl_lines += [
f'EpdFont {varbase}RegularFont(&bookerly_{size}_regular);',
f'EpdFontFamily {varbase}FontFamily(&{varbase}RegularFont, &{varbase}RegularFont, &{varbase}RegularFont, &{varbase}RegularFont);',
]
insert_lines.append(f' renderer.insertFont({key}, {varbase}FontFamily);')
if not decl_lines:
print(" main.cpp already patched, skipping.")
return
anchor_decl = '#ifndef OMIT_FONTS'
content = content.replace(anchor_decl, "\n".join(decl_lines) + "\n" + anchor_decl, 1)
anchor_insert = ' renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);'
content = content.replace(anchor_insert, "\n".join(insert_lines) + "\n" + anchor_insert, 1)
write_file(path, content)
print(f" main.cpp: added {len(decl_lines) // 2} font families.")
def patch_cross_point_settings_h(repo_dir):
path = find_first("CrossPointSettings.h", repo_dir)
content = read_file(path)
if "BOOKERLY = 3" in content:
print(" CrossPointSettings.h already patched, skipping.")
return
if '"activities/settings/BookerlyPlugin.h"' not in content:
content = content.replace(
'#include "activities/settings/SmallerFontsPlugin.h"',
'#include "activities/settings/SmallerFontsPlugin.h"\n#include "activities/settings/BookerlyPlugin.h"'
)
if 'BOOKERLY = 3' not in content:
content = content.replace(
'enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };',
'enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, BOOKERLY = 3, FONT_FAMILY_COUNT };'
)
write_file(path, content)
print(" CrossPointSettings.h patched.")
def patch_cross_point_settings_cpp(repo_dir, new_ids):
path = find_first("CrossPointSettings.cpp", repo_dir)
content = read_file(path)
if "BOOKERLY" in content:
print(" CrossPointSettings.cpp already patched, skipping.")
return
content = content.replace(
' case OPENDYSLEXIC:\n switch (lineSpacing) {\n case TIGHT:\n return 0.90f;\n case NORMAL:\n default:\n return 0.95f;\n case WIDE:\n return 1.0f;\n }\n }\n}\n\nunsigned long',
' case OPENDYSLEXIC:\n switch (lineSpacing) {\n case TIGHT:\n return 0.90f;\n case NORMAL:\n default:\n return 0.95f;\n case WIDE:\n return 1.0f;\n }\n case BOOKERLY:\n switch (lineSpacing) {\n case TIGHT:\n return 0.95f;\n case NORMAL:\n default:\n return 1.0f;\n case WIDE:\n return 1.1f;\n }\n }\n}\n\nunsigned long'
)
use_resolve = 'resolve' in content
bookerly_return = 'resolve(BOOKERLY_12_FONT_ID)' if use_resolve else 'BOOKERLY_12_FONT_ID'
bookerly_case = (
' case BOOKERLY:\n'
f' return {bookerly_return};\n'
)
anchor = None
for candidate in [
' case EXTRA_LARGE:\n return resolve(OPENDYSLEXIC_14_FONT_ID);\n }\n }\n}\n',
' case EXTRA_LARGE:\n return OPENDYSLEXIC_14_FONT_ID;\n }\n }\n}\n',
]:
if candidate in content:
anchor = candidate
break
if anchor:
content = content.replace(
anchor,
' case EXTRA_LARGE:\n return ' + ('resolve(OPENDYSLEXIC_14_FONT_ID)' if 'resolve' in anchor else 'OPENDYSLEXIC_14_FONT_ID') + ';\n }\n'
+ bookerly_case +
' }\n}\n'
)
write_file(path, content)
print(" CrossPointSettings.cpp patched.")
def patch_settings_list_h(repo_dir):
path = find_first("SettingsList.h", repo_dir)
content = read_file(path)
already_patched = 'STR_NONE_OPT}, "fontFamily"' in content and 'bookerlyInstalled' in content
if already_patched:
print(" SettingsList.h already patched, skipping.")
return
if 'STR_NONE_OPT}, "fontFamily"' not in content:
content = content.replace(
'{StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily"',
'{StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC, StrId::STR_NONE_OPT}, "fontFamily"'
)
write_file(path, content)
print(" SettingsList.h patched.")
def patch_settings_activity_h(repo_dir):
path = find_first("SettingsActivity.h", repo_dir)
content = read_file(path)
if 'BookerlyInstalled' in content:
print(" SettingsActivity.h already patched, skipping.")
return
if 'categoryCount = 4' in content:
content = content.replace(
'static constexpr int categoryCount = 4;',
'static constexpr int categoryCount = 5;'
)
if 'pluginsSettings' not in content:
content = content.replace(
' std::vector<SettingInfo> systemSettings;',
' std::vector<SettingInfo> systemSettings;\n std::vector<SettingInfo> pluginsSettings;'
)
content = content.replace(
' Language,\n};',
' Language,\n BookerlyInstalled,\n};'
)
write_file(path, content)
print(" SettingsActivity.h patched.")
def patch_settings_activity_cpp(repo_dir):
path = find_first("SettingsActivity.cpp", repo_dir)
content = read_file(path)
if '"Bookerly Plugin"' in content:
print(" SettingsActivity.cpp already patched, skipping.")
return
content = content.replace(
'#include "fontIds.h"',
'#include "fontIds.h"\n#include "BookerlyPlugin.h"'
)
if 'SettingAction::BookerlyInstalled' not in content:
content = content.replace(
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n'
' pluginsSettings.push_back(SettingInfo::Action(\n'
' StrId::STR_NONE_OPT, SettingAction::BookerlyInstalled\n'
' ));\n'
)
if '"Bookerly Plugin"' not in content:
content = content.replace(
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr,',
' [&settings, this](int index) -> std::string {\n'
' if (selectedCategoryIndex == 4) {\n'
' const auto& s = settings[index];\n'
' if (s.type == SettingType::ACTION && s.action == SettingAction::BookerlyInstalled) return "Bookerly Plugin";\n'
' }\n'
' return std::string(I18N.get(settings[index].nameId));\n'
' }, nullptr, nullptr,'
)
if '"Bookerly"' not in content:
content = content.replace(
' const uint8_t value = SETTINGS.*(setting.valuePtr);\n'
' valueText = I18N.get(setting.enumValues[value]);\n',
' const uint8_t value = SETTINGS.*(setting.valuePtr);\n'
' if (setting.key && std::string(setting.key) == "fontFamily" && value == CrossPointSettings::BOOKERLY) {\n'
' valueText = "Bookerly";\n'
' } else {\n'
' valueText = I18N.get(setting.enumValues[value]);\n'
' }\n'
)
if '"Installed"' not in content:
content = content.replace(
' } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {\n'
' valueText = std::to_string(SETTINGS.*(setting.valuePtr));\n'
' }',
' } else if (setting.type == SettingType::ACTION && setting.action == SettingAction::BookerlyInstalled) {\n'
' valueText = "Installed";\n'
' } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {\n'
' valueText = std::to_string(SETTINGS.*(setting.valuePtr));\n'
' }'
)
write_file(path, content)
print(" SettingsActivity.cpp patched.")
def patch_web_server(repo_dir):
path = find_first("CrossPointWebServer.cpp", repo_dir)
content = read_file(path)
if '"Bookerly"' in content:
print(" CrossPointWebServer.cpp already patched, skipping.")
return
content = content.replace(
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
' doc["name"] = "Smaller Fonts";\n'
' doc["category"] = "Plugins";\n'
' }\n'
' }\n',
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
' doc["name"] = "Smaller Fonts";\n'
' doc["category"] = "Plugins";\n'
' } else if (strcmp(s.key, "fontFamily") == 0) {\n'
' JsonArray opts = doc["options"].to<JsonArray>();\n'
' opts.add("Noto Serif");\n'
' opts.add("Noto Sans");\n'
' opts.add("OpenDyslexic");\n'
' opts.add("Bookerly");\n'
' }\n'
' }\n'
)
write_file(path, content)
print(" CrossPointWebServer.cpp patched.")
def copy_plugin_sources(plugin_dir, repo_dir):
dest = os.path.join(repo_dir, "src", "activities", "settings")
os.makedirs(dest, exist_ok=True)
for fname in os.listdir(plugin_dir):
if fname.endswith((".h", ".cpp", ".hpp")):
src = os.path.join(plugin_dir, fname)
dst = os.path.join(dest, fname)
shutil.copy2(src, dst)
print(f" ✓ {fname}")
def patch(repo_dir: str):
plugin_dir = os.path.dirname(os.path.abspath(__file__))
print(" Copying plugin sources...")
copy_plugin_sources(plugin_dir, repo_dir)
print(" Generating Bookerly font files...")
fonts_dir = generate_fonts(plugin_dir, repo_dir)
print(" Computing font IDs...")
new_ids = compute_font_ids(fonts_dir)
for k, v in new_ids.items():
print(f" {k} = {v}")
print(" Patching fontIds.h...")
patch_font_ids_h(repo_dir, new_ids)
print(" Patching all.h...")
patch_all_h(repo_dir, new_ids)
print(" Patching main.cpp...")
patch_main_cpp(repo_dir, new_ids)
print(" Patching CrossPointSettings.h...")
patch_cross_point_settings_h(repo_dir)
print(" Patching CrossPointSettings.cpp...")
patch_cross_point_settings_cpp(repo_dir, new_ids)
print(" Patching SettingsList.h...")
patch_settings_list_h(repo_dir)
print(" Patching SettingsActivity.h...")
patch_settings_activity_h(repo_dir)
print(" Patching SettingsActivity.cpp...")
patch_settings_activity_cpp(repo_dir)
print(" Patching CrossPointWebServer.cpp (web UI)...")
patch_web_server(repo_dir)
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
patch(sys.argv[1])