paps

之前使用 enscript 來將文字檔轉為 pdf,但實測發現,若文字檔裡面是中文,那麼轉出來是亂碼。

只好找其他的方案,找到的是 paps: https://github.com/dov/paps

paps 比較麻煩的是需要自己安裝,安裝說明可以參考:https://github.com/dov/paps/blob/master/INSTALL.md

我的環境是 Ubuntu 20.04

第一個是需要 meson,這個直接用 apt-get 安裝就可以

sudo apt install meson

第二個是 fmtlib ,Ubuntu 20.04 內的 fmtlib 版本過舊,這邊需要借助 backports

sudo add-apt-repository ppa:savoury1/backports
sudo apt install libfmt-dev

最後就是編譯

https://github.com/dov/paps.git
cd paps
meson build
cd build && ninja
sudo ninja install

這樣就可以得到 paps 了。

大量程式碼輸出為pdf

因為客戶文件需要,得把一堆檔案輸出為pdf。懶人如我,當然要找一下是不是有好的方法可以達成這個需求。

第一個找到的是 pandoc ,這個工具好是好,但遇到 Ansible playbook ,pandoc 會以為檔案是設定檔,就沒辦法輸出。

第二個找到的是 enscript ,這工具就可以很輕易的將文字檔轉換為 postscript 檔案,同時還可以加上語法高亮效果,但可惜語法高亮效果不支援 Ansible playbook 。有了 postscript 檔案,接下來就可以用 ps2pdf 將 postscript 檔案轉換為 pdf。

# Ubuntu
sudo apt install ps2pdf enscript
enscript playbook.yml doc.ps
ps2pdf doc.ps doc.pdf

現在可以處理單一個檔案以後,接下來要想怎麼處理多個檔案,這裡透過 find, xargs 跟 enscript 的協作就可以做到

find . -name '*.yml' -print | xargs enscript --output=doc.ps
ps2pdf doc.ps doc.pdf

不免俗,還要加上頁首跟頁尾,頁尾也要加上頁次,這部份需要為 enscript 加上自訂的頁首跟調整頁尾的高度。

mkdir ~/.enscript
cp /usr/share/enscript/simple.hdr ~/.enscript/my.hdr

然後修改 ~/.enscript/my.hdr

% -- code follows this line --
%Format: fmodstr        $D{%a %b %d %H:%M:%S %Y}
%Format: pagenumstr     $V$%
%FooterHeight: 15

/do_header {    % print default simple header
  % Footer
  gsave
    d_footer_x d_footer_y HFpt_h 3 div add translate
    HF setfont

    user_footer_p {
      d_footer_x  d_footer_y moveto user_footer_left_str show

      d_footer_w user_footer_center_str stringwidth pop sub 2 div
      0 moveto user_footer_center_str show

      d_footer_x d_footer_w add user_footer_right_str stringwidth pop sub
      d_footer_y moveto user_footer_right_str show
    } if
  grestore

  % Header
  gsave
    d_header_x d_header_y HFpt_h 3 div add translate

    HF setfont
    user_header_p {
      5 0 moveto user_header_left_str show

      d_header_w user_header_center_str stringwidth pop sub 2 div
      0 moveto user_header_center_str show

      d_header_w user_header_right_str stringwidth pop sub 5 sub
      0 moveto user_header_right_str show
    } {
      5 0 moveto fname show
      45 0 rmoveto fmodstr show
      45 0 rmoveto pagenumstr show
    } ifelse

  grestore
} def

產出 postscript 的指令改為

find . -name '*.yml' -print | xargs enscript --fancy-header=my --header='$N||' --footer='|$%/%p|' --output=doc.ps
ps2pdf doc.ps doc.pdf

產出以後,會發現頁碼怪怪的,頁碼只有針對個別檔案,不是整份檔案,這下就傷腦筋了。經過Google 的幫忙,找到 pdftk 來幫忙。pdftk 是一個可以操作 pdf 的工具,可以作合併、加浮水印等等的功能。

最後的成果如下

DOC_PS=/tmp/doc.ps
RAW_DOC_PDF=/tmp/raw_doc.pdf
DOC_PDF=/tmp/doc.pdf

# 產出 postscript 檔案
find collections/ansible_collections/gov/twgcb/roles -name '*.yml' -print | xargs enscript \
  --fancy-header=my \
  --header='$N||' \
  --footer='| |' \
  --output=${DOC_PS}

# 前個步驟有產出 ps 檔案
if [[ -f ${DOC_PS} ]]; then
  # 先轉為 pdf
  ps2pdf ${DOC_PS} ${RAW_DOC_PDF}
  # 取得頁數
  number_of_pages=$(pdftk /tmp/doc.pdf dump_data | grep NumberOfPages | sed 's/NumberOfPages: //g')
  # 印出
  echo "number_of_pages=${number_of_pages}"
  # 先產生一份空白的 pdf ,頁次重新編排,然後把這份 pdf 當作是前面產生的 pdf 的浮水印
  # 這樣頁次就是對的了。
  enscript -L1 --fancy-header=my --header='||' --footer '|$% / $=|' -o- < \
  <(for i in $(seq 1 $number_of_pages); do echo; done) | \
  ps2pdf - | \
  pdftk ${RAW_DOC_PDF} multistamp - output ${DOC_PDF}
fi

參考資料

pdfkit/wkhtmltopdf

wkhtmltopdf 是一個可以將網址或是 HTML 檔案轉換為 PDF 的程式。

pdfkit 則是一個 python package (或 library),用來將網址或是 HTML 轉換為 PDF,底層用的是 wkhtmltopdf 。

django-pdfkit 則是給 Django 使用的 app,主要使用 pdfkit ,提供了 PDFView 。要提供產出 PDF 的網頁,只要寫一個繼承自 PDFView 的 View,撰寫 template ,就可以了。

以下是幾個試過以後的心得:

  1. 內部處理順序是先使用 Django template 輸出為 HTML,再用 pdfkit 輸出為 PDF。
  2. 預設用 PATH 去找 wkhtmltopdf,但如果有指定 WKHTMLTOPDF_BIN 這個環境變數的話,就用這裏面的。
  3. 在網址加入 html 參數,表示顯示 HTML,例如:http://server/show_pdf/?html
  4. 在網址加上 inline 參數 ,表示直接顯示 PDF,而非下載,例如:http://server/show_pdf/?inline
  5. 要自訂 pdfkit 使用 wkhtmltopdf 的參數,在繼承的 View 裡加入 pdfkit_options (型態為 dict) 或是自訂 get_pdfkit_options method。
  6. 要自訂輸出的檔名,可以在繼承的 View 裡加入 filename 或是自訂 get_filename method。
  7. wkhtmltopdf 預設不使用 print media-type ,所以在 css 裡加的 print media-type 相關樣式都沒用,除非自訂 pdfkit_options ,加入 {“print-media-type”: “”}。雖然加了 print-media-type 可以強制讓 wkhtmltopdf 參考 print media-type,但是一般可以在網路上查到,使用 print media-type 在每頁加頁次的方法是行不通的,一樣要自訂 pdfkit_options 才行 (參考 wkhtmltopdf -h 說明):{ ‘footer-center’: ‘[page] / [toPage]’}。其他還有調頁面邊界、紙張大小的,也都是要看 wkhtmltopdf 的說明來調整 pdfkit_options 。
  8. bootstrap 3 本身就有支援 print media-type ,直接使用就會有不錯的效果。如果要更漂亮,可以參考 Natshah/bootstrap-print: To manage print media for Twitter Bootstrap v3.
  9. 表格跨頁標題主要是使用 table/thead/tbody 跟調整 css,之前參考的網頁找不到了,這裡直接貼相關的 css 跟 html:
    /* CSS */
    @media print {
      table {display:table;}
      thead {display:table-header-group!important;page-break-after:avoid!important;}
      tbody {display:table-row-group!important;page-break-after: auto!important;}
      tr, img {display:table-row!important;page-break-inside:avoid!important;page-break-after: auto!important;}
      td, th {display:table-cell!important;}
    }
    
    <!-- HTML -->
    <table class="table table-bordered">
      <thead>
        <tr>
            <th class="text-center">編號</th>
            <th class="text-center">姓名</th>
        </tr>
      </thead>
      <tbody>
        <tr>
            <td>1</td>
            <td>王小明</td>
        </tr>
      </tbody>
    </table>