Book Publishing from Google Docs - Paginating Web Based Documents


This is part of a series of posts on my recent venture of using Google Docs to publish our first book on Amazon Kindle Direct Publishing (KDP).

Paginating Web Based Documents

One of the biggest hurdles I ran into with book printing from a web browser is pagination. There’s not really a good way to visualize where pages will break until you print a document.

Approach 1 - Page Elements

One approach to pagination would be to introduce container elements representing pages and then distribute content elements across the page elements. The big challenge with this approach is that it requires iteratively placing content elements into page containers, measuring the current height, and then moving to the next page whenever the space has been exhausted in the current page. This can potentially require a lot of moving of DOM elements in and out of parent elements which I didn’t want to do.

Approach 2 - Page Offsets

The second approach to pagination (the one I went with) is to measure the height of each content element and to offset the first element of each page. This looks something like the following:

  • Iterate over all content elements (paragraphs, sections, headers, etc.)
  • Calculate the height of each element keeping a tally of the total
  • Any time an element’s height causes the total to overflow the current page’s content height, we want this element to move to the next page. We can do this by setting a vertical offset from the top of this element to the bottom of the previous one.

Pagination with Offsets

There’s a little bit of math involved, but this doesn’t require introducing any additional page elements, and the content elements can remain direct children of the document. There are some additional nuances such as places where I wanted explicit page breaks, but the general methodology is pretty straightforward and worked well.

The pagination code looks something like this:

const PAGE_HEIGHT = 11 * 96 // 11 inches
const PAGE_CONTENT_HEIGHT = 9 * 96 // 9 inches

let pageCount = 1
let positionInPageContent = 0
let prevRect: DOMRect = new DOMRect()
let pageTop = 0

const parentRect = docEl.getBoundingClientRect()

for (const el of docEl.children) {
  if (el instanceof HTMLElement) {
    const marginBottom = Number(
      getComputedStyle(el).marginBottom.replace('px', ''),
    )
    const rect = el.getBoundingClientRect()
    const heightPlusMargin = rect.height + marginBottom
    positionInPageContent += heightPlusMargin

    if (positionInPageContent >= PAGE_CONTENT_HEIGHT) {
      pageTop = pageCount * PAGE_HEIGHT + parentRect.top

      const pageTopOffset = pageTop - prevRect.bottom

      // Stylesheet will use `var(--offset)` to set
      // `margin-top` on the first element of the page
      el.style.setProperty('--offset', `${pageTopOffset}px`)

      positionInPageContent = heightPlusMargin
      ++pageCount
    } else {
      // Cleanup any offsets from previous passes in
      // case content has changed
      el.style.removeProperty('--offset')
    }

    prevRect = rect
  }
}

This is probably enough for now. Stay tuned for more details.

If you are looking for a good Bible study workbook for your teenager, you can purchase my wife’s book Identified: Ephesians Bible Study for Youth on Amazon.