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.

Time
About 10 minutes

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.

BlockUnitWhere the geometry livesSpace
Quran SVGayahpath.ayahPolygon in the SVG; polygon in json/NNN.jsonthe file's root user space, defined by its viewBox
Quran SVG Elementsword, markbox in index/by-page/NNN.json; the paths themselves in the SVGthe page's viewBox, y down, page and line transforms already applied
Quran Engineword, markpage.hitBoxes(), page.highlightBoxes() and the …View variantspage 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:

DatasetUnit addressedviewBox of the fileCoordinates for 1:1
Quran SVGthe ayah, as one polygon-53.3109 -198.4777 345 550181.08,18.31 57.54,18.31 57.54,48.94 181.08,48.94
Quran SVG Elementseach word, as a box0 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.

Coordinates are not portable — not across muṣḥafs, not across pages, not across datasets

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:

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.

The polygon field has two shapes

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 …ViewhitTestViewEx, 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

Try it live

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.