Qur'an page geometry
A vectorised muṣḥaf page is glyph outlines plus a map of which ink belongs to which verse or word. That map is a set of coordinates, and every coordinate is measured from an origin that belongs to one file. This page is about what those numbers are anchored to, so that a highlight, a crop or a hit-test lands on the ink you meant.
Three of our blocks publish geometry, at three depths. Coverage narrows as addressing deepens: Quran SVG covers every vectorised muṣḥaf and gives you an ayah region; Elements covers the muṣḥafs that have been split and gives you a box per word and a path per mark; Engine draws the split muṣḥafs on mobile with the same addressing.
| Block | Unit | Where the geometry lives | Space |
|---|---|---|---|
| Quran SVG | ayah | path.ayahPolygon in the SVG; polygon in json/NNN.json | the file's root user space, defined by its viewBox |
| Quran SVG Elements | word, mark | box in index/by-page/NNN.json; the paths themselves in the SVG | the page's viewBox, y down, page and line transforms already applied |
| Quran Engine | word, mark | page.hitBoxes(), page.highlightBoxes() and the …View variants | page units 345 × 550 y down, or viewport pixels — two different APIs |
Every number is anchored to one file
The same verse — the basmalah on page 1 of the same King Fahd Complex Ḥafṣ print — as two of our datasets describe it, read at build time:
| Dataset | Unit addressed | viewBox of the file | Coordinates for 1:1 |
|---|---|---|---|
| Quran SVG | the ayah, as one polygon | -53.3109 -198.4777 345 550 | 181.08,18.31 57.54,18.31 57.54,48.94 181.08,48.94 |
| Quran SVG Elements | each word, as a box | 0 0 345 550 | 1:1:1 [197.35, 227.46, 231.85, 242.12] 1:1:2 [185.33, 219.36, 200.18, 239.21] 1:1:3 [133.91, 218.74, 184.96, 242.71] 1:1:4 [126.29, 218.07, 161.27, 240.59] |
Read at build time from public/demo/page-001.json, public/demo/page-001.svg and public/demo/elements-words.json, all page 1 of the same Ḥafṣ print, at the commits pinned in public/demo/SOURCES.json. The y values differ by roughly the difference between the two origins: the same ink, described in two coordinate spaces.
Same ink, roughly 200 units apart in y. Quran SVG's page 1 declares viewBox="-53.3109 -198.4777 345 550" —
the opening spread has its own origin — while the Elements page declares 0 0 345 550. Neither is wrong.
Each file's coordinates are in that file's space, and the two were never promised to share one.
Overlay an Elements word box on a Quran SVG page, or reuse a polygon from the Ḥafṣ muṣḥaf on the Warsh one, and
you get a rectangle on the wrong ink. Nothing throws; the shape is valid SVG. Read the
viewBox from the file you loaded, use the geometry from that same file, and store the muṣḥaf key and page
beside any coordinate you persist.
Three specific cases from the data:
- The
viewBoxis not constant even within one muṣḥaf. Pages 3–604 of Ḥafṣ declare0 0 345 550; pages 1–2 declare their own origin; Warsh and Qālūn start at x = −6. Surah-variant files are cropped in height. Read it; never assume it. - Recto and verso are drawn under different page matrices inside an Elements file, so raw path coordinates are
not comparable across pages.
getBBox()on ag.wordalready answers in that word's frame; the sidecar boxes exist for when there is no browser. - Page 42 is page 42 of one muṣḥaf only. All five vectorised muṣḥafs have 604 pages, so page numbers look interchangeable. A riwayah divides its verses differently and its pages are set separately.
An ayah region is usually several rectangles
An ayah generally begins mid-line and ends mid-line, so its region is a stack of line strips, not one box. In the Ḥafṣ muṣḥaf, 4,221 of the 6,236 polygons have more than one subpath. Here is 1:7 on page 1 — thirteen points, because the verse turns through three lines:
113.70,130.39 13.82,130.39 13.82,162.77 37.11,162.77 37.11,189.05 73.48,189.05
73.48,214.95 171.86,214.95 171.86,191.54 204.99,191.54 204.99,160.65 113.70,160.28 113.70,130.39
That is why the bounding box is the wrong crop: the box around an ayah covers ink belonging to the verses either side. Clip to the polygon, and use the box only as the viewport.
In Quran SVG's JSON, pages 1–2 store a bare points list like the one above; pages 3 onwards store SVG path data
— M 0.0 0.75 L 337.75 0.75 … Z M 254.12 41.0 … Z. A parser written against one returns NaN on the other,
and Math.min of anything containing NaN is NaN, so the crop you compute is an empty box rather than an
error. Pull every number out and pair them; path commands are letters and every coordinate is written x then y.
In the SVG itself, the d attribute is path data on every page.
// Handles both shapes. This is what the demo on this site runs.
function boxOf(polygon: string) {
const nums = (polygon.match(/-?\d+(?:\.\d+)?/g) ?? []).map(Number);
const xs: number[] = [];
const ys: number[] = [];
for (let i = 0; i + 1 < nums.length; i += 2) { xs.push(nums[i]); ys.push(nums[i + 1]); }
const x = Math.min(...xs);
const y = Math.min(...ys);
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
}
Word boxes interleave
An Elements word box is the exact extent of that word's outlines, rounded to two decimals. Arabic words overlap in x — a neighbour's ascender or an adjacent mark sits inside the same rectangle — so a box frames a word without isolating it. To lift one word out on its own, hide the page and show the one group; the box is the viewport, the group is the content.
const page = await (await fetch("/index/by-page/001.json")).json();
const w = page.words.find((x) => x.word_key === "1:1:2");
const [x0, y0, x1, y1] = w.box;
svg.setAttribute("viewBox", `${x0} ${y0} ${x1 - x0} ${y1 - y0}`);
svg.style.visibility = "hidden";
svg.querySelector('g.word[data-word-key="1:1:2"]').style.visibility = "visible";
Boxes are [x0, y0, x1, y1] — two corners, not origin and size.
Page units and screen pixels
Everything above is in page units. A tap arrives in viewport pixels, through whatever scale and offset your layout
applied. In the DOM, getScreenCTM() on the SVG converts one to the other. In Quran Engine the two spaces are two
APIs: page.width is 345 page units, and anything named …View — hitTestViewEx, wordBoxView,
highlightBoxes() — is in viewport pixels through the current layout. Feed raw mouse coordinates to the page-unit
hitTest and you get the wrong word, or none, with no error.
What geometry does not give you
- The words. A polygon carries
surahandayah; an Elements word carries its printed form and key; neither is a text you can search. Join to Quran Text by word key. - Letters. A word is the smallest addressable unit of text today, and a named mark the smallest unit of ink. Letter split is the goal for Elements and this is how far it has got.
- Another reading's page. Coordinates belong to one printed edition. To show the same ayah in Warsh you load the Warsh page and its own geometry — the ayah may well sit on a different page.
Click any ayah on any of the 604 pages in five riwayat, then crop it — polygon clip, not bounding box — from the files fetched live from the repository.
How this data is made
Quran SVG's pages are vectorised from the printed edition, and the ayah polygons are derived from each page's own ayah medallions, then audited against the ink. Elements regroups that same artwork into words and marks without moving or redrawing a path, and its boxes are read back out of the shipped SVGs. Engine converts the split pages with a pixel-diff gate that every one of the 604 pages passes, keeping coordinates exact to 0.01 page unit.