Печать офисных документов

Материал из Eludia
Перейти к: навигация, поиск
Но тут гражданин Фунт снова замолк и молчал довольно продолжительное время. "У вас контора", – сказал он, наконец.

И. Ильф, Е. Петров, «Золотой телёнок».

Содержание

Сколь бы ни была удобна и эффективна созданная вами информационная система, вы всё-таки, будучи разумным человеком, должны осознавать вторичность своего детища по отношению к традиционной, бумажной форме учёта. Ни в коем случае нельзя забывать, что ваши основные конкуренты – вовсе не злые IT-гении из соседней промзоны, а шариковая ручка с амбарной книгой. Стремление перевести вообще всё в цифру и загнать всех в сеть – симптом инфантильности проектировщика. Способность системы печатать кипы бумажек, попадая в поля, размеченные десятки лет назад – признак зрелости продукта. И оттачивая детали экранной визуализации, готовясь представить своё творение высшему руководству заказчика, стоит всегда быть готовым к стандартному пожеланию: "Ну вы мне сейчас это всё распечатайте, а я посмотрю... потом".

В силу своей природы WEB-приложение не имеет возможности распоряжаться периферией клиента, в частности, управлять принтером, даже если бы параметры его подключения были известны (применение ActiveX здесь не рассматривается). Однако есть неплохой способ подготовить документ к печати так, чтобы пользователю осталось нажать лишь одну-две кнопки: выдать в качестве HTTP-ответа документ Office. В настоящей главе описываются основные приёмы, применяемые для генерации таких документов Eludia-приложениями.


Предупреждение

Описываемые здесь приёмы базируются на использовании функциональности, появившейся в MS Office 2000: дублирование бинарных форматов файлов специфическим HTML. Это было просто замечательно: возможность генерировать текстовые файлы со всеми необходимыми атрибутами разметки и воспринимаемые офисными приложениями, как родные. Вероятно, тогда была такая политика: дать сторонним WEB-разработчикам удобную возможность открытой интеграции с MS Office.

С выходом MS Office 2007 можно констатировать, что политика поменялась на противоположную: создавать внешним WEB-приложениям достаточно ощутимые проблемы, мягко подталкивая клиентов к переходу на SharePoint. В частности, раньше открытие специально размеченного HTML-файла с расширением doc или xls проходило гладко, а при сохранении по умолчанию предлагался родной бинарный формат, теперь же такая практика рассматривается как угроза безопасности: пока работает, но назойливое предупреждение отключить нельзя.

К тому же в Office 2007 внедрены новые форматы документов на базе XML/ZIP. Теоретически это шаг к открытости, однако на практике всё оборачивается дополнительными проблемами: при импорте документов теперь необходимо поддерживать и старые форматы (совместимость), и дополнительно новые, а генерировать архив из нескольких файлов, к тому же с изолированным словарём строк, разумеется, сложнее, чем монолитный текст по шаблону.

Тем не менее, в течение ещё года-двух можно рассчитывать на то, что Office 2000-2003 будет оставаться достаточно распространённым, чтобы можно было игнорировать совместимость с его форматами, а значит, нижеописанные рецепты пока будут работать. А там — посмотрим.

Базовые положения

Прежде всего, следует взять за правило окрывать любые офисные файлы не в основном окне браузера, а исключительно в невидимом iframe. Само по себе это никогда не вредно, а при использовании HTML Applications (HTA) – совершенно необходимо. Итак, ссылки на всех кнопках и ячейках, приводящие к выдаче печатных форм, должны иметь параметр target=invisible.

Далее, выдачу документа следует оформлять в виде не просмотра, а действия. Скажем, если вам требуется реализовать печать письма (type=letters), то соответствующий URL должен иметь вид не /?type=letters_print и ни в коем случае не /?type=letters&print=1, а /?type=letters&action=print. Практика показывает, что печатные формы разможаются почкованием, а копировать и слегка править маленькие процедуры do_print_... в рамках одного content-файла гораздо удобнее, чем раздувать множество типов и тем более чем зашумлять монолитную процедуру просмотра лишним кодом.

Оптимальный формат выходных документов – HTML с расширениями Office или его multipart-вариант MHT: он позволяет использовать практически все типографские функции, будучи текстом, а следовательно, допуская (сравнительно) простую генерацию по шаблонам. Чтобы вызывать для просмотра документа вместо браузера Word или Excel, есть только одно надёжное средство: объявлять заголовками о загрузке файла с расширением .doc или .xls соответственно. Извините за назойливость, но на всякий случай напоминаем, что выдаваемое имя файла не имеет никакого отношения к тому, как вы храните шаблон. И не надо, скажем, приписывать HTML-шаблонам расширение .xls: это никак не влияет на HTTP-ответ, но сбивает с толку текстовый редактор, то есть усложняет вам жизнь.

Итак, мы будем генерировать HTML, подставляя данные из БД в заранее заготовленные шаблоны. Встаёт вопрос: как именно подставлять? Если сильно спешить, несложно придти к гибельному решению: разаработать специальный язык разметки и реализовать его, скажем, с помощью регулярных выражений. Или воспользоваться плодами трудов тех, кто уже совершил подобную глупость (Text::Template и ещё зиллионы подобного). В Eludia-приложениях для этих целей используется родная интерполяция, реализованная в Perl 5 на уровне интерпретатора.

Словоформы

Если вы печатаете статистический отчёт, то извлечение данных вполне может совпасть с какой-либо процедурой get или select (на всякий случай, напоминаем: лучше вызвать её из-под do_print, но ни в коем случае не выдавать документ из-под get/select!). А вот письма и приказы зачастую требуют специфической обработки данных: вычисления словоформ. Если генерируемые вами документы будут, скажем, всегда подразумевать адресата мужского пола ("Уважаемый...") или ставить различные имена исключительно в именительном падеже ("Назначть Иванов И. И. начальником Первый отдел"), толку от формирования таких документов не будет: всё равно исполнителю придётся править всё вручную. Так что относиться к русской грамматике следует со всей серьёзностью.

Если в документе используется заранее известное слово, а от данных зависит только окончание ("Уважаем[ый|ая]"), то всё тривиально: вы вычисляете окончание по данным, сохраняете его в виде одной из компонент $DATA и подставляете в шаблон. К слову сказать, пол физического лица редко вводится напрямую, но в 99% случаев вычисляется по отчеству (да, встречаются, конечно, Оглы, Кызы и NULL, но, к счастью, нечасто). При вычислении окончания важно только не забыть предусмотреть все варианты, особенно при согласовании с числительными ("11 участников", но "21 участник").

А вот если вам требуется получить заданную падежную форму для заранее неизвестного слова или, тем более, словосочетания, да ещё и имени собственного – тут не стоит и пытаться вычислять что-либо. Даже если вы аккуратно выпишите из томика Розенталя все правила склонения имён и у вас хватит терпения реализовать их в виде Perl-функции – она обязательно будет требовать, помимо исходной формы и целевого падежа, ещё дополнительных аргументов. Маленький пример: склонение иноязычных мужских фамилий, оканчивающихся на -а, зависит от языка оригинала. В частности, испанские фамилии изменяются по правилам 1-го склонения (Панса), а французские – не изменяются (Дюма).

Да, падежные формы неизвестных слов может задавать только оператор. Но для одной и той же словоформы не стоит заставлять его делать это многократно, в каждом новом документе. Если вы готовитесь распечатать документ и у вас (сервера) возникли сложности по поводу написания таких-то имён в таких-то падежах, то пользователю надо показать промежуточный экран с конкретными вопросами. После того, как он учточнит словоформы и нажмёт "применить" – вы сгенерируете документ, вооружённые новыми знанями. А ответы на вопросы останутся в таблице voc и будут использоваться в дальнейшем.

И ещё одно важное замечание: женские фамилии, оканчивающиеся на согласные, не склоняются. Следовательно, при вычислении формы фамилии всегда надо обращать внимание по последнюю букву. Если она согласная, то запрос на падежную форму и генерировать не следует: в этом случае фамилия должна иметь форму именительного падежа ("Марии Ивановне Рыбец", а не "Марии Ивановне Рыбцу"). Неправильный вариант может оказаться результатом корректного срабатывания словаря.

Подготовка шаблона

Итак, вам требуется превратить имеющийся документ Office в шаблон. Откройте этот документ в родном пакете и проанализируйте, с чем вам придётся работать. Прежде всего добейтесь того, чтобы "рыба" была заполнена адекватными тестовыми данными. В частности, все цифровые поля должны содержать числа требуемого формата и подходящего порядка, а текстовые – надписи, взятые из реальной жизни. Это нужно не только для отсеивания казусов типа 1 сантиметра на колонку "ФИО, дата рождения, документ, удостоверяющий личность". Если "рыба" выглядит жизненно, значит, в HTML увас будут экспортированы все необходимые CSS-стили.

Далее (если только ваш постановщик задачи не сделал этого) следует взять жёлтый маркер и отметить все фрагменты текста, которые могут зависеть от данных. В дальнейшем их будет легко найти в HTML-коде по слову 'yellow'. Маркер не меняет CSS-стиль содержимого, а порождает только охватывающий элемент span, который можно впоследствии безболезненно убрать.

Теперь – сохраняемся в HTML ("как Веб-страницу"). Если вместо одного файла создаётся несколько, следует всё это стереть и выбрать формат MHT ("Веб-страница в одном файле"). Результат получается несколько ошарашивающим: кодировка всегда us-ascii, а все неамериканские символы конвертированы в цифровые entities. То есть читать русский текст может только тот, кто с детства учил кирилличекую азбуку не в графике, а по номерам Unicode. Но это всё не проблема: достаточно применить процедуру decode_entities. Она же заменяет символ '$' на '$': это помогает избежать многих сюрпризов при последующей интерполяции.

Читая офисный HTML "глазками", поражаешься обилию лишних тегов. Тем не менее, не стоит без большой нужды очищать шаблон от них – тут очень трудно угадать, где именно что аукнется. Конечно, если конец фразы "... 1 января 2000 г." сопровождается метрическим конвертером на 2 килограмма – это вряд ли определяет разбиение на страницы, но... пусть уж валяется – кушать-то не просит. А уберёшь — неровён час, Word обидится.

Сохранив монолитный HTML приемлемого вида в lib/Presentation/templates, напишите do_print_..., состоящую из единственного вызова fill_in_template, который выводил бы исходный документ как есть:

sub do_print_my_document {

  fill_in_template (my_document => 'Письмо.doc');

}

и нарисуйте соответствующую кнопку где-нибудь в презентационной части:

{
 icon   => 'print',
 label  => 'Печать (F10)',
 href   => {action => 'print'},
 hotkey => {code => F10},
 target => 'invisible',
},

Убедитесь, что в отсутствие какой-либо интерполяции всё работает или добейтесь, чтобы так было. Вы получили "рыбу"? Отлично, теперь надо открыть шаблон в текстовом редакторе и пройтись по всем жёлтым фрагментам, подставив туда данные.

Особенности Word

Если вы сохраните Word-документ как HTML, а потом откроете его (даже безо всякого WEB-сервера), то редактор будет в режиме "WEB-документ", а это, как правило, наименее желательный вариант. Чтобы файл открывался в виде "разметки страницы", достаточно приписать в раздел <WordDocument> фрагмент

<w:View>Print</w:View>

Возможно, вы хотите задать ещё и масштаб – его можно приписать следующей строкой:

<w:Zoom>80</w:Zoom>

Встречается ещё такое требование: чтобы документ был открыт только на чтение и пользователь не мог ничего изменить. Вообще-то, с MS Word это невозможно, поскольку содержимое защищённого паролем документа легко переносится через clipboard в новый, незащищённый. А в HTML пароль вообще не сохраняется ни за какие деньги. Тем не менее, если срочно требуется задурить голову наивным людям, которые никогда не найдут в меню пункт "Снять защиту", то припишите ещё

<w:DocumentProtection>Forms</w:DocumentProtection>

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

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

Встечаются случаи, когда некая фиксированная надпись (скажем, ФИО исполнителя) должна присутствовать внизу последнего листа документа, длина которого определяется данными. Попытки рассчитать необходимое количество пустых абзацев бесперспективны: даже если вы знаете количество букв, его довольно сложно пересчитать в количество строк, поскольку текст, как правило, печатается пропорциональным шрифтом, а пробелы при выравнивании justified имеют переменную длину. Решение же задачи заключается в том, чтобы заключить требуемую надпись в плавающий фрейм, расположенный "перед текстом", но с запрещением перекрытия и остоящий от верха страницы, скажем, на 28 см.

Тем не менее, к самодельной нарезке на строки иногда приходится прибегать, но при этом вы не пытаетесь имитировать расчёты редактора, а явно задаёте ему множество строк. Такое случается при упихивании длинной строки в узкую колонку таблицы, которая может расти в высоту. К примеру, наименование подразделения и должность в служебном задании на командировку могут нарезаться на 1-4 строки. Тут приходится задаваться некоторым предельным количеством букв и делить строки по пробелам.

Если вы реализуете рулонную печать, то есть умножаете, скажем, один шаблон письма на количество адресатов в выборке, то, возможно, всё, что вам нужно – это воспользоваться комбинацией map и here-документов (см. выше) да добавить в конец шаблона разрыв страницы:

< br clear=all style='mso-special-character:line-break;page-break-before:always'>

Всё так просто только в том случае, если шаблон не содержит графики: скажем, логотипа. Если же это так, то вам предстоит некоторый объём дополнительной работы: найти все внутренние id-атрибуты, связанные с тегами, имеющими префикс 'v:' (это VML) и приписать к их значениям, скажем, id записи в выборке. Иначе вся графика сосредоточится на одной странице.

Особенности Excel

Перед сохранением из Excel в HTML обязательно сделайте Print Preview. Смотреть на экран не обязательно: сам по себе предварительный показ создаёт некоторые дополнительные элементы, без которых вы впоследствии не сможете, например, поменять формат бумаги.

Не пытайтесь "сохранить лист". Только "книгу целиком".

При интерполяции числовых данных подставляйте значения не в текст тегов td, а в их атрибуты x:num. При этом можно использовать десятичную точку, а не запятую – значения будут отформатированы в соответствии со стилем. Ура.

Разрывы страниц

Редкое издевательство демонстрирует Excel над тем, кто пытается управлять разбиением на страницы средствами HTML. Стиль page-break-before:always имеется, он создаётся при сохранении, но влияния на документ не оказывает никакого. А номера строк, на которых рвутся страницы, идут совершенно отдельным списком в разделе xml.

<xml>
 <x:ExcelWorkbook>
  <x:ExcelWorksheets>
   <x:ExcelWorksheet>
    <x:Name>...</x:Name>
    ...
    <x:PageBreaks>
     <x:RowBreaks>
      <x:RowBreak>
       <x:Row>10</x:Row>
       <x:Row>20</x:Row>
       ...
      </x:RowBreak>
     </x:RowBreaks>
    </x:PageBreaks>
   </x:ExcelWorksheet>
  </x:ExcelWorksheets>
  ...
 </x:ExcelWorkbook>
</xml>

Сквозные строки

Шапки таблиц, общие для всех печатаемых страниц, задаются фрагментом XML, приписываемым в самый конец, после x:ExcelWorkbook (в приведённом примере фиксируются первые 2 строки):

 ...
 </x:ExcelWorkbook>
 <x:ExcelName>
  <x:Name>Print_Titles</x:Name>
  <x:SheetIndex>1</x:SheetIndex>
  <x:Formula>=Лист1!$1:$2</x:Formula>
 </x:ExcelName>
</xml><![endif]-->
</head>

Тут есть тонкая неприятность. Фрагмент текста '$1' будет сочтён за подстановку переменной $1, и закавычивание вида '\$1' не сработает. Однако "выливанием воды из чайника сведём задачу к ранее рассмотренной": значок доллара можно печатать не как литерал, а как результат вычисления списка. Overhead будет, но неощутимый глазом.

 ...
 </x:ExcelWorkbook>
 <x:ExcelName>
  <x:Name>Print_Titles</x:Name>
  <x:SheetIndex>1</x:SheetIndex>
  <x:Formula>=Лист1!@{[ '$' ]}1:@{[ '$' ]}2</x:Formula>
 </x:ExcelName>
</xml><![endif]-->
</head>

Колонтитулы

Руссификация Excel на уровне имён функций и переменных — тема многих хороших анекдотов. Кроме смеха, это серьёзная проблема: некоторые версии Excel требуют кириллических имён объектов, а некоторые (причём локализованные) — английских. В частности, на одном рабочем месте может оказаться неработоспособным колонтитул

mso-footer-data:"&Л&Д&ПСтраница &С из &К";

а на другом — наоборот

mso-footer-data:"&L&D&RСтраница &P из &N";

По этому поводу написано в хорошей статье на ЦИТфоруме, однако там речь идёт о толстых клиентах на Delphi, соответственно, предполагается, что Excel доступен как ActiveX-объект и у него можно, например, узнать

XL.LanguageSettings.LanguageID [msoLanguageIDUI]. 

А мы генерируем такой HTML, который будет читать неизвестно кто.

Здесь есть поле для исследования возможностей конструкции <!--[if ...]>, однако можно поступить просто и предельно цинично: привести оба написания каждой переменной.

mso-footer-data:"&Л&L&Д&D&П&RСтраница &С&P из &К&N";

Печать экрана без вёрстки шаблона

Если шаблон имеет достаточно простую структуру (например, одна таблица стантартным стилем), имеет смысл не разрабатывать отдельный шаблон, а сверстать обычную страницу при помоши draw_table и вызвать её с параметром xls=1 — при этом используется $_SKIN под названием Eludia::Presentation::Skins::XL. В этом случае писать гораздо проще, хотя возможности управления форматированием заметно ограничены. Практически все они сводятся с установке атрибутов td опцией attributes, а также использованию параметров $_REQUEST {_style} и $_REQUEST {_xml}:

$_REQUEST {_xml} = <<EOX;
 <x:ExcelWorkbook>
  <x:ExcelWorksheets>
   <x:ExcelWorksheet>
    <x:Name>dmnds_pays_register</x:Name>
    <x:WorksheetOptions>
     <x:Print>
      <x:ValidPrinterInfo/>
      <x:PaperSizeIndex>9</x:PaperSizeIndex>
      <x:Scale>85</x:Scale>
      <x:HorizontalResolution>600</x:HorizontalResolution>
      <x:VerticalResolution>600</x:VerticalResolution>
     </x:Print>
     <x:Selected/>
     <x:DoNotDisplayGridlines/>
     <x:ProtectContents>False</x:ProtectContents>
     <x:ProtectObjects>False</x:ProtectObjects>
     <x:ProtectScenarios>False</x:ProtectScenarios>
    </x:WorksheetOptions>
   </x:ExcelWorksheet>
  </x:ExcelWorksheets>
  <x:WindowHeight>14475</x:WindowHeight>
  <x:WindowWidth>20970</x:WindowWidth>
  <x:WindowTopX>120</x:WindowTopX>
  <x:WindowTopY>105</x:WindowTopY>
  <x:ProtectStructure>False</x:ProtectStructure>
  <x:ProtectWindows>False</x:ProtectWindows>
 </x:ExcelWorkbook>
EOX

$_REQUEST {_style} = <<EOX;
 \@page
  {
  margin:.2in .2in .2in .2in;
  mso-header-margin:0in;
  mso-footer-margin:0in;
  mso-page-orientation:landscape;
  mso-horizontal-page-align:center;}
EOX
Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
Разработчику
Администратору
Инструменты