Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Comments on Playwright script to generate PDF of a page that requires scrolling (Khan Academy)

Parent

Playwright script to generate PDF of a page that requires scrolling (Khan Academy)

+1
−0

Currently I am using One Click Page to PDF to get the job done, although the lack of working open-source tools fit for this task has led me to creating my own; the web page "that won't print" is this Khan Academy article.

It seems as though I have 2 options, both involving Playwright:

A. (Somehow) embed text on a page screenshot

  • pros:
    • I can get the whole page all at once without worrying about bits being cut off
  • cons:
    • positioning text layer in the right place is likely going to be a pain
    • splitting one long 'scroll' into PDF pages will be a fiddly process

B. Generate a single-page PDF of what is currently visible, scroll just enough for the next page and repeat, combining all PDFs into one

  • pros:
    • no awkward image-to-PDF-with-text conversion required (pagination and text layer come out of the box)
  • cons:
    • figuring out just how much to scroll won't be trivial (easy to under/overshoot)
    • page header and footer removal is required (otherwise they would be repeated for every page)

Before I embarked in this quest, I was curious to hear opinions from the more experienced!

I will try to code in Javascript as I understand this gives greater flexibility in Playwright, although if I can I would generally find my way easier in Python - how do you think I should tackle this problem aiming for a simple and effective solution?

Web2PDF is likely the closest base I can work from: it runs directly in browser, so as a program it is simpler to work with and more straightforward to debug.

History

1 comment thread

clarifying the problem (2 comments)
Post
+2
−0

I'm not sure what you've tried or where you're stuck with your attempts exactly, but I can answer generally as this is a common scenario. It's a good illustration that there are rarely one-size-fits-all solutions in web scraping.

If there's a scroll container like this cutting off content, you'll need to "pop" the content out of the container so it scrolls at the top level of the page. I typically use this to do so, removing or hiding all elements except for those in the subtree of the container.

But doing a hard strip here crashes the page, so this script:

  • forces the scroll container element tree to be visible
  • makes all other elements invisible
  • sets general styles to make the visible elements look presentable
  • finally, captures the PDF.
const {chromium} = require("playwright"); // ^1.58.0

const url = "https://www.khanacademy.org/math/ap-calculus-ab/ab-diff-analytical-applications-new/ab-5-6b/a/review-analyzing-the-second-derivative-to-find-inflection-points";

let browser;
(async () => {
  browser = await chromium.launch({headless: true});
  const width = 1200;
  const page = await browser.newPage({
    viewport: {width, height: 1200},
  });
  await page.goto(url, {waitUntil: "networkidle"});
  await page
    .locator('[data-testid="content-panel-wrapper"]')
    .evaluate(target => {
      for (let el = target; el; el = el.parentElement) {
        el.style.overflow = "visible";
        el.style.overflowY = "visible";
        el.style.overflowX = "visible";
        el.style.height = "auto";
        el.style.maxHeight = "none";
        el.style.minHeight = "0";
        el.style.position = "static";
      }

      const keep = new Set();
      keep.add(target);
      target.querySelectorAll("*").forEach(el => keep.add(el));

      for (
        let el = target.parentElement;
        el;
        el = el.parentElement
      ) {
        keep.add(el);
      }

      document.querySelectorAll("body *").forEach(el => {
        if (!keep.has(el)) {
          el.style.display = "none";
        }
      });

      target.style.position = "absolute";
      target.style.left = "0";
      target.style.top = "0";
      target.style.width = "100%";
      target.style.maxWidth = "none";
      target.style.margin = "0";
      target.style.padding = "20px";
      document.body.style.margin = "0";
      document.body.style.padding = "0";
      document.body.style.overflow = "visible";
      document.documentElement.style.overflow = "visible";
    });

  await page.waitForTimeout(1000);
  const height = await page.evaluate(() =>
    Math.max(
      document.body.scrollHeight,
      document.documentElement.scrollHeight
    )
  );
  await page.setViewportSize({width, height});
  await page.pdf({
    path: "khan.pdf",
    printBackground: true,
    width: `${width}px`,
    height: `${height}px`,
    margin: {
      top: "0",
      right: "0",
      bottom: "0",
      left: "0",
    },
  });
  console.log("Saved khan.pdf");
})()
  .finally(() => browser?.close());
History

1 comment thread

@ggorlen thank you, it works great! My only concern is: can we tell Playwright when to page break... (5 comments)
@ggorlen thank you, it works great! My only concern is: can we tell Playwright when to page break...
Elefy‭ wrote 3 months ago

@ggorlen thank you, it works great!

My only concern is: can we tell Playwright when to page break inside your script so that it outputs A4 sheets?

From my testing, changing any height-related variable in this code will not work as it is currently 'slurping' up the entire page to feed into Playwright. Maybe browser needs to generate a single PDF page, scroll a bit and repeat?

ggorlen‭ wrote 3 months ago · edited 3 months ago

Try await page.pdf({path: "khan.pdf", format: "A4", scale: 0.8});.

Elefy‭ wrote 3 months ago

Ah yes, that is it. I also tried await page.pdf({path: "khan.pdf", format: "A4", scale: 0.8, margin: { top: "3", right: "1", bottom: "3", left: "1"}, outline:true}); to add margins and an outline, but both of those silently failed for some reason...

ggorlen‭ wrote 3 months ago · edited 3 months ago

Try adding units: "0.25in" or "1em" etc.

outline probably needs <h1> headers and so forth, so you may need to make these yourself.

Elefy‭ wrote 3 months ago · edited 3 months ago

You're right: em won't parse, at least inside margin, but in will, so what worked in the end was

await page.pdf({path: "khan.pdf", format: "A4", scale: 0.8, margin: { top: "3in", right: "1in", bottom: "3in", left: "1in"}, outline:true, tagged:true});

I also noticed from https://stackoverflow.com/a/79792675 that tagged:true is needed to make the outline actually appear in the final PDF, and just as you said headings are used to construct it, so to modify the outline writing some extra javascript to first edit the HTML would be necessary: I'd definitely look into doing that in the future, although for now I can get a bit more control semi-automatically with pdftocgen.