Pull to refresh

CSS для печати на бумаге

Reading time10 min
Views9.4K
Original author: voussoir.net

Введение

По работе я довольно часто занимаюсь созданием генераторов печати на HTML для воссоздания и замены форм, которые компания традиционно заполняла от руки на бумаге или в Excel. Это позволяет компании переходить на новые веб-инструменты, в которых форма автоматически заполняется по параметрам URL из нашей базы данных, создавая при этом тот же результат на бумаге, к которому все привыкли.

В этой статье я объясню основы CSS, управляющие внешним видом веб-страниц при печати, и дам пару советов, которые могут вам помочь в этом.

Примеры файлов

Вот несколько примеров генераторов страниц, чтобы вы поняли контекст.

Для начала я признаю, что эти страницы немного уродливые и их можно улучшить. Но они справляются с задачей и меня до сих пор не уволили.

Генератор счетов-фактур

Сопроводительная записка с вводом в боковую колонку

Сопроводительная записка с contenteditable

Генератор QR-кодов

@page

В CSS есть правило @page, указывающее браузеру настройки печати веб-сайта. Обычно я использую

@page
{
    size: Letter portrait;
    margin: 0;
}

Почему я выбрал margin: 0, будет объяснено ниже. Следует использовать Letter или A4, в зависимости от ваших взаимоотношений с метрической системой.

Установка размера и полей (margin) @page — это не то же самое, что установка ширины, высоты и полей элемента <html> или <body>. @page находится за пределами DOM — она содержит DOM. В вебе элемент <html> ограничен краями экрана, а при печати он ограничен @page.

Контролируемые @page параметры более-менее соответствуют параметрам в диалоговом окне печати браузера, открывающемся при нажатии Ctrl+P.

Вот пример файла, который я использовал для экспериментов:

<!DOCTYPE html>
<html>
<style>
@page
{
    /* см. ниже информацию по каждому из экспериментов */
}
html
{
    width: 100%;
    height: 100%;
    background-color: lightblue;

    /* сетка создана shunryu111 https://stackoverflow.com/a/32861765/5430534 */
    background-size: 0.25in 0.25in;
    background-image:
    linear-gradient(to right, gray 1px, transparent 1px),
    linear-gradient(to bottom, gray 1px, transparent 1px);
}
</style>
<body>
    <h1>Sample text</h1>
    <p>sample text</p>
</body>
</html>

Вот как это выглядит в браузере:

but i don't want new chrome

А вот результаты для разных значений @page:

@page { size: Letter portrait; margin: 1in; }:

@page { size: Letter landscape; margin: 1in; }:

@page { size: Letter landscape; margin: 0; }:

Установка размера @page на самом деле не установит этот размер в лотке принтера. Вам придётся менять его самостоятельно.

Обратите внимание, что когда я установил в качестве size A5, мой принтер остался на значении Letter, а размер A5 полностью помещается в размер Letter, что создаёт впечатление наличия полей, несмотря на то, что они берутся не из параметра margin.

@page { size: A5 portrait; margin: 0; }:

Но если я скажу принтеру, что загружена бумага A5, то всё выглядит, как и ожидалось.

Из экспериментов выяснилось, что Chrome следует правилу @page, только если Margin имеет значение Default. Как только вы измените Margin в диалоге печати, результат станет производным от физического размера бумаги и выбранных полей.

@page { size: A5 portrait; margin: 0; }:

Даже если выбрать размер @page, полностью помещающийся в физический размер бумаги, margin всё равно важен. Вот пример квадрата 5x5 без полей и квадрата 5x5 с полями. Размер элемента <html> ограничен общим размером @page и полей.

@page { size: 5in 5in; margin: 0; }:

@page { size: 5in 5in; margin: 1in; }:

Я проделал все эти тесты не потому. что хочу печатать на бумаге A5 или 5x5, а потому, что мне довольно долго пришлось разбираться, что же такое @page. Теперь я уверен, что всегда нужно использовать Letter с margin 0.

Печать @media

Существует медиа-запрос print, позволяющий записывать стили, применяющиеся только во время печати. Мои страницы генераторов часто содержат заголовок, разные опции и вспомогательный текст для пользователя; очевидно, что они не должны выводиться на печать, поэтому сюда добавляется display:none для этих элементов.

/* Обычные стили, отображаемые при подготовке документа */
header
{
    display: block;
}

@media print
{
    /* Пропадают при печати документа */
    header
    {
        display: none;
    }
}

Ширина, высота, поля и отступ

Чтобы не слишком долго бороться с компьютером при получении нужных полей, вам нужно узнать о рамочной модели.

Я всегда устанавливаю @page margin: 0, потому что предпочту обрабатывать поля в самих элементах DOM. Когда я пробовал устанавливать @page margin: 0.5in, то иногда получались двойные поля, сжимавшие содержимое и делавшие его меньше ожидаемого, и мой одностраничный дизайн частично переносился на вторую страницу.

Если бы я хотел использовать поля @page, то содержимое страницы нужно было бы выстроить вплоть до границ DOM; мне сложнее думать об этом и это сложнее отображать при предпросмотре перед печатью. Мне проще помнить, что <html> занимает всю физическую бумагу, а мои поля находятся внутри DOM, а не за его пределами.

@page
{
    size: Letter portrait;
    margin: 0;
}
html,
body
{
    width: 8.5in;
    height: 11in;
}

При работе с генераторами многостраничной печати стоит создавать отдельный элемент DOM для каждой страницы. Так как нескольких <html> и <body> быть не может, нам понадобится другой элемент. Мне нравится <article>. Его можно использовать всегда, даже для одностраничных генераторов.

Так как каждый <article> обозначает одну страницу, мне не нужны поля и отступы в <html> и <body>. Мы размещаем логику на один шаг дальше — проще сделать так, чтобы article занимал физическую страницу целиком и поместить поля внутрь него.

@page
{
    size: Letter portrait;
    margin: 0;
}
html,
body
{
    margin: 0;
}

article
{
    width: 8.5in;
    height: 11in;
}

При добавлении полей в article я пользуюсь не свойством margin, а padding. Причина в том, что margin выходит наружу элемента в рамочной модели (box model). Если использовать margin в 0.5in, то нужно установить article на 7.5×10, чтобы article плюс 2×margin равнялось 8.5×11. А если вам захочется изменить эти поля, то придётся и изменить и другие размерности.

padding находится внутри элемента, поэтому можно задать размер article 8.5×11 с отступом в 0.5in, и все элементы внутри article останутся на странице.

Разбираться в размерностях элементов становится сильно проще, если задать box-sizing: border-box. При этом внешние размерности article фиксированы при настройке внутреннего отступа. Вот мой фрагмент кода:

html
{
    box-sizing: border-box;
}
*, *:before, *:after
{
    box-sizing: inherit;
}

Теперь соединим всё вместе:

@page
{
    size: Letter portrait;
    margin: 0;
}

html
{
    box-sizing: border-box;
}
*, *:before, *:after
{
    box-sizing: inherit;
}

html,
body
{
    margin: 0;
}

article
{
    width: 8.5in;
    height: 11in;
    padding: 0.5in;
}

Позиционирование элементов

После настройки article и margin вы можете настраивать пространство внутри article, как вам угодно. Создавайте дизайн документа при помощи любого HTML/CSS, который кажется вам подходящим для проекта. Иногда для этого нужно размещать элементы при помощи flex или grid, потому что вам дали определённую свободу действий с результатом. Иногда для этого нужно создавать квадраты конкретного размера, чтобы они поместились на конкретной марке бумаги для стикеров. Иногда для этого приходится позиционировать всё в абсолютных величинах, вплоть до миллиметра, потому что пользователю нужно пропустить через принтер специальный кусок бумаги, чтобы напечатать поверх него ваши данные, а вы не имеете контроля над этим куском бумаги.

Я не буду здесь давать рекомендации по написанию HTML, поэтому вы должны уметь делать это сами. Могу только сказать, что нужно помнить, что вы имеете дело с ограниченным пространством бумаги, а не с окном браузера, которое можно скроллить и зумить на любую длину или масштаб. Если документ будет содержат произвольное количество элементов, будьте готовы к реализации пагинации созданием дополнительных <article>.

Многостраничные документы с повторяющимися элементами

Многие из создаваемых мной генераторов печати содержат табличные данные, например, счёт-фактуру с пунктами в виде строк. Если ваша <table> большая и переходит на следующую страницу, то браузер автоматически дублирует <thead> в начале каждой страницы.

<table>
    <thead>
        <tr>
            <th>Sample text</th>
            <th>Sample text</th>
        </tr>
    </thead>
    <tbody>
        <tr><td>0</td><td>0</td></tr>
        <tr><td>1</td><td>1</td></tr>
        <tr><td>2</td><td>4</td></tr>
        ...
    </tbody>
</table>

Это здорово, если вы просто печатаете <table> без лишних украшательств, но в во многих реальных ситуациях всё не так просто. Воссоздаваемый мной документ часто имеет печатный заголовок наверху каждой страницы, сноску внизу и другие специальные элементы, которые должны повторяться на каждой странице. Если просто печатать одну длинную таблицу на все страницы, то вы практически никак не сможете размещать другие элементы выше, ниже и вокруг на промежуточных страницах.

Поэтому я генерирую страницы при помощи Javascript, разбивая таблицу на несколько более мелких. В целом я использую такой подход:

  1. Обращаюсь с элементами <article> как с одноразовыми и готовлюсь перегенерировать их в любой момент из объектов в памяти. Весь ввод и настройки пользователя должны происходить в отдельном блоке заголовка/опций, вне всех article.

  2. Пишу функцию new_page , создающую новый элемент article со всеми необходимыми повторяющимися заголовками/сносками и так далее.

  3. Пишу функцию render_pages, создающую article из базовых данных, вызываю new_page каждый раз. когда она заполняет предыдущую article. Обычно я использую offsetTop , чтобы контролировать, когда контент уходит слишком по странице, но совершенно точно можно использовать более умные методики для идеального размещения на каждой странице.

  4. Вызываю render_pages при каждом изменении базовых данных.

function delete_articles()
{
    for (const article of Array.from(document.getElementsByTagName("article")))
    {
        document.body.removeChild(article);
    }
}

function new_page()
{
    const article = document.createElement("article");
    article.innerHTML = `
    <header>...</header>
    <table>...</table>
    <footer>...</footer>
    `;
    document.body.append(article);
    return article;
}

function render_pages()
{
    delete_articles();

    let page = new_page();
    let tbody = page.query("table tbody");
    for (const line_item of line_items)
    {
        // Обычно я подбираю это пороговое значение экспериментально, но, вероятно, можно
        // задать что-то более строгое.
        if (tbody.offsetTop + tbody.offsetParent.offsetTop > 900)
        {
            page = new_page();
            tbody = page.query("table tbody");
        }
        const tr = document.createElement("tr");
        tbody.append(tr);
        // ...
    }
}

Обычно неплохо добавлять на страницы счётчик «страница X из Y». Так как количество страниц неизвестно, пока не будут сгенерированы все страницы, этого нельзя сделать в цикле for. В конце я вызываю следующую функцию:

function renumber_pages()
{
    let pagenumber = 1;
    const pages = document.getElementsByTagName("article");
    for (const page of pages)
    {
        page.querySelector(".pagenumber").innerText = pagenumber;
        page.querySelector(".totalpages").innerText = pages.length;
        pagenumber += 1;
    }
}

Портретный/альбомный режим

Я показал, что правило @page помогает настроить стандартные параметры печати браузера, но при желании пользователь может их переопределить. Если вы установите в @page портретный режим, а пользователь выберет альбомный, то структура и пагинация могут выглядеть неправильно, особенно если вы жёстко указываете все пороговые значения страниц.

Можно учесть это, создав отдельные элементы <style> для портретного и альбомного режимов и переключаясь между ними при помощи Javascript. Возможно, есть и более хороший способ сделать это, но @-правила наподобие @page ведут себя иначе, чем обычные CSS-свойства, так что я не уверен. Также нужно хранить какую-то переменную, которая позволит функции render_pages работать правильно.

Ещё можно перестать задавать пороговые значения жёстко, но тогда мне придётся следовать своей собственной рекомендации.

<select onchange="return page_orientation_onchange(event);">
    <option selected>Portrait</option>
    <option>Landscape</option>
</select>
<style id="style_portrait" media="all">
@page
{
    size: Letter portrait;
    margin: 0;
}
article
{
    width: 8.5in;
    height: 11in;
}
</style>

<style id="style_landscape" media="not all">
@page
{
    size: Letter landscape;
    margin: 0;
}
article
{
    width: 11in;
    height: 8.5in;
}
</style>
let print_orientation = "portrait";

function page_orientation_onchange(event)
{
    print_orientation = event.target.value.toLocaleLowerCase();
    if (print_orientation == "portrait")
    {
        document.getElementById("style_portrait").setAttribute("media", "all");
        document.getElementById("style_landscape").setAttribute("media", "not all");
    }
    if (print_orientation == "landscape")
    {
        document.getElementById("style_landscape").setAttribute("media", "all");
        document.getElementById("style_portrait").setAttribute("media", "not all");
    }
    render_printpages();
}

function render_printpages()
{
    if (print_orientation == "portrait")
    {
        // ...
    }
    else
    {
        // ...
    }
}

Источник данных

Существует пара способов переноса данных на страницы. Иногда я упаковываю все данные в параметры URL, чтобы Javascript просто делал const url_params = new URLSearchParams(window.location.search);, а затем несколько операций вида url_params.get("title"). У такого подхода есть следующие преимущества:

  • Страница загружается очень быстро.

  • Легко выполнять отладку и экспериментировать, меняя URL.

  • Генератор работает офлайн.

Но у него есть и минусы:

  • URL становятся очень длинными и хаотичными, люди не могут удобным образом отправлять их по почте. См. примеры ссылок в начале этой статьи.

  • Если URL отправляется через электронную почту, то данные «фиксируются», даже если исходная запись в базе данных позже изменяется.

  • У браузеров есть ограничение на длину URL. Оно достаточно большое, но не бесконечное и может быть разным в клиентах.

Иногда вместо этого я использую Javascript для получения записей базы данных через API, поэтому параметры URL содержат только первичный ключ и иногда параметр режима.

У этого есть свои плюсы:

  • URL гораздо короче.

  • Данные всегда актуальны.

и минусы:

  • Пользователю приходится ждать, прежде чем данные будут получены.

  • Нужно писать код.

Иногда я задаю для article contenteditable, чтобы пользователь мог вносить небольшие изменения перед печатью. Кроме того, я люблю использовать реальные действующие чекбоксы, на которые можно нажать перед печатью. Эти функции повышают удобство, но в большинстве случаев лучше сделать так, чтобы пользователь сначала изменил исходную запись в базе данных. Кроме того, это ограничивает возможность работы с элементами article как с одноразовыми.

Шпаргалка по самому важному

sample_cheatsheet.html

<!DOCTYPE html>
<html>
<style>
@page
{
    size: Letter portrait;
    margin: 0;
}
html
{
    box-sizing: border-box;
}
*, *:before, *:after
{
    box-sizing: inherit;
}

html,
body
{
    margin: 0;
    background-color: lightblue;
}

header
{
    background-color: white;
    max-width: 8.5in;
    margin: 8px auto;
    padding: 8px;
}

article
{
    background-color: white;
    padding: 0.5in;
    width: 8.5in;
    height: 11in;

    /* Для центрирования страницы на экране в процессе подготовки */
    margin: 8px auto;
}

@media print
{
    html,
    body
    {
        background-color: white !important;
    }
    body > header
    {
        display: none;
    }
    article
    {
        margin: 0 !important;
    }
}
</style>

<body>
    <header>
        <p>Текст подсказки с объяснением задачи этого генератора.</p>
        <p><button onclick="return window.print();">Print</button></p>
    </header>

    <article>
        <h1>Sample page 1</h1>
        <p>sample text</p>
    </article>

    <article>
        <h1>Sample page 2</h1>
        <p>sample text</p>
    </article>
</body>
</html>

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 30: ↑29 and ↓1+28
Comments17

Articles