Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
xcopy Convert2EBRL.dist output\Convert2EBRL\Convert2EBRL.dist /S /I
xcopy gui\icons output\Convert2EBRL\icons /S /I
copy gui\conveyor.conf output\Convert2EBRL\conveyor.conf
copy vendor\liblouis\COPYING.LESSER output\Convert2EBRL\Convert2EBRL.dist\louis\COPYING.LESSER
- name: Create release zip
run: Compress-Archive -Path output\\Convert2EBRL -Destination convert2ebrl-continuous.zip
- name: Update continuous tag
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A set of tools for converting documents to eBraille. This repository contains a
* gui: A graphical tool for converting BRF documents into eBraille.
* brf2ebrl: The parser library for converting documents.
* plugins: Subprojects within this directory are parser plugins for specific Braille codes.
* vendor/liblouis: A vendored copy of liblouis used by brf2ebrl for back-translation, see [vendor/liblouis/README.md](vendor/liblouis/README.md).

## Development status

Expand Down
4 changes: 4 additions & 0 deletions brf2ebrl/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"pypdf>=6.13.3",
"lxml>=6.1.0",
"html5lib>=1.1",
"liblouis",
]
requires-python = ">=3.12"
readme = "README.md"
Expand All @@ -36,3 +37,6 @@ testpaths = ["tests",]
[build-system]
requires = ["uv_build>=0.11.3,<0.12.0"]
build-backend = "uv_build"

[tool.uv.sources]
liblouis = { workspace = true }
5 changes: 4 additions & 1 deletion brf2ebrl/src/brf2ebrl/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from brf2ebrl.parser import Parser
from brf2ebrl.utils import list_sub_paths
from brf2ebrl.utils.back_translation import back_translate_page_number
from brf2ebrl.utils.ebrl import create_navigation_html, PageRef, HeadingRef
from brf2ebrl.utils.metadata import DEFAULT_METADATA, MetadataItem, ensure_default_metadata
from brf2ebrl.utils.opf import PACKAGE, METADATA, MANIFEST, SPINE, ITEM, ITEMREF, META, FORMAT, DATE
Expand Down Expand Up @@ -149,8 +150,10 @@ def _create_navigation_html(self, opf_name: str) -> str:
_HEADING_TAGS.index(element.tag) + 1)))
elif element.tag == "span" and element.get("role") == "doc-pagebreak":
page_id = element.get("id")
page_num_braille = element.text_content()
page_refs.append(
PageRef(href=f"{vol_name}#{page_id}", page_num_braille=element.text_content(), title=""))
PageRef(href=f"{vol_name}#{page_id}", page_num_braille=page_num_braille,
title=back_translate_page_number(page_num_braille)))
if detected_title is None:
detected_title = ""
return create_navigation_html(opf_name=opf_name, page_refs=page_refs, heading_refs=headings,
Expand Down
24 changes: 24 additions & 0 deletions brf2ebrl/src/brf2ebrl/utils/back_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) 2026. American Printing House for the Blind.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Back-translation of braille to print using liblouis."""
import logging
from functools import cache

import louis
from louis.bundled import table_path

# Page numbers are uncontracted, and grade 2 would back-translate roman numerals such as ⠭ and ⠇ as "it" and "like".
_PAGE_NUMBER_TABLES = [table_path("unicode.dis"), table_path("en-ueb-g1.ctb")]


@cache
def back_translate_page_number(page_num_braille: str) -> str:
"""Back-translate a Unicode braille page number to its print equivalent."""
print_page_num = louis.backTranslateString(_PAGE_NUMBER_TABLES, page_num_braille.strip()).strip()
# liblouis marks braille it cannot back-translate as \dots/ or with private use characters.
if "\\" in print_page_num or any("" <= c <= "" for c in print_page_num):
logging.warning(f"Page number {page_num_braille} could not be fully back-translated, got {print_page_num!r}")
return print_page_num
2 changes: 1 addition & 1 deletion brf2ebrl/src/brf2ebrl/utils/ebrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def create_navigation_html(title: str = "-", braille_title: str = "⠤", heading
ATTR(hidden=""),
H2("⠠⠇⠊⠌ ⠷ ⠏⠁⠛⠑⠎"),
OL(
*[LI(A(ATTR(href=r.href), r.page_num_braille)) for r in page_refs]
*[LI(A(ATTR(href=r.href, title=r.title), r.page_num_braille)) for r in page_refs]
)
)
)
Expand Down
40 changes: 40 additions & 0 deletions brf2ebrl/tests/test_back_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright (c) 2026. American Printing House for the Blind.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

import lxml.html
import pytest

from brf2ebrl.utils.back_translation import back_translate_page_number
from brf2ebrl.utils.ebrl import create_navigation_html, PageRef


@pytest.mark.parametrize("page_num_braille,expected", [
("⠼⠁", "1"),
("⠼⠁⠚⠚", "100"),
("⠊", "i"),
("⠭⠊⠧", "xiv"),
("⠭", "x"),
("⠇", "l"),
("⠠⠺⠼⠁", "W1"),
("⠁⠼⠃", "a2"),
("⠼⠁⠤⠼⠃", "1-2"),
(" ⠼⠉ ", "3"),
])
def test_back_translate_page_number(page_num_braille: str, expected: str):
assert back_translate_page_number(page_num_braille) == expected


def test_navigation_page_list_has_print_page_title():
nav = create_navigation_html(page_refs=[
PageRef(href="ebraille/vol0.html#page_1", title="i", page_num_braille="⠊"),
PageRef(href="ebraille/vol0.html#page_2", title="1", page_num_braille="⠼⠁"),
])
root = lxml.html.fromstring(nav.encode("utf-8"), parser=lxml.html.xhtml_parser)
links = root.xpath("//*[@role='doc-pagelist']//*[local-name()='a']")
assert [(a.get("href"), a.get("title"), a.text) for a in links] == [
("ebraille/vol0.html#page_1", "i", "⠊"),
("ebraille/vol0.html#page_2", "1", "⠼⠁"),
]
2 changes: 2 additions & 0 deletions gui/Convert2EBRL.pyw
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# nuitka-project: --enable-plugins=pyside6
# nuitka-project: --include-package-data=brf2ebrl
# nuitka-project: --include-package=brf2ebrl_bana
# nuitka-project: --include-package-data=louis
# nuitka-project: --include-data-files={MAIN_DIRECTORY}/../vendor/liblouis/src/louis/liblouis.dll=louis/liblouis.dll
# nuitka-project: --product-version={APP_VERSION}

import sys
Expand Down
76 changes: 76 additions & 0 deletions liblouis_changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# liblouis integration: page list titles

## Why

eBraille 1.0, section 8.3.2 (Page list):

> Each entry in the page list MUST include the print page number equivalent in a title attribute.

The spec's example puts the `title` on the `<a>` inside each `<li>`:

```html
<li><a href="chap01.html#p001" title="1">⠼⠁</a></li>
```

Before this change, `index.html` page list entries had no `title`. `PageRef.title` existed but was always set to `""` and never written out.

## What changed

### New: `vendor/liblouis` workspace package (distribution `liblouis`, import `louis`)

liblouis does not publish its Python bindings to PyPI. The `louis` and `pylouis` projects on PyPI are unrelated to liblouis and must not be used. Everything here comes from liblouis v3.39.0:

| File | Source |
| --- | --- |
| `src/louis/__init__.py` | Official ctypes bindings (`python/louis/__init__.py.in`). The only change is the library loading, marked with a `Convert2EBRL` comment: it loads `liblouis.dll` from the package directory on Windows and a system liblouis elsewhere. |
| `src/louis/liblouis.dll` | `bin/` of `liblouis-3.39.0-win64.zip`. The sha256 `64d669ac…fbc6591` matches the GitHub release asset digest. It only depends on `KERNEL32.dll` and `msvcrt.dll`. |
| `src/louis/tables/` | `unicode.dis`, `en-ueb-g1.ctb` and the 7 tables they include (about 95 KB, not the full 15 MB table set). |
| `src/louis/bundled.py` | New helper: `table_path(name)` returns the absolute path to a bundled table. |
| `COPYING.LESSER` | liblouis license (LGPL-2.1-or-later). |
| `README.md` | Provenance and update steps. |

Tables are passed to liblouis as absolute paths. Setting `LOUIS_TABLEPATH` from Python does not reach the DLL, because it keeps a separate `msvcrt` environment, and `lou_setDataPath` is deprecated.

### `brf2ebrl`

- `pyproject.toml`: depends on `liblouis`, with `[tool.uv.sources] liblouis = { workspace = true }`.
- New `src/brf2ebrl/utils/back_translation.py`: `back_translate_page_number(braille) -> str`. It back-translates with `unicode.dis` + `en-ueb-g1.ctb` and caches results. It uses grade 1 because page numbers are uncontracted, and grade 2 would turn roman numerals into words (`⠭` → "it", `⠇` → "like"). It logs a warning if liblouis returns untranslatable markers.
- `src/brf2ebrl/plugin.py`: `EBrlZippedBundler._create_navigation_html` fills `PageRef.title` with the back-translated page number.
- `src/brf2ebrl/utils/ebrl.py`: page list links are written as `<a href="…" title="…">`.
- New `tests/test_back_translation.py`: tests page number back-translation (arabic, roman, `W1`-style prefixes, continuation `a2`, ranges `1-2`) and checks that the nav page list outputs `title`.

### Workspace and GUI

- Root `pyproject.toml`: adds `vendor/liblouis` to the workspace members.
- `uv.lock`: adds the `liblouis` entry.
- `gui/Convert2EBRL.pyw`: new Nuitka options. `--include-package-data` copies the tables but never DLLs, so the DLL needs its own option:
```
# nuitka-project: --include-package-data=louis
# nuitka-project: --include-data-files={MAIN_DIRECTORY}/../vendor/liblouis/src/louis/liblouis.dll=louis/liblouis.dll
```
- Root `README.md`: lists the `vendor/liblouis` subproject.
- `.github/workflows/python-app.yml`: the "Collect files for artifact" step copies liblouis's `COPYING.LESSER` to `Convert2EBRL.dist\louis\` in the artifact and release zip, as the LGPL requires.

## Result

```html
<a href="ebraille/vol0.html#page_1" title="W1">⠠⠺⠼⠁</a>
<a href="ebraille/vol0.html#page_6" title="1-2">⠼⠁⠤⠼⠃</a>
<a href="ebraille/vol0.html#page_7" title="a2">⠁⠼⠃</a>
```

## Verification

- `uv run pytest` in `brf2ebrl`: 137 passed, 4 failed. The 4 failures are in `test_block_detectors.py` and also fail without these changes.
- CLI conversion of `A-B2517-V01.BRF` and `A-B2517-V02.BRF`: all 54 page list entries have a `title`, and none have untranslatable markers.
- Minimal Nuitka standalone build using the options above: the exe loads the bundled DLL and tables and back-translates correctly.
- Full GUI Nuitka build (`gui\Convert2EBRL.pyw`, same command as CI): succeeds, and `Convert2EBRL.dist/louis/` contains `liblouis.dll` and all 9 tables.

## Status

Committed on branch `liblouis-page-list-titles`, not yet pushed. The files touched are exactly those listed under "What changed" plus this file.

## Follow-ups

- Other braille codes (non-UEB plugins) will need their own tables. The table choice is currently fixed to UEB grade 1 in `back_translation.py`.
- The same mechanism can fill the nav `<title>` and `dc:title`, which are currently `"-"`. That needs `en-ueb-g2.ctb` added to the bundled tables.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ members = [
"gui",
"brf2ebrl",
"plugins/brf2ebrl_bana",
"vendor/liblouis",
]
8 changes: 8 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading