diff --git a/README.md b/README.md
index 473ad01..99d6002 100644
--- a/README.md
+++ b/README.md
@@ -340,6 +340,18 @@ These two functions return an object that implements the following interface:
- `value`: a string; the default value of the parameter
- `.search(map)`: a function, whose argument is a `Map` whose values are `Map`s (i.e. a two-dimensional map). The first key is the namespace of the search parameter. For non-namespaced parameters, the first key must be `null`. The second key is the parameter's name. Returns a string representing the URL of the search results.
+### Generating Images for Quotes
+
+With `quote-image.js`, one can generate shareable images for quotes:
+
+```js
+document.querySelector('foliate-quoteimage').getBlob({
+ title: 'The Time Machine',
+ author: 'H. G. Wells',
+ text: 'Can an instantaneous cube exist?',
+})
+```
+
### Supported Browsers
The main use of the library is for use in [Foliate](https://github.com/johnfactotum/foliate), which uses WebKitGTK. As such it's the only engine that has been tested extensively. But it should also work in Chromium and Firefox.
diff --git a/quote-image.js b/quote-image.js
new file mode 100644
index 0000000..29eadf5
--- /dev/null
+++ b/quote-image.js
@@ -0,0 +1,83 @@
+const SVG_NS = 'http://www.w3.org/2000/svg'
+
+// bisect
+const fit = (el, a = 1, b = 50) => {
+ const c = Math.floor(a + (b - a) / 2)
+ el.style.fontSize = `${c}px`
+ if (b - a === 1) return
+ if (el.scrollHeight > el.clientHeight
+ || el.scrollWidth > el.clientWidth) fit(el, a, c)
+ else fit(el, c, b)
+}
+
+const width = 540
+const height = 540
+const pixelRatio = 2
+
+const html = `
+
+
+ “
+
+
+
+`
+
+// TODO: lang, vertical writing
+customElements.define('foliate-quoteimage', class extends HTMLElement {
+ #root = this.attachShadow({ mode: 'closed' })
+ constructor() {
+ super()
+ this.#root.innerHTML = html
+ }
+ async getBlob({ title, author, text }) {
+ this.#root.querySelector('#title').textContent = title
+ this.#root.querySelector('#author').textContent = author
+ this.#root.querySelector('#text').innerText = text
+
+ fit(this.#root.querySelector('main'))
+
+ const img = document.createElement('img')
+ return new Promise(resolve => {
+ img.onload = () => {
+ const canvas = document.createElement('canvas')
+ canvas.width = pixelRatio * width
+ canvas.height = pixelRatio * height
+ const ctx = canvas.getContext('2d')
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
+ canvas.toBlob(resolve)
+ }
+ const doc = document.implementation.createDocument(SVG_NS, 'svg')
+ doc.documentElement.setAttribute('viewBox', `0 0 ${width} ${height}`)
+ const obj = doc.createElementNS(SVG_NS, 'foreignObject')
+ obj.setAttribute('width', width)
+ obj.setAttribute('height', height)
+ obj.append(doc.importNode(this.#root.querySelector('main'), true))
+ doc.documentElement.append(obj)
+ img.src = 'data:image/svg+xml;charset=utf-8,'
+ + new XMLSerializer().serializeToString(doc)
+ })
+ }
+})