Tetris in a PDF
Recently, just for fun, I managed to create a playable version of Tetris inside a PDF. I posted about this a couple days ago on Hacker News and Twitter. You can play it by opening this file in a compatible desktop browser (Firefox and anything Chromium-based). The “source code” can be found here.
As there was quite some feedback, I’ll share a bit more context here.
Why?
Why not? I learned a bit about PDF’s JavaScript API and its implementations and realized there might be just enough I/O possibility there for a simple game.
How?
It is relatively well-known that PDFs can be quite feature-rich when opened in Adobe Acrobat/Reader, with scripting support for forms and other dynamic content. However, it turns out that both PDFium (Chromium’s PDF reader) and PDF.js (Firefox’s) implement a little bit of scripting support as well. This piqued my interest, as I tend to see them as more modern/static/safe1.
Both engines provide a sandboxed JavaScript runtime, which only has access to several PDF-specific APIs. Many APIs that were specced at some point (and even supported by some readers) are not implemented however, likely because they don’t make a lot of (security) sense from the perspective of a web browser. What is implemented, is mostly related to form-validation.
Forms? Yes, PDFs can contain text input fields, buttons, checkboxes, etcetera. JavaScript handlers can be tied to events on those widgets (“fields”), and properties of these fields can be modified from within JavaScript (they are referenced using this.getField("<field_name>")
). It is also possible to run JavaScript upon page-level events, such as when it loads, or when the user attempts to print the page.
Display
For Tetris, I just created a 10x20 grid of buttons (they can be styled however you want), which can be shown/hidden from JavaScript by setting their .hidden
property, creating monochrome pixels:
var pixel_fields = [];
function game_init() {
// Gather references to pixel field objects
for (var x = 0; x < 10; ++x) {
pixel_fields[x] = [];
for (var y = 0; y < 20; ++y) {
pixel_fields[x][y] = this.getField(`P_${x}_${y}`);
}
}
// ...
}
function set_pixel(x, y, state) {
// ...
pixel_fields[x][20 - 1 - y].hidden = !state;
}
Game loop and input
To make a proper game, there has to be a game loop. Luckily, the PDF JavaScript API has setInterval(...)
(and setTimeOut(...)
). With that, you can easily run update/draw logic X times per second.
Now we have enough primitives to implement something like game-of-life or possibly play Bad Apple. But to play a game, we need to be able to read inputs. Button click events are fine for this, but keyboard controls would be even better. Luckily, the /K
event of text-input fields emits an event with a .change
property. Upon a keystroke, this property contains the character that was inserted. With this, handling inputs becomes quite easy, though with the restrictions that the input box needs to be focused first, and only printable characters can be used (this is fine for Tetris):
function handle_input(event) {
switch (event.change) {
case 'w': rotate_piece(); break;
case 'a': move_left(); break;
case 'd': move_right(); break;
case 's': lower_piece(); break;
}
}
But can it run DOOM?
UPDATE: Yes! I got it running using ASCII-art output, see here.
Outdated notes below:
Of course I wondered this as well (I have some experience porting Doom). There are several constraints here, mostly from a display perspective, but it may be possible with the right tricks. Especially if a single PDF engine is targeted. I’ll leave my thoughts below, looking forward to seeing someone attempt this.
- Something like ASM.js might work, especially in PDFium where native V8 is used. With that, it should be possible to compile “anything” to PDF, from a computation perspective.
- The color of the “pixels” (
.fillColor
) cannot be updated in PDFium (but it can in PDF.js!), so having anything more than monochrome work in PDFium would require a button field for each “pixel” coordinate, for every color. Even for say a GameBoy screen (160x144x3) this is almost 70K fields, which slows things down way too much. - Even monochrome, high resolutions quickly result in a long load time, and a laggy update rate. However, for PDFium a workaround could be a “scanline”-like technique where a single row of pixels if moved verically across the screen. This might work because:
- PDFium allows changing a field’s
rect
, i.e. its position and size. - It also leaves a “windows solitaire”-like trace, i.e. when a field is moved, its previous location is not redrawn immediately.
- PDFium allows changing a field’s
- A simpler approach could be to use ASCII-art in a text field, or maybe even fancier text-based art using custom fonts.
- Finally, there’s something called XFA which seems to be an even more cursed addition to PDF, allowing for HTML-like content. While somewhat supported in PDF.js, it is not scriptable. In PDFium however, there seems to be support for scripting this, though it’s locked behind a feature flag. If enabled, this might allow for less resource-intensive ways of creating a “pixel”-grid, but I did not play with this.
-
Not that supporting JavaScript in PDFs is necessarily unsafe, but there seems to be a trend towards treating PDFs as more static things, maybe partially due to the history of security problems relating to scripting in Acrobat. ↩︎
❦