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 -o [options]
+paper-muncher -o [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 -o [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