Skip to content

Latest commit

 

History

36 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PaperView

npm version License: MIT npm downloads

A JavaScript library for paged media viewing. Fork of paged.js with enhanced support for browser, Angular, and Vue.

PaperView Screenshot


Table of Contents


Features

  • Paged Media Rendering: Chunks documents into paginated flows with print styles
  • CSS @page Support: Full support for @page rules, margins, sizes, and named pages
  • Page Breaks: break-before, break-after, break-inside with left/right/recto/verso
  • Generated Content: Running headers, string-sets, target-counters, target-text
  • Page Numbering: Configurable page numbers with multiple positions and styles
  • PDF Export: Browser and Node.js PDF generation
  • Lazy Rendering: Render only visible pages for large documents (100+ pages)
  • Web Workers: Offload layout calculations to background threads
  • Image Handling: Automatic image overflow prevention
  • Overset Detection: Automatic detection and handling of content that doesn't fit
  • CSS Level 4 Selectors: :has() and :is() for powerful page matching
  • Running Headers: Full string-set support with first, last, start, first-except keywords
  • CSS Hyphens: Automatic hyphenation with dictionaries for 6 languages (en, es, fr, de, it, pt)
  • CSS Footnotes: Full float: footnote support with ::footnote-call, ::footnote-marker, and @footnote area
  • Spread Viewer: Book view with navigation, zoom, and touch support
  • Framework Support: Works with vanilla JS, Angular, and Vue.js

Installation

npm

npm install paper-view

yarn

yarn add paper-view

CDN

<script src="https://unpkg.com/paper-view/dist/paperview.js"></script>

Quick Start

Browser

<!DOCTYPE html>
<html>
<head>
  <script src="dist/paperview.js"></script>
  <link rel="stylesheet" href="dist/css/paper.css">
</head>
<body>
  <!-- Source content (hidden) -->
  <div id="content" style="display: none;">
    <h1>My Document</h1>
    <p>Your content goes here...</p>
  </div>

  <!-- Render target -->
  <div id="render"></div>

  <script>
    const content = document.getElementById('content');
    const renderTo = document.getElementById('render');

    const previewer = new PaperView.Previewer();
    previewer.preview(content, renderTo, []).then((flow) => {
      console.log(`Rendered ${flow.total} pages in ${flow.performance}ms`);
    });
  </script>
</body>
</html>

Angular

1. Add CSS to angular.json:

{
  "styles": [
    "node_modules/paper-view/dist/css/paper.css"
  ]
}

2. Import and use in your component:

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Previewer } from 'paper-view';

@Component({
  selector: 'app-root',
  template: `
    <div #content style="display: none;">
      <h1>My Document</h1>
      <p>Content here...</p>
    </div>
    <div #render></div>
  `
})
export class AppComponent implements OnInit {
  @ViewChild('content', { read: ElementRef }) contentRef!: ElementRef;
  @ViewChild('render', { read: ElementRef }) renderRef!: ElementRef;

  ngOnInit() {
    const previewer = new Previewer();
    previewer.preview(
      this.contentRef.nativeElement,
      this.renderRef.nativeElement,
      []
    );
  }
}

Vue.js

<template>
  <div id="app">
    <div ref="content" style="display: none;">
      <h1>My Document</h1>
      <p>Content here...</p>
    </div>
    <div ref="render"></div>
  </div>
</template>

<script>
import { Previewer } from 'paper-view';
import 'paper-view/dist/css/paper.css';

export default {
  name: 'App',
  mounted() {
    const previewer = new Previewer();
    previewer.preview(this.$refs.content, this.$refs.render, []);
  }
};
</script>

API Reference

Previewer

The main entry point for rendering documents.

const previewer = new Previewer(options);

Options

Option Type Default Description
viewMode string 'spread' View mode: 'single' or 'spread'
lazyRender boolean false Enable lazy rendering
lazyRenderOptions object {} Lazy rendering configuration
useWorkers boolean false Enable Web Workers for layout
workerOptions object {} Web Workers configuration

Methods

Method Description
preview(content, renderTo, stylesheets) Render content to target element
exportPDF(options) Export rendered document as PDF
scrollToPage(index) Scroll to page by index
scrollToPageNumber(pageNumber) Scroll to page by number
getLazyRenderInfo() Get lazy rendering statistics
getWorkerStats() Get Web Workers statistics
getOversetInfo() Get overset content information
destroy() Clean up resources

Events

previewer.on('rendered', (flow) => {
  console.log(`Rendered ${flow.total} pages`);
});

previewer.on('page', (page) => {
  console.log(`Page ${page.index} rendered`);
});

previewer.on('chunkingComplete', (data) => {
  console.log(`Chunked ${data.totalPages} pages`);
});

previewer.on('oversetDetected', (data) => {
  console.log(`Overset on page ${data.pageIndex}`);
});

Chunker

Chunks documents into paged media flows.

import { Chunker } from 'paper-view';

const chunker = new Chunker(content, renderTo, options);

Polisher

Processes CSS stylesheets for paged media.

import { Polisher } from 'paper-view';

const polisher = new Polisher();
await polisher.add('styles.css');

Advanced Features

Page Numbering

Add configurable page numbers to your documents.

Basic Usage

const previewer = new Previewer();
const flow = await previewer.preview(content, renderTo, stylesheets);

// Enable page numbering
flow.setPageNumberingEnabled(true);

// Update configuration
flow.updatePageNumbering({
  position: 'bottom-center',
  style: 'decimal',
  template: 'Page {current} of {total}'
});

Configuration Options

Option Type Default Description
enabled boolean false Enable page numbering
position string 'bottom-center' Position: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
style string 'decimal' Style: decimal, upper-roman, lower-roman, upper-alpha, lower-alpha
start number 1 Starting page number
template string '{current}' Template with {current} and {total}

CSS Integration

@page {
  pagedjs-page-numbering: enabled;
  pagedjs-page-numbering-position: top-right;
  pagedjs-page-numbering-style: upper-roman;
}

@page :first {
  pagedjs-page-numbering: disabled;
}

PDF Export

Export documents as PDF in browser or Node.js.

Browser Export

const previewer = new Previewer();
const flow = await previewer.preview(content, renderTo, stylesheets);

await previewer.exportPDF({
  filename: 'document.pdf',
  metadata: {
    title: 'My Document',
    author: 'John Doe'
  },
  onProgress: ({ stage, progress }) => {
    console.log(`${stage}: ${progress}%`);
  }
});

Node.js Export

const { NodePDFExporter } = require('paper-view/dist/paperview.node-pdf.js');

const pdfBuffer = await NodePDFExporter.fromHTML(html, css, {
  format: 'A4',
  margin: { top: '2cm', bottom: '2cm' }
});

await NodePDFExporter.save(pdfBuffer, 'output.pdf');

Lazy Rendering

Render only visible pages for large documents.

const previewer = new Previewer({
  viewMode: 'single',
  lazyRender: true,
  lazyRenderOptions: {
    bufferPages: 3,      // Pages to render around viewport
    unloadPages: 5,      // Pages away before unloading
    enableUnload: true   // Unload pages when out of view
  }
});

const flow = await previewer.preview(content, renderTo, stylesheets);

// Get render statistics
const info = previewer.getLazyRenderInfo();
console.log(info);
// { totalPages: 150, renderedPages: 12, progress: 8, chunkingComplete: true }

Web Workers

Offload layout calculations to background threads.

const previewer = new Previewer({
  useWorkers: true,
  workerOptions: {
    workerCount: 2  // Number of workers
  }
});

// Listen for worker events
previewer.on('workersReady', (data) => {
  console.log(`${data.workerCount} workers ready`);
});

previewer.on('workerTaskComplete', (data) => {
  console.log(`Task completed in ${data.duration}ms`);
});

Image Overflow Handling

Images are automatically constrained to fit within page boundaries.

/* Applied automatically */
img {
  max-width: 100%;
  height: auto;
  object-fit: contain;
}

Images that exceed the page area are scaled down while maintaining aspect ratio. Constrained images receive the pagedjs_image_constrained class.

Overset Content Handling

PaperView automatically detects and handles content that doesn't fit within the allocated pages (overset content). When content overflows a page, new pages are created and the remaining content is re-flowed.

Automatic Handling

Overset handling is automatic and runs after the initial render:

const previewer = new Previewer();
const flow = await previewer.preview(content, renderTo, stylesheets);

// Overset content is automatically detected and handled
// New pages are added as needed

Detecting Overset Content

Check if any pages have overset content:

// Get overset information
const oversetInfo = previewer.getOversetInfo();
console.log(oversetInfo);
// {
//   hasOverset: false,
//   oversetPageCount: 0,
//   oversetPages: []
// }

Listening for Overset Events

Subscribe to overset events for debugging or custom handling:

previewer.on('oversetDetected', (data) => {
  console.log(`Overset on page ${data.pageIndex}, iteration ${data.iteration}`);
  console.log('Break token:', data.breakToken);
});

previewer.on('oversetMaxIterations', (data) => {
  console.warn(`Overset handling stopped after ${data.iterations} iterations`);
  console.warn('Some content may be truncated');
});

Manual Overset Handling

For advanced use cases, you can manually handle overset content:

const chunker = flow; // Chunker instance

// Find pages with overset content
const oversetPages = chunker.oversetPages();

if (oversetPages.length > 0) {
  console.log(`${oversetPages.length} pages have overset content`);

  // Manually trigger overset handling
  await chunker.handleOverset(chunker.source, {
    maxIterations: 100  // Custom max iterations
  });
}

Page Overset Property

Each page has an overset property that indicates if it has remaining content:

flow.pages.forEach((page, index) => {
  if (page.overset) {
    console.log(`Page ${index + 1} has overset content`);
    console.log('Remaining content starts at:', page.endToken);
  }
});

Configuration

CSS @page Rules

@page {
  size: A4;
  margin: 2cm;
}

@page :first {
  margin-top: 4cm;
}

@page :left {
  margin-left: 3cm;
}

@page :right {
  margin-right: 3cm;
}

Named Pages

@page chapter {
  size: A5;
  margin: 1.5cm;
}

.chapter {
  page: chapter;
}

Page Breaks

h1 {
  break-before: page;
}

figure {
  break-inside: avoid;
}

.chapter-end {
  break-after: right;
}

CSS Level 4 Selectors: :has() and :is()

PaperView supports CSS Level 4 :has() and :is() selectors for powerful page matching based on content.

:has() Selector

Style pages that contain specific elements:

/* Style pages that contain h1 elements */
@page:has(h1) {
  margin-top: 3cm;
  background: #f9f9f9;
}

/* Style pages with figures */
@page:has(img.figure) {
  margin: 1.5cm;
}

/* Style pages with tables */
@page:has(table) {
  size: landscape;
}

/* Combine with named pages */
@page chapter:has(h1) {
  @top-center {
    content: "Chapter Start";
  }
}

:is() Selector

Apply styles to pages matching any of multiple conditions:

/* Style both first and blank pages */
@page:is(:first, :blank) {
  margin: 3cm;
}

/* Style first, left, and right pages differently */
@page:is(:first, :left, :right) {
  bleed: 6mm;
  marks: crop cross;
}

/* Combine with named pages */
@page chapter:is(:first, :nth(1)) {
  margin-top: 5cm;
}

Specificity Order

Selectors are applied based on specificity (highest to lowest):

  1. :has() - Content-based matching (highest)
  2. :first, :blank - Position-based
  3. :is() - Multi-selector matching
  4. :nth() - Position-based
  5. Named pages (e.g., @page chapter)
  6. :left, :right - Spread position
  7. * - Default (lowest)

Browser Compatibility

The :has() selector requires browser support for :has() in matches(). For older browsers, PaperView automatically falls back to querySelector() for matching.

Running Headers and String-Sets

PaperView supports CSS Paged Media string-set for running headers and footers with advanced keyword support.

Basic Usage

/* Set a string from h1 elements */
h1 {
  string-set: chapter content();
}

/* Use it in page margins */
@page {
  @top-center {
    content: string(chapter);
  }
}

String-Set Keywords

PaperView supports all CSS Paged Media string-set keywords:

Keyword Description
content() or content(first) First value on the page (default)
content(last) Last value on the page
content(start) First value from the first page, or first value from current page
content(first-except) First value except on the first page (empty on first page)

Examples

/* Use first heading on each page */
h1 {
  string-set: chapter content(first);
}

/* Use last heading on each page (useful for section endings) */
h2 {
  string-set: section content(last);
}

/* Use title from first page throughout document */
h1.title {
  string-set: doctitle content(start);
}

/* Show chapter title except on first page */
h1.chapter {
  string-set: chapter content(first-except);
}

@page {
  @top-left {
    content: string(doctitle);
  }
  @top-right {
    content: string(chapter);
  }
}

Multiple String-Sets

You can define multiple string-sets in one declaration:

h1 {
  string-set: chapter content(first), chapter-last content(last);
}

@page {
  @top-left {
    content: string(chapter);
  }
  @top-right {
    content: string(chapter-last);
  }
}

Fallback Behavior

If no matching element exists on a page, the previous page's value is used (persistent behavior), ensuring running headers remain consistent across pages without headings.

CSS Hyphens Support

PaperView provides comprehensive support for the CSS hyphens property with automatic hyphenation using language-specific dictionaries.

Basic Usage

/* Enable automatic hyphenation */
p {
  hyphens: auto;
}

/* Only hyphenate at explicit soft hyphens (&shy; or \u00AD) */
p {
  hyphens: manual;
}

/* Disable hyphenation */
p {
  hyphens: none;
}

Hyphens Modes

Value Description
auto Automatic hyphenation using language dictionaries
manual Only break at soft hyphens (&shy; or \u00AD)
none No hyphenation

Hyphenation Properties

PaperView supports all CSS hyphenation-related properties:

/* Custom hyphen character */
body {
  hyphenate-character: "\2010";  /* Hyphen bullet: ‐ */
}

/* Minimum characters before/after hyphen and minimum word length */
body {
  hyphenate-limit-chars: 3 2 6;  /* before after total */
}

/* Maximum consecutive hyphenated lines */
body {
  hyphenate-limit-lines: 2;
}

/* Unsqueezed space threshold to trigger hyphenation */
body {
  hyphenate-limit-zone: 20%;
}

Language Support

Hyphenation uses the lang attribute on elements to select the appropriate dictionary:

<article lang="en">
  <p>This text will be hyphenated using English rules.</p>
</article>

<article lang="es">
  <p>Este texto se separará usando reglas en español.</p>
</article>

<article lang="de">
  <p>Dieser Text wird nach deutschen Regeln getrennt.</p>
</article>

Supported languages: English (en), Spanish (es), French (fr), German (de), Italian (it), Portuguese (pt)

How It Works

The hyphenation engine uses the Knuth-Liang algorithm, which matches word patterns against language-specific dictionaries to identify valid syllable boundaries. The system includes:

  1. Pattern-based hyphenation: Compact pattern dictionaries for each language
  2. Exception words: Hundreds of common words with predefined hyphenation points
  3. Browser API fallback: Uses Intl.Segmenter when available for better performance
  4. Result caching: Hyphenated words are cached to avoid redundant calculations

Example

See examples/hyphens-example.html for an interactive demo showing different hyphenation modes, languages, and configuration options.

CSS Footnotes

PaperView implements CSS Paged Media footnotes support, allowing you to create professional-quality documents with footnotes.

Basic Usage

/* Mark elements as footnotes */
.footnote {
  float: footnote;
}

/* Style the footnote area */
@page {
  @footnote {
    border-top: 1px solid #333;
    padding-top: 6pt;
    margin-top: 12pt;
    font-size: 9pt;
  }
}

/* Style footnote markers */
::footnote-call {
  color: #0066cc;
  font-size: 0.75em;
}

::footnote-marker {
  font-weight: bold;
}
<p>This is some text with a footnote.<span class="footnote">This is the footnote content.</span></p>

Float: Footnote

The float: footnote property removes the element from normal flow and places it in the footnote area at the bottom of the page. A footnote-call (superscript number) is automatically inserted at the original location.

Footnote Display

The footnote-display property controls how footnotes are rendered:

Value Description
block (default) Each footnote displayed as a block with marker
inline Footnotes displayed inline, one after another
compact Compact display minimizing vertical space
.footnote {
  float: footnote;
  footnote-display: block;
}

Footnote Policy

The footnote-policy property determines behavior when there is insufficient space:

Value Description
line (default) Footnote may move to next page if area is full
keep Reference and footnote kept together on same page
block Same as keep, for block-level elements
.footnote {
  float: footnote;
  footnote-policy: keep;
}

@footnote Area

The @footnote at-rule within @page allows styling the footnote area:

@page {
  @footnote {
    border-top: 1px solid #333;
    padding-top: 6pt;
    font-size: 9pt;
    line-height: 1.3;
  }
}

Pseudo-elements

PaperView supports the following footnote pseudo-elements:

  • ::footnote-call: The reference marker in the main text (superscript number)
  • ::footnote-marker: The number marker in the footnote area
::footnote-call {
  color: #0066cc;
  vertical-align: super;
  font-size: 0.75em;
}

::footnote-marker {
  font-weight: bold;
  color: #333;
}

How It Works

  1. Elements with float: footnote are identified during CSS parsing
  2. Each footnote is replaced in the text flow with a footnote-call span
  3. Original footnote content is collected and placed in the footnote area
  4. Footnotes are numbered sequentially using CSS counters
  5. The footnote area is styled according to @footnote rules

Example

See examples/footnotes-example.html for an interactive demo showing footnotes with different display modes and policies.

Spread Viewer (Book View)

PaperView includes an enhanced spread viewer that provides a book-like reading experience with navigation, zoom, and mode switching.

Basic Usage

const previewer = new PaperView.Previewer({
  viewMode: "single",
  spreadViewer: true,
  spreadViewerOptions: {
    spreadMode: "book",    // "book" or "scroll"
    zoom: 1,               // Initial zoom level
    minZoom: 0.5,          // Minimum zoom
    maxZoom: 3,            // Maximum zoom
    zoomStep: 0.25,        // Zoom increment
    enableKeyboard: true,  // Keyboard navigation
    enableTouch: true      // Touch/swipe support
  }
});

const flow = await previewer.preview(content, renderTo, stylesheets);

Features

Feature Description
Book View Side-by-side page spreads with spine effect
Scroll View All pages in a scrollable column
Arrow Navigation Click arrows or use keyboard to navigate spreads
Zoom Mouse wheel (Ctrl+scroll), buttons, or keyboard shortcuts
Touch/Swipe Swipe left/right on mobile to change spreads
Page Indicator Shows current spread and page numbers

Keyboard Shortcuts

Key Action
/ PageUp Previous spread
/ PageDown Next spread
Home First spread
End Last spread
Ctrl + + Zoom in
Ctrl + - Zoom out
Ctrl + 0 Reset zoom

Programmatic Control

// Get the spread viewer instance
const spreadViewer = previewer.getSpreadViewer();

// Navigate
spreadViewer.nextSpread();
spreadViewer.prevSpread();
spreadViewer.goToSpread(5);

// Zoom
spreadViewer.zoomIn();
spreadViewer.zoomOut();
spreadViewer.setZoom(1.5);
spreadViewer.zoomToFit();

// Switch mode
previewer.setSpreadViewerMode("book");  // or "scroll"

// Get state
const state = spreadViewer.getState();
console.log(state);
// { currentSpread: 2, totalSpreads: 10, zoom: 1, mode: "book" }

// Listen for events
spreadViewer.on("spreadChange", (data) => {
  console.log(`Now viewing spread ${data.spread}`);
  console.log(`Pages: ${data.pages}`);
});

spreadViewer.on("zoomChange", (data) => {
  console.log(`Zoom: ${data.zoom * 100}%`);
});

Development

Install Dependencies

npm install

Build

npm run build

Development Server

Run local dev-server with livereload at http://localhost:9090/:

npm start

Run Tests

# Unit tests
npm run tests

# Integration tests
npm run specs

# Lint
npm run lint

Project Structure

paper-view/
├── src/
│   ├── index.js              # Main entry point
│   ├── paperview.js          # Core PaperView class
│   ├── chunker/              # Content chunking
│   ├── polisher/             # CSS processing
│   ├── modules/              # Feature modules
│   ├── export/               # PDF export
│   └── polyfill/             # Polyfills & Previewer
├── examples/                 # Example projects
├── tests/                    # Unit tests
├── specs/                    # Integration tests
└── dist/                     # Built bundles

Examples

Complete examples are available in the examples/ directory:

Example Description
index.html Basic browser usage
page-numbering-example.html Page numbering with controls
pdf-export-example.html PDF export with options
pdf-export-node-example.js Node.js PDF generation
lazy-render-example.html Lazy rendering for large docs
worker-layout-example.html Web Workers for layout
spread-viewer-example.html Book view with navigation & zoom
hyphens-example.html CSS hyphens with multiple languages
footnotes-example.html CSS footnotes with float: footnote

License

MIT License

About

paper-view a javascript library to help view content in mode paper.

Topics

Resources

Stars

4 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages