soma0sd blog

[전산물리 웹교재] 웹사이트를 책으로 만들기: 페이지 자르기

반응형

지킬(Jekyl)로 만든 깃허브 페이지(Github Pages)를 종이에 출력 가능한 책의 형태로 만듭니다. 여기서는

  • 출판을 위한 새로운 지킬 레이아웃을 생성하고
  • 프린트 환경을 위한 스타일시트를 작성하고
  • 타입스크립트(자바스크립트)를 사용해 문서 내용을 페이지에 맞게 자르는

방법을 소개합니다. 인쇄는 크롬의 인쇄 기능(Ctrl+P)으로 테스트 하였습니다.

레이아웃 생성

HTML5 템플릿 자동 생성 도구를 사용하거나 하면 이런 태그가 있는 경우가 많습니다.

<meta name="viewport" content="width=device-width, initial-scale=1.0">

이 태그를 제외해야 브라우저의 너비에 따라 자동으로 글자 크기 등을 조절하지 않습니다. 동일한 출력 결과를 원한다면 이 태그를 제거해야 합니다.

{% capture CONTENTS %}
{% for chapter in site.data.toc -%}
  {% assign chapter_dir = "/" | append: site.docsurl | append: chapter[0] | append: "/" -%}
  {% assign doc = site.html_pages | where: 'dir', chapter_dir | first -%}
  <div class="warpper chapter"><div class="page chapter"><span class="page-number"></span><h1 id="{{ doc.title | url_encode}}">{{ doc.title }}</h1></div>
  <div class="page clauses"><span class="page-number"></span>{{ doc.content }}</div>
  {% for clauses in chapter[1] -%}
    {% assign clauses_dir = chapter_dir | append: clauses | append: "/" -%}
    {% assign doc = site.html_pages | where: 'dir', clauses_dir | first -%}
    <div class="page clauses"><span class="page-number"></span>{{ doc.content }}</div>
  {% endfor -%}
  </div>
{% endfor -%}
{% endcapture -%}

레이아웃 파일의 리퀴드 부분입니다. CONTENTS 변수에 페이지를 내용물을 담습니다. 문서의 내부구조는 [전산물리 웹교재] 리퀴드로 문서 목차 구현에서 소개한 내용을 따릅니다. div.warpper.chapter안에 각 장의 제목을 담는 div.page.chapter와 본문의 내용을 담는div.page.clauses이 들어갑니다. 이 구조는 스타일시트에서 counter()를 사용해 장과 절 번호를 표시할 때 사용하기 위한 구조입니다.

<span class="page-number"></span>는 나중에 페이지번호를 표시하고 목차를 만들 때 사용합니다.

_layouts/textbook.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
  <title>{{ site.title }}</title>
  <!-- 스타일과 스크립트 -->
  <script defer src="{{ "js/onload.js" | absolute_url }}"></script>
  <link rel="stylesheet" href="{{ "css/main.css" | absolute_url }}">
</head>
<body class="print">
  <main>
    <article>
      <div class="page title">
        <h1 class="title">{{ site.bookname }}</h1>
        <p class="desc">{{ site.description }}</p>
        {% for author in site.authors -%}
        <div class="author">
          <span class="name">{{ author.name }}</span>
        </div>
        {% endfor -%}
      </div>
      <div class="page home">
        {% assign home = site.docsurl | relative_url -%}
        {% assign doc = site.html_pages | where: 'dir', home | first -%}
        {{ doc.content }}
      </div>
      <div class="page toc">
        <h1>차례</h1>
        <ul id="textbook-toc"></ul>
      </div>
      {{ CONTENTS }}
    </article>
  </main>
</body>
</html>

본문의 내용이 들어가기 전에 표지로 들어갈 div.page.title과 인트로가 들어갈 div.page.home, 차례가 들어갈 div.page.toc를 만들어 둡니다. 이후 실제 출판에 도전할 때 더 적당한 양식으로 제작하겠습니다. 지금은 그냥 PDF로 읽기 괜찮은 수준으로 만들었습니다.

docs/textbook.md

---
layout: textbook
---

그리고 이 레이아웃을 사용하는 빈 문서 하나를 생성합니다.

_config.yml

bookname: 책 이름
authors:
  -
    name: "저자 A"
  -
    name: "저자 B"

새로운 레이아웃에서 사용하는 사이트 속성을 추가합니다.

스타일 시트

스타일시트는 SASS로 작성하였습니다.

@media print
  html, body
    width: 210mm
    height: 297mm

body.print
  width: 100%
  height: 100%
  margin: 0
  padding: 0
  background-color: #FAFAFA
  box-sizing: border-box

body.print div.page
  width: 210mm
  min-height: 297mm
  padding: 20mm
  margin: 10mm auto
  border: 1px #D3D3D3 solid
  border-radius: 5px
  background: white
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1)
  @media print
    margin: 0 0 0 0
    border: initial
    border-radius: initial
    width: initial
    min-height: initial
    box-shadow: initial
    background: initial
    page-break-after: always

A4 크기로 페이지를 나누고 각 페이지가 끝나는 부분에는 page-break-after: always 속성으로 새 페이지에 인쇄하도록 합니다.

스크립트

앞선 레이아웃은 문서 하나에 한 페이지를 배정합니다. 내용이 들어있는 페이지가 유독 길어지고 인쇄할때는 긴 부분이 잘려나오게 됩니다. 스크립트를 이용해 모든 페이지가 기준높이를 맞출 수 있도록 합니다. 모든 페이지가 적절한 여백을 가진 상태로 출력될 수 있도록 조정하는 역할을 합니다.

타입스크립트로 작성한 페이지 자르기 함수입니다.

function page_slice() {
    // 기준 높이를 잡을 페이지로부터 랜더링한 높이를 기준 높이로 출력
    let refHeight = window.getComputedStyle(document.querySelector("div.page.title")!).height
    document.querySelectorAll("div.page:not(.title)")!.forEach(elem => {
        let sub = 1
        let refPage = elem
        let newPage = document.createElement("div")
        let child
        newPage.setAttribute("class", elem.className)
        while (true) {
            // 모든 페이지가 기준 크기보다 작아질 때까지 페이지 분할 반복
            if (refPage.lastElementChild) {
                child = refPage.lastElementChild!.cloneNode(true)
            } else {
                break
            }
            if (window.getComputedStyle(refPage).height > refHeight) {
                // 기존 페이지의 높이가 기준높이보다 크면
                // 기존 페이지의 마지막 자식 노드를 새 페이지로 이동
                if (newPage.childNodes[0]) {
                    newPage.insertBefore(child, newPage.childNodes[0])
                } else {
                    newPage.appendChild(child)
                }
                refPage.removeChild(refPage.lastElementChild!)
            } else if (window.getComputedStyle(newPage).height > refHeight) {
                // 새 페이지의 높이가 기준 높이보다 크면 새 페이지 생성
                sub++
                newPage.innerHTML += '<span class="page-number"></span>'
                refPage.after(newPage)
                refPage = newPage
                newPage = document.createElement("div")
                newPage.setAttribute("class", elem.className)
            } else if (newPage.innerHTML != "") {
                // 새 페이지를 기존 페이지 뒤에 추가
                newPage.innerHTML += '<span class="page-number"></span>'
                refPage.after(newPage)
                break
            } else {
                break
            }
        }
    })
}
window.onload = () => {
  if(document.querySelector("body.print")){
    page_slice()
  }
}

자바스크립트로는 이렇게 쓸 수 있습니다.

// 페이지 자르기 함수
function page_slice() {
    // 기준 높이를 잡을 페이지로부터 랜더링한 높이를 기준 높이로 출력
    var refHeight = window.getComputedStyle(document.querySelector("div.page.title")).height;
    document.querySelectorAll("div.page:not(.title)").forEach(function (elem) {
        var sub = 1;
        var refPage = elem;
        var newPage = document.createElement("div");
        var child;
        newPage.setAttribute("class", elem.className);
        while (true) {
            // 모든 페이지가 기준 크기보다 작아질 때까지 페이지 분할 반복
            if (refPage.lastElementChild) {
                child = refPage.lastElementChild.cloneNode(true);
            }
            else {
                break;
            }
            if (window.getComputedStyle(refPage).height > refHeight) {
                // 기존 페이지의 높이가 기준높이보다 크면
                // 기존 페이지의 마지막 자식 노드를 새 페이지로 이동
                if (newPage.childNodes[0]) {
                    newPage.insertBefore(child, newPage.childNodes[0]);
                }
                else {
                    newPage.appendChild(child);
                }
                refPage.removeChild(refPage.lastElementChild);
            }
            else if (window.getComputedStyle(newPage).height > refHeight) {
                // 새 페이지의 높이가 기준 높이보다 크면 새 페이지 생성
                sub++;
                newPage.innerHTML += '<span class="page-number"></span>';
                refPage.after(newPage);
                refPage = newPage;
                newPage = document.createElement("div");
                newPage.setAttribute("class", elem.className);
            }
            else if (newPage.innerHTML != "") {
                // 새 페이지를 기존 페이지 뒤에 추가
                newPage.innerHTML += '<span class="page-number"></span>';
                refPage.after(newPage);
                break;
            }
            else {
                break;
            }
        }
    });
}
window.onload = function () {
    if (document.querySelector("body.print")) {
        page_slice();
    }
};

문서의 길이에 따라 페이지 조정에는 상당한 시간이 걸릴 수 있습니다. 별도의 로딩 화면을 구성하는 것이 좋다고 생각합니다.

반응형