содержание   назад   вперед


Виктор Кон,   Постскрипт: инструкция к применению

4. Третий уровень программирования

4.1 Замечания по ходу

Знания основ программирования на Постскрипте в рамках второго уровня вполне достаточно для описания любых сложных рисунков. Наиболее удобными являются чертежи и различные схемы с небольшим числом деталей. Как вы уже заметили, язык содержит только примитивные операции. Иногда их надо записывать очень много, например, если изобразить зависимость, полученную в виде 1000 точек. Но и это не проблема - есть циклы. Однако все же чего-то не хватает. Во-первых, нет русских фонтов. Во-вторых, непонятно как описывать растровые картинки. Очень мало сказано про массивы. Совсем ничего не сказано про словари. Ну и работа с файлами.

Поэтому, если кто недоволен и хочет узнать больше, то может подниматься на третий уровень. В этом разделе я поясню более подробно структуру работы интерпретатора. Мы уже знаем, что все, что написано в программе, заносится в стек. Этот стек называется стеком операций и он не единственный. Как вы уже понимаете, все определения процедур в стек не заносятся. На самом деле это не так. Определения процедур представляют собой пару объектов (имя - процедура). Набор таких пар в Постскрипте называется словарем. И для словарей есть свой стек, мы будем называть его дикт-стек. В этом стеке всегда находятся два словаря. Первый словарь - системный, он устанавливает соотвествие между операторами языка Постскрипт и процедурами, которые надо выполнить. Он находится в самом низу и просматривается последним. Второй словарь - пользовательский. В него заносятся все определения, которые делаются в программе. Не только те, что записаны в Преамбуле, но и вообще все. Этот словарь просматривается раньше и он в частности может переопределить даже операторы самого языка. Но можно объявить любой другой словарь как объект. И заполнить его парами (имя-процедура) как элементами. Этот словарь можно поставить в дикт-стек, все такие словари будут просматриваться раньше остальных, но за порядком можно следить. Я сам словарями не пользуюсь, но при описании фонтов - это необходимо. Работа со словарями очень похожа на работу с массивами и строками. Поэтому разумно все это рассмотреть вместе.


4.2 Словари, массивы и строки

Итак, строка - это набор символов, массив - это набор любых объектов, словарь - это набор определений. Мы знаем, что строку можно задать текстом с круглых скобках (abcd), а массив - элементами в квадратных скобках [45 (df) 4.56]. Такие определения сразу задают и размер и содержание. Но есть стандартные операторы, которые сначала резервируют место под набор элементов, а затем могут вносить или извлекать элементы из набора. Оператор, задающий строку символов, мы уже встречали. Это

string -- он берет из стека одно целое число как размер строки и создает пустую строку такого размера. Аналогично

array -- берет из стека одно целое число как размер массива и создает пустой массив такого размера. И наконец

dict -- берет из стека одно целое число как размер словаря и создает пустой словарь такого размера.

Данные операторы очень удобны тем, что при создании этих объектов им сразу можно присвоить имя, используя конструкцию типа

/str 10 string def или

/arr 10 array def или

/dic 10 dict def.

Мы только что определили строку, массив и словарь размером 10 элементов и присвоили им имя. Эти определения очень похожи на задания массивов и строк в других языках программирования, например, в Java. Теперь мы можем манипулировать с ними, используя их имена. Очевидно самые нужные операции -- наполнение пустых мест содержанием и использование элементов набора. У нас как бы есть автоматическая камера хранения на вокзале, имеющая пронумерованные ящики. Мы можем открыть ящик и положить в него что-то. А в другое время открыть ящик и вынуть из него это же самое. Такие операции выполняют команды put и get, которые на английском языке как раз имеют смысл "положить" и "взять". Команда put берет из стека значение (процедуру), затем индекс (имя), затем имя набора и выполняет операцию. Проще всего это показать на примерах

str 5 65 put

arr 5 (abs) put

dic /A 65 put

Как видно на примере, символы в строку вводятся своими ASCII кодами. Так 5-м элементом строки srt будет символ "A". А в словарях ключи задаются литерами, как в определениях. Команда get берет из стека индекс (имя), затем имя набора и возвращает значение (процедуру).

str 5 get --> 65

arr 5 get --> (abs)

dic /A get --> 65

Ранее было сказано, что оператор цикла forall работает с массивами. Но он также работает и со словарем. Он берет из словаря каждую пару (имя-значение) и помещает их в стек, затем выполняет процедуру. Если процедура пуская, то все пары из словаря переписываются в стек. Указанные команды позволяют работать с отдельными элементами. Но если надо определить весь словарь сразу, то более эффективным способом является использовать операторы begin и end. Оператор

begin -- берет из стека имя словаря и устанавливает его текущим словарем. После этого можно смело писать одно определение за другим, как это обычно делается в Преамбуле. При этом пары (имя-процедура) попадают в словарь в порядке следования их в программе. Так как словарь имеет конечное число элементов, то после его полного заполнения нужно выполнить команду

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


4.3 Создаем собственный фонт

В этом разделе я покажу пример программы создания собственного фонта с помощью обычных графических операторов. Такой фонт интересен тем, что вместо символов в него можно записать любые картинки, которые затем можно многократно использовать как рисование обычных символов. В частности можно нарисовать русские буквы или псевдографику или систему иконок и вообще все, что необходимо многократно вставлять в текст наряду с обычным текстом. Так как очень долго все объяснять я покажу пример кода, с помощью которого я создал фонт русских символов, но для краткости нарисую только один символ. Полный код прилагается в отдельном файле. Итак я сначала покажу весь код, а потом объясню отдельные важные моменты.

/Rf{20 slw 1 setlinejoin 1 setlinecap /Cyr30vk f}bd
/R{14 Rf 2 1 rm s -1 -1 rm}bd /Ec{Encoding}bd
/mW{169 def}bd /mw{147 def}bd /mn{125 def}bd /bW{[-35 -35 134 155] def}bd
/bw{[-35 -35 112 155] def}bd /bn{[-35 -35 90 155] def}bd
%% ------ Russian font /Cyr30vk -------
10 dict dup begin
/FontType 3 def
/FontMatrix [0.0042 0 0 0.0046 0 0] def
/FontBBox [-35 -35 134 155] def
/Encoding 128 array def
0 1 127{Ec exch /.notdef put}for Ec 65 /A put Ec 97 /a put
/Metrics 3 dict def Metrics begin /.notdef mn /A mw /a mn end
/BBox 3 dict def BBox begin /.notdef [0 0 0 0] def /A bw /a bn end
/CharProcs 3 dict def CharProcs begin
/.notdef {} def /A{0 0 m 50 140 l 100 0 l sl 22 50 78 50 ls}bd
/a{0 85 m 30 110 60 110 76 85 c 76 15 79 0 83 0 c sl 70 65 m -30 65 -10 -50 76 30 c sl}bd end
/BuildChar{0 begin /char ed /fontdict ed /charn fontdict /Encoding get
char get def fontdict begin Metrics charn get 0 BBox charn get aload pop
setcachedevice CharProcs charn get exec end end} def
/BuildChar load 0 3 dict put /UniqueID 1 def end
/Cyr30vk exch definefont pop
%% --- end of font --------

Здесь первая и вторая строки кода фактически настраивают фонт на одну букву R стандартным размером 14 pt и немного корректируют способ постановки букв, чтобы он правильно смотрелся рядом с другими фонтами. После такой настройки текст можно писать так (123)L(Aa)R и русские буквы будут рисоваться наравне с остальными. Я специально это проверил - все работает. Кроме того, я активно использую команды языка vkPS, который я описывал раньше. Остальные определения просто вводят более короткую запись, обратите внимание, что они содержат оператор def, то есть данные команды сами определяют другие команды. Чтобы не удивляться можете рассматривать процедуры в определениях просто как кусок текста, который надо поставить вместо имени и выполнить. Само описание фонта начинается после комментария. Оно начинается с объявления словаря размером 10 элементов. Так как словарь не имеет имени, то для команды begin вместо имени используется команда dup. Первым элементом словаря должно быть имя /FontType. Мы определяем его как 3. Вообще говоря типов фонтов очень много. Тип 3 означает, что буквы фонта будут описаны обычными графическими процедурами Постскрипта. То есть мы их можем описывать любыми картинками, нарисованными линиями, областями и вообще всеми средствами, которые мы только что изучили. Следующий элемент /FontMatrix задает массив из 6 элементов, представляющий собой матрицу преобразования CTM (coordinate transformation matrix). Это мы еще не проходили, но об этом будет рассказано в следующей секции.

Следующий элемент /FontBBox задает Bounding Box для рисования символов, то есть область в системе координат, в которой будет рисунок букв. Следующий элемент определяет массив /Encoding. Вообще фонт может содержать до 256 знаков (полную ASCII таблицу), но набрать с клавиатуры можно только первые 128 знаков, поэтому нет смысла задавать остальные. Наш фонт будет иметь 128 знаков, соответсвенно массив имеет такой размер. Далее этот массив надо определить. Это делается в цикле. Сначала всем элементам присваивается литеральное имя /.notdef , то есть не определен. Затем мы даем имя /A 65-му элементу и /a 97-му элементу. Каждый элемент должен иметь свое уникальное имя, но здесь только пример, поэтому в нашем фонте будут только 3 буквы, одна из них - пустышка. Так как слово Encoding нужно было писать много раз, я его сократил до Ec. Соотвественно для словаря /Metrics уже достаточно 3-x элементов. Метрика просто задает ширину букв. Можно было бы в цикле всем задать одинаково, но я выделил три размера: для заглавных букв, для маленьких и промежуточный и для сокращения записи определил их заранее. Такое сокращение необходимо, когда описываются не 3, а 65 букв. Далее идет словарь /BBox, который устанавливает Bounding Box для каждого символа индивидуально. Иногда это можно использовать. У меня снова заготовлено три массива, так что они просто указываются. И вот мы подошли к самому главному: следующий словарь /CharProcs как раз и описывает все процедуры конкретного рисования символов. То есть при появлении символа в тексте будет запускаться именно та процедура, которая указана для этого символа в данном словаре. У нас опять только три символа, но в файле есть все.

Для понимания остального кода у нас нехватает образования но важно, что он совершенно стандартный и тут от нас уже ничего не зависит. Его можно просто копировать при создании любого фонта. Пожалуй стоит только отметить, что последняя команда присваивает нашему фонту имя Cуr30vk. Замечу также, что так как этот фонт нестандартный, то нет смысла держать его в отдельном файле. Интерпретатор Gstools его не найдет. Точнее, даже если вы и настроите свой собственный интерпретатор, но ваши коллеги не смогут это сделать. Поэтому разумно вставлять определение фонта в любой постскрипт файл, в котором он используется прямо в Преамбулу. Это все равно компактнее, чем использовать для русских текстов растровые картинки. По указанному образцу можно создать свои фонты для любых знаков или переписать мои русские буквы на свой манер.


4.4 Некоторые сведения для образования

Здесь я сообщю некоторую информацию, которую нужно знать, хотя необходимость ее использования встречается не так уж часто. Прежде всего расскажу про CTM - матрицу преобразования системы координат. Это полезно, потому что она используется не только в языке Постскрипт, но и перекочевала в другие языки. Эта матрица задает преобразование системы координат самого общего вида. Она имеет 6 элементов и мы условно ее обозначим как

CTM == [a b c d tx ty] -- Тогда общее преобразование системы координат выглядит следующим образом

x' = a*x + c*y + tx; y' = b*x + d*y +ty

Очевидно операции translate = [1 0 0 1 X Y], scale = [X 0 0 Y 0 0], rotate = [C S -S C 0 0], где C = cos(a), S = sin(a) и a - преобразование угла в радианы из градусов, являются частными случаями этого преобразования. Используя CTM можно выполнять более сложные преобразования. Различные CTM матрицы умножаются по определенным законам. Можно сразу установить CTM командой

setmatrix -- она берет матрицу из стека и устанавливает ее в качестве текущей матрицы преобразования без учета всех предыдущих преобразований. Используется довольно редко. Чаще используется команда

concat -- она берет матрицу из стека и умножает ее на текущую матрицу преобразования. Именно так работают стандартные операторы изменения системы координат.

Далее, нужно знать, что объекты разных типов можно преобразовывать, изменяя их тип. Мы уже сталкивались с оператором

cvs -- который берет из стека строку и число и возвращает новую подстроку, в которой число преобразовано в текст. На самом деле таких операторов много, все они начинаются с буквы "c" (convert). Я просто перечислю их здесь

cvi -- берет из стека число или строку и возвращает целое число, полученное из аргумента по определенному правилу.

cvr -- берет из стека число или строку и возвращает реальное число, полученное из аргумента по определенному правилу.

cvx -- берет из стека произвольный объект и делает его исполняемым, если он таковым не был (короче /A превращает в A).

cvlit -- берет из стека произвольный объект и делает его литеральным, если он таковым не был, то есть именем, меткой. Обратный к предыдущему. Есть еще два оператора cvrs и cvn, но я не уверен, что их кто нибуль использует.

Важно также знать, что в языке Постскрипт очень много операторов, имена которых начинаются со слова сurrent. Как правило они помещают в стек некоторую информацию, которую можно использовать в дальнейшем. Так оператор

currentpoint -- помещает в стек координаты X и Y текущей точки, которые можно распечатать. Естественно, что при задании координат командой m мы их и так знаем. Но иногда бывают очень запутанные ситуации, например, после выполнения команды fill. Другой оператор

currentrgbcolor -- помещает в стек три числа R G B - компоненты текущего цвета. Я указываю именно такой порядок с каким они идут в стек. Интересный оператор

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

currentuserparams -- помещает в стек словарь, содержащий все определения, сделанные в программе. Я укажу также те операторы, которые возвращают состояния, нам уже известные. Так

currentdush -- помещает в стек матрицу и число

currentlinecap -- помещает в стек целое число

currentlinejoin -- помещает в стек целое число

currentlinewidth -- помещает в стек число

currentgrey -- помещает в стек число

currentdict -- помещает в стек словарь не вынимая его из дикт-стека

currentmatrix -- берет из стека пустую матрицу из 6 элементов и возвращает CTM матрицу

currentfile -- помещает в стек файл, с которым работал последним, о файлах будем еще говорить

currentfont -- помещает в стек фонт, который был установлен последним командой setfont.


4.5 Работа с файлами

В вычислительных языках работа с файлами очень важна. Но в Постскрипте главная задача - показать принтеру или программе Gstools как надо сделать картинки. Собственно говоря, принтер не любит много файлов. Также много файлов неудобны при передаче документа. Однако при отладке программы и в ряде специальных случаев использование файлов оправдано. В этом разделе показано как использовать файлы в постскрипт программе. Прежде всего, аналогично string, array, dict есть команда

file -- она берет из стека две строки: параметр доступа ((r) или (w)) и имя файла и создает новый объект типа file. Команда

renamefile -- берет из стека две строки: второе имя файла, первое имя файла и переименовывает имя файла с первого на второй. Команда

fileposition - берет из стека объект типа file и возврашает целое число как число уже прочитанных в файле байтов. Далее есть несколько команд чтения из файла. Первая

read - берет из стека объект типа file и возврашает целое число как содержимое одного байта, и true если операция успешна или false, если файл, например, закончился.

readhexstring - берет из стека пустую строку и объект типа file, а возврашает заполненную строку символами из файла, причем в файле символы записаны каждый двумя байтами в 16-ричной системе, а также true если операция прошла успешно, иначе false.

readline - берет из стека пустую строку и объект типа file, а возврашает заполненную строку символами из файла до тех пор, пока не встретится символ конца строки, и true если операция успешна или false, если файл, например, закончился.

readstring - берет из стека пустую строку и объект типа file, а возврашает заполненную строку символами из файла до тех пор, пока строка не заполнится или файл не закончится. В первом случае возвращает true, во втором false. Легко догадаться, что команды записи в файл работают примерно также. Так

write - берет из стека целое число (эквивалентное байту), объект типа file и записывает один байт в файл. Эта команда ничего не возвращает, в случае неудачи возникает ошибка.

writehexstring - берет из стека строку, объект типа file и записывает в файл строку каждый символ двумя 16-ричными байтами, ничего не возврашает.

writestring - берет из стека строку, объект типа file и записывает в файл всю строку, ничего не возврашает. Наконец файл надо закрыть. Это делает команда

closefile - она берет из стека объект типа file и разрывает связь с ним, выполняя все необходимое, чтобы записать файл в полном виде в операционной системе. Есть еще одна команда, которая использует файлы. Она может быть особенно полезной при отладке программы. Это

run -- она берет из стека имя файла как строку и читает в этом файле все постскрипт команды так как будто они находятся в родном файле, пока файл не закончится.


4.6 Кодирование растровых изображений

В этом разделе мы не будем касаться вопроса о том, как создавать растровые изображения. Они обычно создаются приборами, например, фотоаппаратом, или специальными графическими программами. Есть также графический блок в любом языке программирования, в том числе и в моем языке ACL. Речь ниже пойдет только о том, как их записать в постскрипт файл с тем, чтобы потом применить все преобразования и совместить с другими возможностями языка Постскрипт. Так как постскрипт -- это текстовый язык, то мы не можем использовать байты напрямую, а значит не можем и применять алгоритмы сжатия. В Постскрипте растровые изображения записываются точка за точкой причем каждый байт кодируется двумя 16-тиричными числами. Предположим, что у нас уже есть такая матрица 16-тиричных чисел. Как указать программе, что мы хотим получить растровую картинку. Для черно-белых картинок существует команда

image -- она берет из стека нужную информацию и показывает картинку. Так как информация порой очень большая, то имеет значение разумная организация данных. Проще всего показать пример и объяснить его код. Пусть нам дана черно-белая картинка и каждая точка описывается одним байтом. Размер картинки 200х300. Тогда код может быть таким

/str 200 string def /pspic{gs 200 300 8 [200 0 0 300 0 -300]
{currentfile str readhexstring pop} image gr} def
0 -300 translate 200 300 scale pspic
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
и так далее

Разберем что здесь написано. Наша картинка размером 200 на 300 байтов. Совсем не обязательно считывать сразу всю картинку в строку символов. Можно это делать по частям. Поэтому мы заказали пустую строку с именем str на один ряд точек, то есть размером 200 знаков. После этого мы определили процедуру (это совсем не обязательно, но возможно). В этой процедуре запомнили состояние графики, затем ввели три числа: ширину, высоту картинки и число битов на один пиксель, после этого задали CTM и затем процедуру получения данных. Данные читаются из файла командой readhexstring, в качестве файла чтения ей указан, текущий файл, то есть файл программы и наша строка, которая на выходе возвращает строку из файла и true. Последний объект мы из стека убираем. После этого стоит команда image. Теперь ясно, что перед запуском этой команды в стек надо положить размеры картинки, число битов на пиксель, CTM и процедуру. Эта команда будет использовать процедуру до тех пор, пока не получит в стеке данные на всю картинку. В нашем случае - это 300 раз. А что дальше? После определения процедуры, мы устанавливаем условия рисования и запускаем процедуру. Данные для самой картинки должны следовать сразу за процедурой, но можно с новой строки, так как при чтении все знаки, кроме 1-9,a-f ингорируются. Мы должны записать 200*300*2 байтов на всю картинку. Самый сложный момент для понимания - это конкретный вид матрицы преобразования CTM и аргументы у операторов translate и scale. Опять же совсем не обязательно понимать все. Но важно знать, что данные аргументы предполагают чтение пикселей картинки в порядке слева направо и снизу вверх. То есть сначала читается нижний ряд, последним читается верхний ряд. А режим масштабирования устанавливает размер 1 pt на 1 пиксель.

Если заменить CTM на [200 0 0 -300 0 300] и поставить 0 0 translate в указанной выше процедуре, то картинка будет читаться слева направо и сверху вниз. Такой способ более распространен в графических программах. Все это надо понимать, чтобы правильно задать данные. Сами 16-тиричные данные нужно получать с использованием программ конвертирования картинок. Одной из таких программ является vkACL, в которой можно вставить в постскрипт файл черно-белую или цветную картинку. Для цветных картинок надо использовать команду

colorimage -- эта команда во многом аналогична предыдущей, но программу можно записать по другому, используя словарь. Я просто покажу пример как это можно сделать по другому. Итак

save 10 dict begin /xp 200 def /yp 300 def /rb 600 def
/bpс 8 def /ctm [1 0 0 -1 0 yp] def /str rb string def
/readimage {currentfile str readhexstring pop} def
xp yp bpc ctm /readimage load false 3 colorimage
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
и так далее
end restore

Новое здесь то, что перед процедурой запоминается вся память компьютера, относящаяся к интерпретатору, с помощью команды save, а затем она снова восстанавливается командой restore. Эти команды редко используются, но иногда это удобно. Затем открывается временный словарь и все параметры записываются в переменные. Отличие данной процедуры и команды colorimage от команды image в том, что перед ее вызовом записываются имена и команда load заменяет все имена на их значения в текущем словаре, и новые два аргумента указывают на rgb представление и запись 3 байта на пиксель. Здесь совсем нет команд translate и scale, но матрица преобразования записана по другому. И важно, что картинка читается сверху вниз.

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

Нужно отметить, что запись пикселей картинки в сам файл сразу за командами image и colorimage часто приводит к нежелательным артефактам. Это связано с тем, что интерпретатор читает файл не побайтно, а большими кусками, которые он помещает в буфер. Если растровая картинка стоит последней в коде, то все работает нормально. Но если после нее есть еще и другие команды языка постскрипт, то может оказаться так, что некоторые из них прочитаются вместе с пикселями картинки в буфер, и после рисования картинки будут потеряны.

Это часто приводит к ошибкам, когда eps картинка является частью документа, например, написанного на Латехе. В связи с этим нужно использовать более сложный код для описания растровой картинки. В свое время я такой код нашел в интернете и использовал его в виде команды в своей программе vkACL. Описывать весь код здесь я не буду, так как и сам в нем не разбирался. Программа vkACL просто использует код другой программы для конвертирования растровой картинки в форматах gif, jpg и png в формат eps, из которого можно вынуть код растровой картинки без лишних команд.

Оригинальная программа называется Img2eps и она до сих стоит на сайте каталога программ, откуда ее можно скачать, вот ссылка
http://sourceforge.net/projects/img2eps/
Для меня было важно, что эта программа написана на языке Java. Я использовал код версии 2004 года, сейчас там стоит версия 2013 года. Можно скачать программу и использовать ее через командную строку вот так
java -jar img2eps.jar name.png name.eps
Третьим аргументом bat файла можно указать разрешение, если не указано, то по умолчанию 90 пикселей на дюйм. Это часто неважно, потому что все легко масштабируется. Можно найти и другие графические программы, которые конвертируют растровые картинки из разных форматов в посткрипт.

 


содержание   назад   вперед