diff --git a/doc/install.md b/doc/install.md
index 61c7907e..87056cc8 100644
--- a/doc/install.md
+++ b/doc/install.md
@@ -14,7 +14,7 @@ cd paper-muncher
 export PATH=$PATH:$HOME/.local/bin
 
 # Render a webpage to PDF
-paper-muncher --unsecure print index.html -o output.pdf
+paper-muncher index.html -o output.pdf
 
 # For more options, run
 paper-muncher --help
diff --git a/doc/usage.md b/doc/usage.md
index 52578d81..6dd8c60f 100644
--- a/doc/usage.md
+++ b/doc/usage.md
@@ -2,14 +2,9 @@
 
 **Paper-Muncher** is a document rendering tool that converts web documents into high-quality **PDFs** or **rasterized images** using the Vaev layout engine. It supports HTTP and local I/O, and CSS units for layout configuration.
 
-## Commands
-
----
-
-### `print`
 
 ```
-paper-muncher --unsecure print  [options]
+paper-muncher  [options]
 ```
 
 Renders a web document to a print-ready file (typically PDF).
@@ -24,47 +19,16 @@ Renders a web document to a print-ready file (typically PDF).
 - `-h,--height `: Override paper height
 - `-f,--format `: Output format (default: `public.pdf`)
 - `-o,--output `: Output file or URL (default: stdout)
-- `--unsecure`: Allows paper-muncher to access local files and the network. If omitted it will only get access to stdin and stdout.
-- `--timeout `: Adds a timeout to the document generation, when reached exits with a failure code.
-- `-v,--verbose`: Makes paper-muncher be more talkative, it might yap about how its day's going
-
-**Examples:**
-
-```sh
-paper-muncher --unsecure print article.html -o out.pdf
-paper-muncher --unsecure print article.html -o out.pdf --paper Letter --orientation landscape
-paper-muncher --unsecure print article.html -o https://example.com/doc.pdf --output-mime application/pdf
-```
-
----
-
-### `render`
-
-```
-paper-muncher --unsecure render  [options]
-```
-
-Renders a web document to a raster image (BMP, PNG, etc.).
-
-**Options:**
-
-- `--scale `: CSS resolution (default: `96dpi`)
-- `--density `: Pixel density (default: `96dpi`)
-- `-w,--width `: Viewport width (default: `800px`)
-- `-h,--height `: Viewport height (default: `600px`)
-- `-f,--format `: Output format (default: `public.bmp`)
-- `--wireframe`: Show wireframe overlay of the layout
-- `-o,--output `: Output file or URL (default: stdout)
-- `--unsecure`: Allows paper-muncher to access local files and the network. If omitted it will only get access to stdin and stdout.
+- `--sandboxed`: Disallows paper-muncher to access local files and the network.
 - `--timeout `: Adds a timeout to the document generation, when reached exits with a failure code.
 - `-v,--verbose`: Makes paper-muncher be more talkative, it might yap about how its day's going
 
 **Examples:**
 
 ```sh
-paper-muncher --unsecure render page.html -o out.bmp
-paper-muncher --unsecure render page.html -o out.png --width 1024px --height 768px --density 192dpi
-paper-muncher --unsecure render page.html -o out.png --wireframe
+paper-muncher article.html -o out.pdf
+paper-muncher article.html -o out.pdf --paper Letter --orientation landscape
+paper-muncher article.html -o https://example.com/doc.pdf --output-mime application/pdf
 ```
 
 *NOTE: ``, ``, ``, ``, `` all use [CSS unit synthax](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Values_and_Units#units)*
diff --git a/meta/plugins/reftest.py b/meta/plugins/reftest.py
index 51257b0d..e02014ed 100644
--- a/meta/plugins/reftest.py
+++ b/meta/plugins/reftest.py
@@ -31,11 +31,11 @@ def fetchMessage(args: model.TargetArgs, type: str) -> str:
 
 
 def compareImages(
-        lhs: bytes,
-        rhs: bytes,
-        lowEpsilon: float = 0.05,
-        highEpsilon: float = 0.1,
-        strict=False,
+    lhs: bytes,
+    rhs: bytes,
+    lowEpsilon: float = 0.05,
+    highEpsilon: float = 0.1,
+    strict=False,
 ) -> bool:
     if strict:
         return lhs == rhs
@@ -62,7 +62,10 @@ def compareImages(
 
 
 def runPaperMuncher(executable, type, xsize, ysize, page, outputPath, inputPath):
-    command = ["--feature", "*=on", "--verbose", "--unsecure", type or "render"]
+    command = ["--feature", "*=on", "--verbose"]
+
+    if type == "print":
+        command.extend(["--flow", "paginate"])
 
     if xsize or not page:
         command.extend(["--width", (xsize or 200) + "px"])
@@ -197,7 +200,7 @@ def getInfo(txt):
                     expected_image_url = ref_image
 
             for tag, info, rendering in re.findall(
-                    r"""<(rendering|error)([^>]*)>([\w\W]+?)(?:rendering|error)>""", test
+                r"""<(rendering|error)([^>]*)>([\w\W]+?)(?:rendering|error)>""", test
             ):
                 renderingProps = getInfo(info)
                 test_skipped = category_skipped or "skip" in renderingProps
@@ -222,7 +225,9 @@ def getInfo(txt):
                     xsize = "800"
                     ysize = "600"
 
-                runPaperMuncher(paperMuncher, type, xsize, ysize, page, img_path, input_path)
+                runPaperMuncher(
+                    paperMuncher, type, xsize, ysize, page, img_path, input_path
+                )
 
                 with img_path.open("rb") as imageFile:
                     output_image: bytes = imageFile.read()
@@ -233,7 +238,7 @@ def getInfo(txt):
                     if not expected_image:
                         expected_image = output_image
                         with (TEST_REPORT / f"{counter}.expected.bmp").open(
-                                "wb"
+                            "wb"
                         ) as imageWriter:
                             imageWriter.write(expected_image)
                         continue
diff --git a/readme.md b/readme.md
index 7f27ef27..710d5756 100644
--- a/readme.md
+++ b/readme.md
@@ -32,7 +32,7 @@ Paper-Muncher is now in early alpha. We're currently focused on improving stabil
 # Basic usage
 
 ```bash
-paper-muncher --unsecure print index.html -o output.pdf
+paper-muncher index.html -o output.pdf
 ```
 
 # Introduction
@@ -62,7 +62,7 @@ cd paper-muncher
 export PATH=$PATH:$HOME/.local/bin
 
 # Render a webpage to PDF
-paper-muncher --unsecure --timeout=10s print index.html -o output.pdf
+paper-muncher index.html -o output.pdf
 
 # For more options, run
 paper-muncher --help
diff --git a/src/main.cpp b/src/main.cpp
index a1c45b7d..fe989fa3 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,356 +1,115 @@
 #include 
 
 import Karm.Cli;
-import Karm.Gc;
-import Karm.Http;
-import Karm.Image;
+import PaperMuncher;
 import Karm.Print;
-import Karm.Debug;
-import Karm.Sys;
-import Karm.Gfx;
-import Karm.Math;
 import Karm.Logger;
-import Karm.Scene;
 
 import Vaev.Engine;
 
 using namespace Karm;
 
-template <>
-struct Cli::ValueParser {
-    static Res<> usage(Io::TextWriter& w) {
-        return w.writeStr("margin"s);
-    }
-
-    static Res parse(Cursor& c) {
-        if (c.ended() or c->kind != Token::OPERAND)
-            return Error::invalidInput("expected margin");
-
-        auto value = c.next().value;
-
-        if (value == "default")
-            return Ok(Print::Margins::DEFAULT);
-        else if (value == "none")
-            return Ok(Print::Margins::NONE);
-        else if (value == "minimum")
-            return Ok(Print::Margins::MINIMUM);
-        else
-            return Error::invalidInput("expected margin");
-    }
-};
-
-namespace PaperMuncher {
-
-static Rc _createHttpClient(bool unsecure) {
-    Vec> transports;
-
-    transports.pushBack(Http::pipeTransport());
-
-    if (unsecure) {
-        transports.pushBack(Http::httpTransport());
-        transports.pushBack(Http::localTransport(Http::LocalTransportPolicy::ALLOW_ALL));
-    } else {
-        // NOTE: Only allow access to bundle assets and standard input/output.
-        transports.pushBack(Http::localTransport({"bundle"s, "fd"s, "data"s}));
-    }
-
-    auto client = makeRc(
-        Http::multiplexTransport(std::move(transports))
-    );
-    client->userAgent = "Paper-Muncher/" stringify$(__ck_version_value) ""s;
-
-    return client;
-}
-
-struct PrintOption {
-    Vaev::Resolution scale = Vaev::Resolution::fromDppx(1);
-    Vaev::Resolution density = Vaev::Resolution::fromDppx(1);
-    Opt width = NONE;
-    Opt height = NONE;
-    Print::PaperStock paper = Print::A4;
-    Print::Orientation orientation = Print::Orientation::PORTRAIT;
-    Print::Margins margins = Print::Margins::DEFAULT;
-    Ref::Uti outputFormat = Ref::Uti::PUBLIC_PDF;
-
-    auto preparePrintSettings(this auto const& self) -> Print::Settings {
-        Vaev::Layout::Resolver resolver;
-        resolver.viewport.dpi = self.scale;
-
-        auto paper = self.paper;
-
-        if (self.orientation == Print::Orientation::LANDSCAPE)
-            paper = paper.landscape();
-
-        if (self.width) {
-            paper.name = "custom";
-            paper.width = resolver.resolve(*self.width).template cast();
-        }
-
-        if (self.height) {
-            paper.name = "custom";
-            paper.height = resolver.resolve(*self.height).template cast();
-        }
-
-        return {
-            .paper = paper,
-            .margins = self.margins,
-            .scale = self.scale.toDppx(),
-        };
-    }
-};
-
-static Async::Task<> printAsync(
-    Rc client,
-    Vec const& inputs,
-    Ref::Url const& output,
-    PrintOption options = {}
-) {
-    auto printer = co_try$(
-        Print::FilePrinter::create(
-            options.outputFormat,
-            {
-                .density = options.density.toDppx(),
-            }
-        )
-    );
-
-    for (auto& input : inputs) {
-        logInfo("rendering {}...", input);
-        auto window = Vaev::Dom::Window::create(client);
-        co_trya$(window->loadLocationAsync(input));
-        window->print(options.preparePrintSettings()) | forEach([&](Print::Page& page) {
-            page.print(
-                *printer,
-                {
-                    .showBackgroundGraphics = true,
-                }
-            );
-        });
-    }
-
-    logInfo("saving {}...", output);
-    Io::BufferWriter bw;
-    co_try$(printer->write(bw));
-    co_trya$(client->doAsync(
-        Http::Request::from(
-            Http::Method::PUT,
-            output,
-            Http::Body::from(bw.take())
-        )
-    ));
-
-    co_return Ok();
-}
-
-struct RenderOption {
-    Vaev::Resolution scale = Vaev::Resolution::fromDpi(96);
-    Vaev::Resolution density = Vaev::Resolution::fromDpi(96);
-    Vaev::Length width = 800_au;
-    Vaev::Length height = 600_au;
-    Ref::Uti outputFormat = Ref::Uti::PUBLIC_BMP;
-};
-
-static Async::Task<> renderAsync(
-    Rc client,
-    Ref::Url const& input,
-    Ref::Url const& output,
-    RenderOption options = {}
-) {
-    auto window = Vaev::Dom::Window::create(client);
-    co_trya$(window->loadLocationAsync(input));
-    Vaev::Layout::Resolver resolver;
-    Vec2Au imageSize = {
-        resolver.resolve(options.width),
-        resolver.resolve(options.height),
+Async::Task<> entryPointAsync(Sys::Context& ctx) {
+    auto sandboxedArg = Cli::flag(NONE, "sandboxed"s, "Disallow local file and http access"s);
+    auto verboseArg = Cli::flag(NONE, "verbose"s, "Makes paper-muncher be more talkative, it might yap about how its day's going"s);
+    Cli::Section runtimeSection{
+        "Runtime Options"s,
+        {sandboxedArg, verboseArg},
     };
-    window->changeMedia(Vaev::Style::Media::forRender(imageSize.cast(), options.scale));
-
-    Rc body = Http::Body::empty();
-
-    Image::Saver saves{.format = options.outputFormat, .density = options.density.toDppx()};
-
-    auto scene = window->render();
-
-    // NOTE: Override the background of HTML document, since no
-    //       one really expect a html document to be transparent
-    if (window->document()->documentElement()->namespaceUri() == Vaev::Html::NAMESPACE) {
-        scene = makeRc(scene, Gfx::WHITE);
-    }
 
-    body = Http::Body::from(co_try$(Image::save(scene, imageSize.cast(), saves)));
-
-    co_trya$(client->doAsync(
-        Http::Request::from(Http::Method::PUT, output, body)
-    ));
-    co_return Ok();
-}
-
-} // namespace PaperMuncher
-
-Async::Task<> entryPointAsync(Sys::Context& ctx) {
-    auto inputArg = Cli::operand("input"s, "Input files (default: stdin)"s, "-"s);
     auto inputsArg = Cli::operand>("inputs"s, "Input files (default: stdin)"s, {"-"s});
     auto outputArg = Cli::option('o', "output"s, "Output file (default: stdout)"s, "-"s);
     auto formatArg = Cli::option('f', "format"s, "Override the output file format"s, ""s);
-    auto unsecureArg = Cli::flag(NONE, "unsecure"s, "Allow local file and http access"s);
-    auto verboseArg = Cli::flag(NONE, "verbose"s, "Makes paper-muncher be more talkative, it might yap about how its day's going"s);
+    auto densityArg = Cli::option(NONE, "density"s, "Density of the output document in css units (e.g. 96dpi)"s, "1x"s);
 
-    Cli::Command cmd{
-        "paper-muncher"s,
-        "Munch the web into crisp documents"s,
-        {
-            {
-                "Global Options"s,
-                {
-                    unsecureArg,
-                    verboseArg,
-                },
-            },
-        },
-        [=](Sys::Context&) -> Async::Task<> {
-            setLogLevel(verboseArg.value() ? PRINT : ERROR);
-            if (not unsecureArg.value())
-                co_try$(Sys::enterSandbox());
-            co_return Ok();
-        }
+    Cli::Section inOutSection{
+        "Input/Output Options"s,
+        {inputsArg, outputArg, formatArg, densityArg},
     };
 
-    auto scaleArg = Cli::option(NONE, "scale"s, "Scale of the input document in css units (e.g. 1x)"s, "1x"s);
-    auto densityArg = Cli::option(NONE, "density"s, "Density of the output document in css units (e.g. 96dpi)"s, "1x"s);
-    auto widthArg = Cli::option(NONE, "width"s, "Width of the output document in css units (e.g. 800px)"s, ""s);
-    auto heightArg = Cli::option(NONE, "height"s, "Height of the output document in css units (e.g. 600px)"s, ""s);
     auto paperArg = Cli::option(NONE, "paper"s, "Paper size for printing (default: A4)"s, "A4"s);
     auto orientationArg = Cli::option(NONE, "orientation"s, "Page orientation (default: portrait)"s, "portrait"s);
-    auto marginArg = Cli::option(NONE, "margins"s, "Page margins (default: default)"s, Print::Margins::DEFAULT);
-
-    cmd.subCommand(
-        "print"s,
-        "Render a web page into a printable document"s,
-        {
-            {
-                "Input/Output Options"s,
-                {
-                    inputsArg,
-                    outputArg,
-                    formatArg,
-                    densityArg,
-                },
-            },
-
-            {
-                "Paper Options"s,
-                {
-                    paperArg,
-                    orientationArg,
-                    marginArg,
-                },
-            },
-
-            {
-                "Viewport Options"s,
-                {
-                    widthArg,
-                    heightArg,
-                    scaleArg,
-                },
-            },
-        },
-        [=](Sys::Context&) -> Async::Task<> {
-            PaperMuncher::PrintOption options{};
-
-            options.scale = co_try$(Vaev::parseValue(scaleArg.value()));
-            options.density = co_try$(Vaev::parseValue(densityArg.value()));
-
-            if (widthArg.value())
-                options.width = co_try$(Vaev::parseValue(widthArg.value()));
-
-            if (heightArg.value())
-                options.height = co_try$(Vaev::parseValue(heightArg.value()));
-
-            options.paper = co_try$(Print::findPaperStock(paperArg.value()));
-            options.orientation = co_try$(Vaev::parseValue(orientationArg.value()));
-            options.margins = marginArg.value();
-
-            Vec inputs;
-            for (auto& i : inputsArg.value())
-                if (i == "-"s)
-                    inputs.pushBack("fd:stdin"_url);
-                else
-                    inputs.pushBack(Ref::parseUrlOrPath(i, co_try$(Sys::pwd())));
+    auto marginArg = Cli::option(NONE, "margins"s, "Page margins (default: default)"s, Print::Margins::DEFAULT);
+    Cli::Section paperSection{
+        "Paper Options"s,
+        {paperArg, orientationArg, marginArg},
+    };
 
-            Ref::Url output = "fd:stdout"_url;
-            if (outputArg.value() != "-"s)
-                output = Ref::parseUrlOrPath(outputArg.value(), co_try$(Sys::pwd()));
+    auto widthArg = Cli::option(NONE, "width"s, "Width of the output document in css units (e.g. 800px)"s, ""s);
+    auto heightArg = Cli::option(NONE, "height"s, "Height of the output document in css units (e.g. 600px)"s, ""s);
+    auto scaleArg = Cli::option(NONE, "scale"s, "Scale of the input document in css units (e.g. 1x)"s, "1x"s);
+    auto extendArg = Cli::option(NONE, "extend"s, "How content extending past the initial viewport is handled (default: crop)"s, PaperMuncher::Extend::CROP);
+    auto flowArg = Cli::option(NONE, "flow"s, "Flow of the document (default: paginate for PDF, otherwise continuous)"s, PaperMuncher::Flow::AUTO);
 
-            if (formatArg.value() != ""s) {
-                options.outputFormat = co_try$(Ref::Uti::fromMime({formatArg.value()}));
-            } else {
-                auto mime = Ref::sniffSuffix(output.path.suffix());
-                options.outputFormat = mime ? co_try$(Ref::Uti::fromMime(*mime)) : Ref::Uti::PUBLIC_PDF;
-            }
+    Cli::Section viewportSection{
+        "Viewport Options"s,
+        {widthArg, heightArg, scaleArg, extendArg, flowArg},
+    };
 
-            auto client = PaperMuncher::_createHttpClient(unsecureArg.value());
+    Cli::Section formatSection{
+        .title = "Supported Formats"s,
+        .prolog =
+            "Input: HTML, XHTML, SVG\n"
+            "Output: PDF or image\n"
+            "Image formats: BMP, PNG, JPEG, TGA, QOI, SVG\n"s
+    };
 
-            co_return co_await PaperMuncher::printAsync(client, inputs, output, options);
+    Cli::Command cmd{
+        "paper-muncher"s,
+        "Convert web pages (HTML, XHTML, or SVG) into printable or viewable documents like PDFs or images."s,
+        {
+            runtimeSection,
+            inOutSection,
+            paperSection,
+            viewportSection,
+            formatSection,
         }
-    );
+    };
 
-    cmd.subCommand(
-        "render"s,
-        "Render a web page into an image"s,
-        {
-            {
-                "Input/Output Options"s,
-                {
-                    inputArg,
-                    outputArg,
-                    formatArg,
-                    densityArg,
-                },
-            },
+    co_trya$(cmd.execAsync(ctx));
+    if (not cmd)
+        co_return Ok();
 
-            {
-                "Viewport Options"s,
-                {
+    setLogLevel(verboseArg.value() ? PRINT : ERROR);
+    if (sandboxedArg.value())
+        co_try$(Sys::enterSandbox());
 
-                    widthArg,
-                    heightArg,
-                    scaleArg,
-                },
-            },
-        },
-        [=](Sys::Context&) -> Async::Task<> {
-            PaperMuncher::RenderOption options{};
+    PaperMuncher::Option options{};
 
-            options.scale = co_try$(Vaev::parseValue(scaleArg.value()));
-            options.density = co_try$(Vaev::parseValue(densityArg.value()));
+    options.scale = co_try$(Vaev::parseValue(scaleArg.value()));
+    options.density = co_try$(Vaev::parseValue(densityArg.value()));
 
-            if (widthArg.value())
-                options.width = co_try$(Vaev::parseValue(widthArg.value()));
+    if (widthArg.value())
+        options.width = co_try$(Vaev::parseValue(widthArg.value()));
 
-            if (heightArg.value())
-                options.height = co_try$(Vaev::parseValue(heightArg.value()));
+    if (heightArg.value())
+        options.height = co_try$(Vaev::parseValue(heightArg.value()));
 
-            Ref::Url input = "fd:stdin"_url;
-            if (inputArg.value() != "-"s)
-                input = Ref::parseUrlOrPath(inputArg.value(), co_try$(Sys::pwd()));
+    options.paper = co_try$(Print::findPaperStock(paperArg.value()));
+    options.orientation = co_try$(Vaev::parseValue(orientationArg.value()));
+    options.margins = marginArg.value();
 
-            Ref::Url output = "fd:stdout"_url;
-            if (outputArg.value() != "-"s)
-                output = Ref::parseUrlOrPath(outputArg.value(), co_try$(Sys::pwd()));
+    Vec inputs;
+    for (auto& i : inputsArg.value())
+        if (i == "-"s)
+            inputs.pushBack("fd:stdin"_url);
+        else
+            inputs.pushBack(Ref::parseUrlOrPath(i, co_try$(Sys::pwd())));
 
-            if (formatArg.value() != ""s) {
-                options.outputFormat = co_try$(Ref::Uti::fromMime({formatArg.value()}));
-            } else {
-                auto mime = Ref::sniffSuffix(output.path.suffix());
-                options.outputFormat = mime ? co_try$(Ref::Uti::fromMime(*mime)) : Ref::Uti::PUBLIC_BMP;
-            }
+    Ref::Url output = "fd:stdout"_url;
+    if (outputArg.value() != "-"s)
+        output = Ref::parseUrlOrPath(outputArg.value(), co_try$(Sys::pwd()));
 
-            auto client = PaperMuncher::_createHttpClient(unsecureArg.value());
+    if (formatArg.value() != ""s) {
+        options.outputFormat = co_try$(Ref::Uti::fromMime({formatArg.value()}));
+    } else {
+        auto mime = Ref::sniffSuffix(output.path.suffix());
+        options.outputFormat = mime ? co_try$(Ref::Uti::fromMime(*mime)) : Ref::Uti::PUBLIC_PDF;
+    }
 
-            co_return co_await renderAsync(client, input, output, options);
-        }
-    );
+    options.flow = flowArg.value();
+    options.extend = extendArg.value();
 
-    co_return co_await cmd.execAsync(ctx);
+    auto client = PaperMuncher::defaultHttpClient(sandboxedArg.value());
+    co_return co_await PaperMuncher::run(client, inputs, output, options);
 }
diff --git a/src/mod.cpp b/src/mod.cpp
new file mode 100644
index 00000000..4475af63
--- /dev/null
+++ b/src/mod.cpp
@@ -0,0 +1,186 @@
+module;
+
+#include 
+
+export module PaperMuncher;
+
+import Karm.Gc;
+import Karm.Http;
+import Karm.Image;
+import Karm.Print;
+import Karm.Debug;
+import Karm.Sys;
+import Karm.Gfx;
+import Karm.Math;
+import Karm.Logger;
+import Karm.Scene;
+import Karm.Core;
+import Karm.Ref;
+
+import Vaev.Engine;
+
+using namespace Karm;
+
+namespace PaperMuncher {
+
+export enum struct Flow {
+    AUTO,
+    PAGINATE,
+    CONTINUOUS,
+    _LEN,
+};
+
+export enum struct Extend {
+    CROP, //< The document is cropped to the container
+    FIT,  //< Container is resized to fit the document
+    _LEN,
+};
+
+export Rc defaultHttpClient(bool sandboxed) {
+    Vec> transports;
+
+    transports.pushBack(Http::pipeTransport());
+
+    if (sandboxed) {
+        // NOTE: Only allow access to bundle assets and standard input/output.
+        transports.pushBack(Http::localTransport({"bundle"s, "fd"s, "data"s}));
+    } else {
+        transports.pushBack(Http::httpTransport());
+        transports.pushBack(Http::localTransport(Http::LocalTransportPolicy::ALLOW_ALL));
+    }
+
+    auto client = makeRc(
+        Http::multiplexTransport(std::move(transports))
+    );
+    client->userAgent = "Paper-Muncher/" stringify$(__ck_version_value) ""s;
+
+    return client;
+}
+
+export struct Option {
+    Vaev::Resolution scale = Vaev::Resolution::fromDppx(1);
+    Vaev::Resolution density = Vaev::Resolution::fromDppx(1);
+    Opt width = NONE;
+    Opt height = NONE;
+    Print::PaperStock paper = Print::A4;
+    Print::Orientation orientation = Print::Orientation::PORTRAIT;
+    Print::Margins margins = Print::Margins::DEFAULT;
+    Ref::Uti outputFormat = Ref::Uti::PUBLIC_DATA;
+    Flow flow = Flow::AUTO;
+    Extend extend = Extend::CROP;
+
+    auto preparePrintSettings() -> Print::Settings {
+        Vaev::Layout::Resolver resolver;
+        resolver.viewport.dpi = this->scale;
+
+        auto paper = this->paper;
+
+        if (this->orientation == Print::Orientation::LANDSCAPE)
+            paper = paper.landscape();
+
+        if (this->width or this->height) {
+            paper.name = "custom";
+            if (this->width)
+                paper.width = resolver.resolve(*this->width).cast();
+
+            if (this->height)
+                paper.height = resolver.resolve(*this->height).cast();
+        }
+
+        return {
+            .paper = paper,
+            .margins = this->margins,
+            .scale = this->scale.toDppx(),
+        };
+    }
+
+    Vaev::Style::Media prepareMedia() {
+        Vaev::Layout::Resolver resolver;
+        auto width = this->width ? resolver.resolve(*this->width) : 800_au;
+        auto height = this->height ? resolver.resolve(*this->height) : 600_au;
+        return Vaev::Style::Media::forRender({width, height}, this->scale);
+    }
+};
+
+export Async::Task<> run(
+    Rc client,
+    Vec const& inputs,
+    Ref::Url const& output,
+    Option options = {}
+) {
+    if (options.flow == Flow::AUTO)
+        options.flow =
+            options.outputFormat == Ref::Uti::PUBLIC_PDF
+                ? Flow::PAGINATE
+                : Flow::CONTINUOUS;
+
+    auto printer = co_try$(
+        Print::FilePrinter::create(
+            options.outputFormat,
+            {
+                .density = options.density.toDppx(),
+            }
+        )
+    );
+
+    for (auto& input : inputs) {
+        logInfo("loading {}...", input);
+        auto window = Vaev::Dom::Window::create(client);
+        co_trya$(window->loadLocationAsync(input));
+
+        logInfo("rendering {}...", input);
+        if (options.flow == Flow::PAGINATE) {
+            auto settings = options.preparePrintSettings();
+            window->print(settings) | forEach([&](Print::Page& page) {
+                page.print(
+                    *printer,
+                    {.showBackgroundGraphics = true}
+                );
+            });
+        } else {
+            auto media = options.prepareMedia();
+            window->changeMedia(media);
+
+            auto scene = window->render();
+
+            // NOTE: Override the background of HTML document, since no
+            //       one really expect a html document to be transparent
+            if (window->document()->documentElement()->namespaceUri() == Vaev::Html::NAMESPACE) {
+                scene = makeRc(scene, Gfx::WHITE);
+            }
+
+            Print::PaperStock paper{
+                "image",
+                media.width.cast(),
+                media.height.cast(),
+            };
+
+            if (options.extend == Extend::FIT) {
+                auto overflow = window->ensureRender().frag->scrollableOverflow();
+                paper.width = overflow.width.cast();
+                paper.height = overflow.height.cast();
+            }
+
+            Print::Page page = {paper, scene};
+            page.print(
+                *printer,
+                {.showBackgroundGraphics = true}
+            );
+        }
+    }
+
+    logInfo("saving {}...", output);
+    Io::BufferWriter bw;
+    co_try$(printer->write(bw));
+    co_trya$(client->doAsync(
+        Http::Request::from(
+            Http::Method::PUT,
+            output,
+            Http::Body::from(bw.take())
+        )
+    ));
+
+    co_return Ok();
+}
+
+} // namespace PaperMuncher
\ No newline at end of file