Use markers, frames and ornaments
Your page is typeset from Quran Text and it looks like a web page. This is how to dress it as a muṣḥaf: a banner above the surah, a roundel after each verse, a border around the page — ornaments traced from printed editions or drawn from a font’s own end-of-ayah glyph, recolourable, with a transparent slot where your text goes.
Two lineages, two licences
The catalogue has 71 assets in two lineages, and the lineage decides whether you may ship the file.
lineage | What it is | Count | license | Ship it? |
|---|---|---|---|---|
scan | Surah headers, ayah markers and page frames traced from eight printed muṣḥafs | 24 — 8 of each | CC-BY-NC-SA-4.0 · status: "provisional" | Provisional. The designs belong to their publishers and permission is being settled per publisher |
font | Ayah markers built from the end-of-ayah glyph (U+06DD) of 26 Arabic font families, in their weights — 20 designs | 47 | 40 × OFL-1.1 · confirmed; 7 × unverified · pending | Yes for the 40 confirmed. Read license.status, not the lineage |
There is no release and no package. The files this page uses are the copies this site serves for demonstration.
Every entry in assets-index.json carries its own license block: id, status and the source it was traced
from, so you can read the position per asset rather than take it on trust. It is a departure from
the default licences a repository carries (Repositories and licensing, rule 1.1),
and a deliberate one.
What you get
Three kinds of ornament: a surah header, an ayah marker and a page frame. There is no Qur’anic text in any of these files. The text is yours, from Quran Text; the ornament is a picture frame for it. If you want the whole printed page, text and ornament together, that is Quran SVG, and it needs none of this.
Matching an asset to a printing
Decoration is decoration. Any of these ornaments will dress any edition — a Warsh header above a Ḥafṣ page is a
design choice, not an error, and nothing in the data stops you. The style key records the printing a scan asset
was traced from, so use the table when you want the page to look like the muṣḥaf it came from. Ḥafṣ has three
printings here, and the keys are not the ones the other blocks use.
Asset style | Printing | Quran Text edition key |
|---|---|---|
mushaf-hafs-madinah-mumtaza | Ḥafṣ, Madinah, mumtāza | hafs |
mushaf-hafs-madinah-kabir | Ḥafṣ, Madinah, large — its frame does not tile | |
mushaf-hafs-adi | Ḥafṣ, al-ʿAdī | |
mushaf-shubah | Shuʿbah | shubah |
mushaf-warsh | Warsh | warsh |
mushaf-qalon | Qālūn | qalun |
mushaf-douri | al-Dūrī | duri |
mushaf-sousi | al-Sūsī | susi |
Three keys are spelled differently on the two sides and there is no plain hafs style, so a join needs the table
above in your code. Font markers carry riwaya: null — they were never tied to a printing at all.
Pick the variant
Each asset ships as separate drawings, not filters over one file. Scan assets have three; font markers have two.
| Variant | What it is | Groups inside | Qālūn header |
|---|---|---|---|
color | the full-colour drawing, one group per printed ink | slot, c1 … cN, line | 26.1 KB, six inks |
mono | a single-colour silhouette drawn as currentColor | slot, ink | 12.3 KB |
line | linework only, constant-width strokes, currentColor — scan assets only | slot, line | 10.4 KB |
How many c groups a colour file has depends on the source: the Qālūn header has six, the Ḥafṣ Madinah mumtāza
header ten, the Alkalami font marker five. Read them from the file, or from the asset’s palette in the
catalogue.
Put a surah header on your page
This is the whole integration, adapted from the code the demo on this site runs.
Find the asset in the catalogue
assets-index.jsonis one entry per asset. Look it up bytypeandstyle, and take the file name fromvariants.const catalog = await (await fetch("/demo/assets-index.json")).json(); const header = catalog.assets.find((a) => a.type === "surah-headers" && a.style === "mushaf-qalon"); header.variants.color; // "assets-surah-headers-mushaf-qalon-color.svg" header.slots[0]; // { role: "surah-name", x: 180.8571, y: 8.5714, w: 501.4286, h: 83.1429, cx: …, cy: … } header.license; // { id: "CC-BY-NC-SA-4.0", status: "provisional", … }Inline the SVG
An
<img src>renders the ornament but seals it: no CSS reaches inside, nothing can be positioned over the slot. Fetch the markup and put it in the DOM.async function mountOrnament(host: HTMLElement, file: string) { host.innerHTML = await (await fetch(`/demo/${file}`)).text(); const svg = host.querySelector("svg")!; svg.style.width = "100%"; // the files carry a viewBox and no width/height; height is always 100 return svg; } const svg = await mountOrnament(banner, header.variants.color);The root element of that file, trimmed to what you will use:
<svg viewBox="0 0 863.429 100" data-asset="surah-header" data-lineage="scan" data-mushaf="qalon" data-style="mushaf-qalon" data-variant="color" data-symmetry="4" data-slot="180.8571 8.5714 501.4286 83.1429">Convert the slot to percentages
data-slot— andslots[0]in the catalogue — isx y w hin viewBox units. Never type the numbers into your code: the Qālūn header keeps 58% of its width for the name, the Ḥafṣ Madinah mumtāza header 40%.function slotBox(svg: SVGSVGElement) { const [, , vbW, vbH] = svg.getAttribute("viewBox")!.split(/\s+/).map(Number); const [x, y, w, h] = svg.getAttribute("data-slot")!.split(/\s+/).map(Number); return { left: `${(x / vbW) * 100}%`, top: `${(y / vbH) * 100}%`, width: `${(w / vbW) * 100}%`, height: `${(h / vbH) * 100}%`, }; } slotBox(svg); // { left: "20.95%", top: "8.57%", width: "58.07%", height: "83.14%" } ← the Qālūn headerSlot units are neither pixels nor percentTreat
180.8571as pixels and the surah name lands 180 px from the left of a 300 px banner — off the artwork, with no error. Divide by the viewBox first, every time.Place the surah name over the slot
The container is
position: relative, the ornament fills it, and the name — from Quran Text, in its own font — sits absolutely inside the slot.const name = document.createElement("div"); name.dir = "rtl"; name.lang = "ar"; name.textContent = m.surah(2).nameAr; // from Quran Text — never typed in Object.assign(name.style, { position: "absolute", display: "grid", placeItems: "center", fontFamily: `"${m.font.family}"`, ...slotBox(svg), }); banner.style.position = "relative"; banner.append(name);The header slot is sized for a calligraphic surah name. Nothing enforces that; longer text overflows the ornament.
Do the same for the ayah marker
The marker contract is the same. Its slot has the role
ayah-numberand is sized for one to three digits. The font markers are the settled set — pick one whose licence isconfirmed:const marker = catalog.assets.find( (a) => a.type === "ayah-markers" && a.lineage === "font" && a.license.status === "confirmed", ); // e.g. ayah-markers/font-001-regular — Alkalami's U+06DD, OFL-1.1 // viewBox "0 0 99.456 100", data-slot "32.7218 40.5624 34.1679 19.1082" const roundel = await mountOrnament(afterAyah, marker.variants.color); const n = document.createElement("div"); n.textContent = String(ayah.number); // the edition's own number — 255 in Ḥafṣ, 253 in Warsh Object.assign(n.style, { position: "absolute", display: "grid", placeItems: "center", ...slotBox(roundel) }); afterAyah.style.position = "relative"; afterAyah.append(n);That number is the edition’s own count, read from the loaded muṣḥaf — the second reason the asset key and the edition key travel together.
Font markers carry data-slot but no slot groupA scan asset has a transparent
<g class="slot" data-part="slot">you can style; a font marker’scolor.svghas onlyc1…c5, with the slot given by thedata-slotattribute alone. Position overdata-slotas above and both lineages behave the same; a.slot { fill }rule only ever affects scan assets.Recolour by group
Colours are presentation attributes, so any CSS rule wins. Every drawable group carries
data-part, and a theme does not need to know the muṣḥaf:const palette: Record<string, string> = { c2: "#15705D", c3: "#D6AD64" }; svg.querySelectorAll<SVGGElement>("[data-part]").forEach((g) => { const part = g.getAttribute("data-part")!; if (part === "slot") return; // leave the slot transparent if (part === "line") { g.setAttribute("stroke", "#222"); return; } // stroked, not filled const printed = g.getAttribute("fill"); // keep it, so "as printed" can come back g.setAttribute("fill", palette[part] ?? printed!); });c1is very often the paper —#ffffffin every scan asset,#fff8e7in the Alkalami marker. Map it to a dark colour and the ornament turns inside out.data-symmetry="4"on a header means one quadrant is drawn and placed four times with<use>, so one fill change recolours all four corners.
Tile a page border
A frame’s color.svg has the aspect of the page it was scanned from — the Warsh frame is 0 0 68.317 100 —
and stretching it to your page distorts the corner motifs. Seven of the eight frames carry a slices block in the
catalogue instead, and three slice files, all in the frame’s own units:
"slices": {
"corner": { "w": 9.4557, "h": 6.4871 },
"repeat": { "h": 6.1572, "v": 6.2122 },
"corner_mode": "mirror",
"files": {
"corner": "assets-mushaf-warsh-slice-corner.svg",
"edge-h": "assets-mushaf-warsh-slice-edge-h.svg",
"edge-v": "assets-mushaf-warsh-slice-edge-v.svg"
}
}
Each slice file repeats the same facts on its root — viewBox, data-repeat, and data-frame-viewbox="0 0 68.3168 100",
the frame it was cut from. The corner is drawn once and mirrored into the other three positions (corner_mode is
"mirror" for all seven at this commit); the units repeat along the runs.
/** How many units fit a run, and how much each must stretch so the run ends at the corner. */
function run(total: number, unit: number) {
const count = Math.max(1, Math.round(total / unit));
return { count, step: total / count };
}
const { corner, repeat } = frame.slices;
const across = run(width - 2 * corner.w, repeat.h); // top and bottom
const down = run(height - 2 * corner.h, repeat.v); // left and right
for (let i = 0; i < across.count; i++) {
const x = corner.w + i * across.step;
const k = across.step / repeat.h;
place("edge-h", `translate(${x} 0) scale(${k} 1)`); // top
place("edge-h", `translate(${x} ${height}) scale(${k} -1)`); // bottom, mirrored
}
Leave the border its margin: the frame is drawn around the text, not over it, and slots[0] with role text-area
gives the inset — for Warsh, x 5.4455, y 5.3355, w 57.4257, h 89.3289 inside 0 0 68.317 100.
It has no slices block, because its border does not repeat cleanly and the project measured that rather than
guessed. Check for slices before you reach for a frame, and fall back to scaling its color.svg.
Switch a page between the eight muṣḥafs and the font markers, recolour any ink group, and watch a border tile itself from three files.
How this data is made
A scan asset is cropped from a page of a named scan, cleaned of text and paper, traced from one quadrant where it
is symmetric, and vectorised into the grouped SVG above; every file records the scan, page, crop box and the SHA-256
of the source PDF. A font asset is the U+06DD glyph of a named open font, in a named weight, decomposed into the
same group contract. The files on this page are the copies served under /demo/, built from
quran-ws/quran-assets at 591e8f5; the full contract is in the
Quran Assets reference.