module MarkpdfWeb

Defined in:

web.cr

Constant Summary

BUSY_RENDERS = Atomic(Int32).new(0)

How many requests may be waiting for (or running) a render before the server starts answering 429.

EMPTY_BASE_DIR = begin dir = File.join(Dir.tempdir, "markpdf-web-no-local-images") Dir.mkdir_p(dir) dir end

Local/relative image sources resolve against this directory, which is created empty and never written to: server files must not be reachable as "images" through a submitted markdown.

LANGUAGES = ["en", "es"] of ::String
MAX_CSS_BYTES = 64 * 1024
MAX_MARKDOWN_BYTES = MAX_MARKDOWN_KB * 1024
MAX_MARKDOWN_KB = env_int("MARKPDF_WEB_MAX_MARKDOWN_KB", 512)

All limits are env-tunable so a deployment can tighten them without a recompile.

MAX_RENDER_WAIT = env_int("MARKPDF_WEB_MAX_QUEUE", 8)
MAX_RENDERS_PER_MIN = env_int("MARKPDF_WEB_MAX_RENDERS_PER_MINUTE", 30)
MAX_TEXT_FIELD_CHARS = 500
RENDER_MUTEX = Mutex.new

Rendering runs through the C++ shim and libharu, whose thread-safety is unknown, and markpdf's font and emoji font registration are process-global. Serialize renders instead of finding out the hard way.

RENDER_TIMEOUT = (env_int("MARKPDF_WEB_MAX_RENDER_SECONDS", 30)).seconds
SAMPLES = [{title: "Feature tour", style: "default", pageless: false, markdown: "# markpdf feature tour\n\nEverything in this document is plain **Markdown**, rendered to PDF\nby *markpdf* — no LaTeX, no word processor, no online service.\n\n## GFM goodness\n\nTables, of course:\n\n| Feature | Status |\n|------------|--------|\n| Tables | ✅ |\n| Footnotes | ✅[^1] |\n| Task lists | ✅ |\n\n- [x] works in tables\n- [ ] and in lists\n\n> Blockquotes look nice, too — perfect for pull quotes and asides.\n\n> [!NOTE]\n> GitHub-style alerts are supported: note, tip, important,\n> warning, caution.\n\n## Code, highlighted\n\n```crystal\ndef fib(n : Int32) : Int64\n n < 2 ? 1i64 : fib(n - 1) + fib(n - 2)\nend\n\nputs fib(40)\n```\n\n## Math\n\nEuler was here: $e^{ipi} + 1 = 0$\n\n$$E = mc^2$$\n\n## Emoji 🎉 and [links](https://github.com/ralsina/markterm)\n\nNow play with the knobs: switch the **style**, pick a base16\n**theme**, toggle **hyphenation**, set a **header** or **footer**,\nor check **pageless** for one long page.\n\n[^1]: Rendered as endnotes: the notes collect at the end of the\ndocument, with backlinks."}, {title: "Book excerpt", style: "book", pageless: false, markdown: "# The Great Gatsby\n\n### F. Scott Fitzgerald\n\n> Then wear the gold hat, if that will move her\\\n> If you can bounce high, bounce for her too\\\n> Till she cry “Lover, gold-hatted, high-bouncing lover,\\\n> I must have you!”\n>\n> — *Thomas Parke d’Invilliers*\n\n## I\n\nIn my younger and more vulnerable years my father gave me some advice\nthat I’ve been turning over in my mind ever since.\n\n“Whenever you feel like criticizing anyone,” he told me, “just remember\nthat all the people in this world haven’t had the advantages that you’ve\nhad.”\n\nHe didn’t say any more, but we’ve always been unusually communicative in\na reserved way, and I understood that he meant a great deal more than\nthat. In consequence, I’m inclined to reserve all judgements, a habit\nthat has opened up many curious natures to me and also made me the\nvictim of not a few veteran bores. The abnormal mind is quick to detect\nand attach itself to this quality when it appears in a normal person,\nand so it came about that in college I was unjustly accused of being a\npolitician, because I was privy to the secret griefs of wild, unknown\nmen. Most of the confidences were unsought—frequently I have feigned\nsleep, preoccupation, or a hostile levity when I realized by some\nunmistakable sign that an intimate revelation was quivering on the\nhorizon; for the intimate revelations of young men, or at least the\nterms in which they express them, are usually plagiaristic and marred by\nobvious suppressions. Reserving judgements is a matter of infinite hope.\nI am still a little afraid of missing something if I forget that, as my\nfather snobbishly suggested, and I snobbishly repeat, a sense of the\nfundamental decencies is parcelled out unequally at birth.\n\nAnd, after boasting this way of my tolerance, I come to the admission\nthat it has a limit. Conduct may be founded on the hard rock or the wet\nmarshes, but after a certain point I don’t care what it’s founded on.\nWhen I came back from the East last autumn I felt that I wanted the\nworld to be in uniform and at a sort of moral attention forever; I\nwanted no more riotous excursions with privileged glimpses into the\nhuman heart. Only Gatsby, the man who gives his name to this book, was\nexempt from my reaction—Gatsby, who represented everything for which I\nhave an unaffected scorn. If personality is an unbroken series of\nsuccessful gestures, then there was something gorgeous about him, some\nheightened sensitivity to the promises of life, as if he were related to\none of those intricate machines that register earthquakes ten thousand\nmiles away. This responsiveness had nothing to do with that flabby\nimpressionability which is dignified under the name of the “creative\ntemperament”—it was an extraordinary gift for hope, a romantic readiness\nsuch as I have never found in any other person and which it is not\nlikely I shall ever find again. No—Gatsby turned out all right at the\nend; it is what preyed on Gatsby, what foul dust floated in the wake of\nhis dreams that temporarily closed out my interest in the abortive\nsorrows and short-winded elations of men.\n\n*Excerpt from Chapter I, public domain via Project Gutenberg.*"}, {title: "Screen reading", style: "dark", pageless: true, markdown: "# Reading, not printing\n\nPaper wants margins and page breaks. Screens want one long,\nscrollable page that gets out of the way. The **pageless** knob\ngives markpdf a screen brain: a single page exactly as tall as\nthe document, with headers and footers turned off.\n\n## What to notice\n\n- Light text on a dark page, easy on the eyes\n- Code that does not glow: the dark style picks a matching\n syntax theme automatically\n\n```python\ndef dark_mode(matters: bool = False) -> str:\n return \"easier on the eyes\"\n```\n\n> [!TIP]\n> Pageless output is great for sharing in chats and reading in\n> the browser's PDF viewer — and exactly wrong for the printer,\n> which is why it is a knob and not a default."}]

Class Method Summary

Class Method Detail

def self.env_int(name : String, default : Int32) : Int32 #

[View source]
def self.last_render_seconds : Float64 #

[View source]
def self.page_themes : Array(String) #

The base16 themes sixteen ships: the page-theme choices. Theme names from tartrazine's own roster (e.g. "monokai") color code blocks but not the page, so they are not offered here.


[View source]
def self.render_to_bytes(render_params : RenderParams) : Bytes #

[View source]
def self.render_to_bytes_limited(render_params : RenderParams) : Bytes #

Rendering cannot be interrupted once started (the C shim has no cancellation), but the client does not have to wait forever: the render runs in a fiber and this returns 503 when it outlives RENDER_TIMEOUT. The abandoned fiber finishes on its own and the render mutex stays fair for whoever is next.


[View source]