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

7f766f9695dc1a8a6036ac901c2a5247.png

Введение

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

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

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

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

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

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

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

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

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

@page

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

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

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

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

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

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





    

Sample text

sample text

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

but i don't want new chrome

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

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

32dfc1bffcf61b291d80768ace29a9e4.png

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

5a69a85c4b5f6412b2d8743e84dc9249.png

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

6bc3c6dca43111df62eeffde769beb96.png

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

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

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

945e0ffeeb861d0061662751aa1540f9.png

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

cfeff5fbcc951b94ee89d7b176bdd388.png

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

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

b68954b3b63e5e1172b8a2af9cd26682.png755dd41fcb6e9fef9018946cd0181f7e.png

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

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

6956324d450943ea3f1ceffb3a03e1a3.png

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

78f87182b189d053a172675ad91f6afc.png

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

Печать @media

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

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

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

9982ba8f6227aec6fae87272f0f59fb2.pngc0b827ef020d20b3220f8e9b3e43d8a4.png

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

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

cd10e6185381569cea1fddab8ed923d8.png

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

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

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

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

. Его можно использовать всегда, даже для одностраничных генераторов.

Так как каждый

 обозначает одну страницу, мне не нужны поля и отступы в  и . Мы размещаем логику на один шаг дальше — проще сделать так, чтобы 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;
}

166a9e412b554739d9fcaa1d7175610d.png

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

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

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

.

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

Многие из создаваемых мной генераторов печати содержат табличные данные, например, счёт-фактуру с пунктами в виде строк. Если ваша

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

...
Sample text Sample text
00
11
24

61868c46e60e11330a1ef26d1f00cbe7.png

Это здорово, если вы просто печатаете

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

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

  1. Обращаюсь с элементами

     как с одноразовыми и готовлюсь перегенерировать их в любой момент из объектов в памяти. Весь ввод и настройки пользователя должны происходить в отдельном блоке заголовка/опций, вне всех 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 = `
    
...
...
...
`; 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 портретный режим, а пользователь выберет альбомный, то структура и пагинация могут выглядеть неправильно, особенно если вы жёстко указываете все пороговые значения страниц.

Можно учесть это, создав отдельные элементы

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 содержат только первичный ключ и иногда параметр режима.

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

и минусы:

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

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

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

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

sample_cheatsheet.html






    

Текст подсказки с объяснением задачи этого генератора.

Sample page 1

sample text

Sample page 2

sample text

© Habrahabr.ru