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

—

от автора

Введение

По работе я довольно часто занимаюсь созданием генераторов печати на 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 всё равно важен. Вот пример квадрата 5×5 без полей и квадрата 5×5 с полями. Размер элемента <html> ограничен общим размером @page и полей.

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

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

Я проделал все эти тесты не потому. что хочу печатать на бумаге A5 или 5×5, а потому, что мне довольно долго пришлось разбираться, что же такое @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>


ссылка на оригинал статьи https://habr.com/ru/articles/798765/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *