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]+?)""", test + r"""<(rendering|error)([^>]*)>([\w\W]+?)""", 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