切換語言為:簡體

CSS 列印屬性與 PDF 生成

  • 爱糖宝
  • 2024-11-11
  • 2036
  • 0
  • 0

CSS 列印屬性

介紹 CSS 列印相關的屬性。

print 媒體查詢

透過媒體查詢在列印模式下應用不同的 CSS,有兩種方式:

<style>
  @media print {
    /* 在列印模式下生效的 CSS */
  }
</style>

<!-- 列印模式下載入應用此 css 檔案 -->
<link href="/path/to/print.css" media="print" rel="stylesheet" />

@page

@page 規則,可以控制列印頁面的各項屬性,包括每頁大小、邊距、頁面方向等:

@page {
  size: 10mm 10mm; /* 控制所有頁面寬高為 10 毫米 */
  margin-top: 10pt;
}

@page :left {
  size: 15in; /* 控制所有偶數頁寬高為 15 英寸 */
}

@page :rigth {
  size: a4 landscape; /* 控制所有奇數頁的頁面大小與方向 */
}

@page :first {
  /* 應用於第一頁 */
}

@page :blank {
  /* 應用於空白頁 */
}

.simple-page {
  page: selector; /* 宣告一個名為 selector 的 page */
}
@page selector {
  /* 選中一個名為 selector 的 page */
}

@page 規則受支援的屬性有限,完整的支援列表看 MDN#@page

@page 支援 4 個偽類引數:

  • @page :left 控制偶數頁的頁面屬性

  • @page :right 控制奇數頁的頁面屬性

  • @page :first 控制第一頁的頁面屬性

  • @page :blank 控制空白頁的頁面屬性

注意 @page 規則不能內嵌 CSS 選擇器,比如下列 CSS 本意是隱藏第一頁的頁尾元素,但語法不受支援:

@page :first {
  /* 不被支援的語法 */
  .footer {
    display: none;
  }
}

page

page 屬性用於宣告一個指定名稱的頁面:

.simple-page {
  page: main;
}

break-*

CSS 提供了三個用於控制分頁邏輯的屬性:

  • break-before

  • break-after

  • break-inside

他們的屬性值大部分是通用的,有兩個常用的屬性值:

  • always:始終在遇到指定元素前、後插入分頁符

  • avoid:避免在指定元素前、後、內部插入分頁符

假設我們有多個標籤頁,需要將這些標籤頁生成為一個 PDF,但每個標籤頁應該是一頁而不能粘連,就可以使用分頁屬性來實現:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Label</title>

    <style>
      @media print {
        /* 選中名為 main 的 page */
        @page main {
          margin: 10px;
          /* 頁面大小為 10x15 */
          size: 100mm 150mm;
          font-size: 12px;
          line-height: 2;
          color: #333;
          font-weight: 500;
        }

        .container-box {
          /* 宣告一個 main 的 page */
          page: main;
          /* 每次遇到 container-box 後進行分頁 */
          break-after: always;
          /* container-box 內部的元素是緊湊的,不允許分割 */
          break-inside: avoid;
        }

        .container {
          width: 358px;
          height: 540px;
          border: 1px solid #000;
        }

        .header {
          display: flex;
          flex-direction: column;
          justify-content: space-between;
          height: 120px;
          border-bottom: 2px solid #000;
        }

        .content {
          padding: 10px;
          height: calc(540px - 124px);
        }

        .title {
          padding: 10px;
          margin-top: 12px;
          font-size: 16px;
          letter-spacing: 2px;
        }
      }
    </style>
  </head>
  <body>
    <!-- 一頁標籤 -->
    <div>
      <main>
        <header>
          <h1>TITLE</h1>
        </header>

        <section>
          <p>Content</p>
        </section>
      </main>
    </div>

    <!-- 一頁標籤 -->
    <div>
      <main>
        <header>
          <h1>TITLE</h1>
        </header>

        <section>
          <p>Content</p>
        </section>
      </main>
    </div>

    <!-- 一頁標籤 -->
    <div>
      <main>
        <header>
          <h1>TITLE</h1>
        </header>

        <section>
          <p>Content</p>
        </section>
      </main>
    </div>

    <!-- 一頁標籤 -->
    <div>
      <main>
        <header>
          <h1>TITLE</h1>
        </header>

        <section>
          <p>Content</p>
        </section>
      </main>
    </div>
  </body>
</html>

效果如下:

CSS 列印屬性與 PDF 生成

上訴 HTML 在遇到包含類名 container-box 的元素時認為需要進行分頁,由 break-after: always 完成;而 break-inside: avoid 保證 container-box 元素是高度聚合的,不允許內部元素跨頁。

我們試試去掉這兩個分頁控制元素,得到的效果如下:

CSS 列印屬性與 PDF 生成

去掉了分頁控制元素後得到的效果很明顯不盡人意。

頁面邊距區域

列印分頁除了內容區外,還有 16 個圍繞在內容區的邊距區域,如下圖:

CSS 列印屬性與 PDF 生成

我們可以透過 @ + 指定方位來控制對應區域的內容:

@page :first {
  @top-center {
    content: 'My Book';
  }
}

上述程式碼在第一頁的頂部中間區域新增了一個標題,內容為 “My Book”。

注意,目前瀏覽器中只有 Chrome 131 版本實現了頁面邊距區域的特性。

除了頁面邊距區域外,未來的 CSS 分頁模組還會新增:設定字元傳、腳註、交叉引用等功能。

內建的頁面計數器

CSS 列印內建了兩個分頁相關的計數器,分別為 pagepages,表示當前頁與總頁數,我們可以這樣使用他:

@media print {
  @page :left {
    @bottom-left {
      content: 'Page ' counter(page) ' of ' counter(pages);
    }
  }

  @page :right {
    @bottom-right {
      content: 'Page ' counter(page) ' of ' counter(pages);
    }
  }
}

上述 CSS 在每頁底部新增了類似 Page 1 of 13 的內容,表示當前第幾頁以及總共有幾頁,對於偶數頁而言新增的內容在左下,對於奇數頁而言在右下。

PDF 生成

使用 Puppeteer 控制無頭瀏覽器生成一些簡單的 PDF:

  1. 編寫 HTML 模板,透過 CSS 控制頁面大小與分頁

  2. 模板渲染工具動態生成 HTML,進行資料的填充

  3. 呼叫 Puppeteer 的 API,生成 PDF

這種方式有幾個顯而易見的好處:

  • HTML + CSS 對於內容的排版和樣式是很友好的

  • 縮排開發週期,HTML 基本可以做到所見即所得

  • 易於除錯,只要一個瀏覽器就能實現大部分功能

  • PDF 內容清晰,且每塊內容是各自獨立可選中的(如果由前端使用 HTML2Canvas 之類的庫生成圖片並新增至 PDF,可能會出現內容模糊的情況,且圖片作為一個整體,無法進行內容的修改)

我們以 Node 中的 Puppeteer 看一下如何實現列印功能(Puppeteer 實現了多語言的版本,可以自行查詢):

import puppeteer from 'puppeteer';

(async () => {
  // 啟動瀏覽器
  const browser = await puppeteer.launch();
  // 開啟一個新的頁面
  const page = await browser.newPage();

  // 獲取動態渲染的頁面內容
  const html = /**/;

  // 將頁面內容替換為指定內容
  await page.setContent(html);

  // 生成並獲取 PDF 內容
  const uint8Array = await page.pdf();

  console.log('PDF byte data: ' uint8Array);

  await browser.close();
})();

上述程式碼中,pdf 方法支援傳遞一個選項引數,基本與瀏覽器列印中選項能夠對應:

// 只展示部分欄位...
interface PDFOptions {
  // 是否展示頁面頁尾,大部分時候不需要
  displayHeaderFooter: boolean;
  // 頁尾內容
  footerTemplate: string;
  // 一個列舉值,理解為紙張大小或分頁大小,優先於寬度與高度設定
  format: Enum;
  // 頁首內容
  headerTemplate: string;
  // 分頁寬度,可以是帶單位的字串
  width: string | number;
  // 分頁高度,可以是帶單位的字串
  height: string | number;
  // 是否橫版內容
  landscape: boolean;
  // 頁面邊距
  margin: {
    bottom: string | number;
    left: string | number;
    right: string | number;
    top: string | number;
  };
  // 是否以 html 中 @page 定義的 size 爲準設定分頁的尺寸
  preferCSSPageSize: boolean;
}

這裏要注意 marginpreferCSSPageSize 屬性:

  • 如果沒有指定 margin 引數,則預設以 CSS 中定義的邊距值爲準,這在大部分時候是符合我們預期的

  • 當你的頁面大小是自定義尺寸時,一定要設定 preferCSSPageSize: true,這樣生成的 PDF 每頁尺寸纔是符合預期的

最後說說一些需要注意的地方:

  • 列印模式下 position: fixed 的行為是相對於所有分頁去定位,即一個 fixed 元素在視覺上會出現在所有分頁中。

  • 想在多頁列印中將一個元素固定在最後一頁的底部目前沒有什麼很好的方法,原因如上所述(這也是我認為這種模式只能生成簡單 PDF 的原因)

參考內容

-- end


作者:yuanyxh
連結:https://juejin.cn/post/7435637476888461348

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.