SysTools Logo SysTools


Разработка программ для текстового редактора NewsMaster ("Журналист").
Часть 3 из 3: конвертер клипарта


Эта статья была специально написана для публикации в номере N29'2019 журнала Downgrade.


Завершая серию статей, посвящённых редактору NewsMaster (см. Downgrade N27 и N28), давайте разберём формат библиотек клипарта для возможности извлечения стандартных, а также добавления внешних изображений из уже знакомого нам по прошлой статье BMP-формата.

Замечание. Здесь, в отличие от двух предыдущих статей, бинарный код готовой программы не обязан быть максимально компактным, чтобы умещаться в статический буфер или экономить память для запуска дочернего процесса, также как и нет необходимости в низкоуровневой работе с оборудованием или операционной системой. Поэтому для удобства и упрощения программы будем использовать язык C и компилятор Borland Turbo C 2.01 (1988) для DOS, который был официально отдан правообладателем в свободный доступ в 2006 году. На всякий случай код также будет совместим и с 32-битной версией свободного компилятора GCC для возможности собрать программу на современных системах.

Для начала стоит сказать, что каждое использованное изображение клипарта сохраняется редактором NewsMaster в статью целиком. Поэтому при передаче *.NWS-файлов со статьями кому-либо, или размещения их в публичном доступе, нет необходимости также прикладывать к ним и использованные библиотеки клипарта, даже если они пользовательские и в состав NewsMaster не входят. Что удобно, экономит место и позволяет обходиться одним единственным файлом статьи.

Библиотеки клипарта, как нетрудно догадаться, лежат в каталоге GRAPHICS, хотя имя и путь можно изменить через параметр GraphicsLibraryPath конфигурационного файла NEWS.CFG на что угодно, даже на точку (текущий каталог) - тогда все файлы будут в корне каталога программы (что неудобно, но такие версии NewsMaster тоже встречаются на просторах Интернета). Поэтому будем опираться на NewsMaster версии 1.0 с каталогом GRAPHICS, в противном случае нужно искать соответствующие файлы в каталоге программы или её подкаталогах.

Легко заметить, что библиотеки клипарта состоят из трёх типов файлов (в некоторых версиях часть файлов может быть утеряна, т.к. они необязательные, но об этом позже) с расширениями: *.SHP, *.SDR и *.SDX. Для удобства возьмём библиотеку NEWS, которая практически в неизменном виде есть во всех версиях программы. Состоит она из трёх файлов (в порядке убывания размера):

NEWS.SHP - 77532 байта

NEWS.SDR - 2400 байта

NEWS.SDX - 600 байт

Изображения, по всей видимости, хранятся в самом большом файле - NEWS.SHP. Попробуем просмотреть его в любом шестнадцатеричном редакторе или HEX-режиме просмотра в большинстве файловых менеджеров (начало файла, первые 64 байта в HEX):

0B 34 58 00 00 3C 00 00 00 00 00 00 00 FF 80 00
FE 00 00 00 00 00 00 0F AB 00 01 FF 00 00 00 00
00 00 35 56 00 01 FF 00 00 00 00 00 00 EA AC 00
01 7F 80 00 00 00 00 03 55 58 00 02 3F 80 00 00

Увы, пока что, содержимое никак не радует чем-то внятным и понятным. Мы вернёмся к этому файлу позже, а пока что давайте попытаем счастья с двумя другими. Взглянем на следующий файл - NEWS.SDR (нулевые байты заменены на точки, а все остальные символы из верхней половины таблицы ASCII - на знаки вопроса):

 M  a  s  k  s  .  ?  ?  ?  ?  ?  ?  ?  .  U  '
4D 61 73 6B 73 00 FF FF 0C 2D 98 06 01 00 55 27
 T  i  c  k  e  t  s  .  ?  .  ?  ?  V  ?  j  ?
54 69 63 6B 65 74 73 00 1B 00 BA 16 56 18 6A 1C
 P  e  r  f  o  r  m  e  r  .  .  .  .  .  .  .
50 65 72 66 6F 72 6D 65 72 00 00 00 00 00 00 00
 B  a  l  l  e  r  i  n  a  .  .  .  .  .  .  .
42 61 6C 6C 65 72 69 6E 61 00 00 00 00 00 00 00

640x400 NewsMaster NEWS.SDR hex view
Содержимое файла NEWS.SDR в HEX-режиме просмотра Volkov Commander

Хорошо видно, что файл состоит из строк по 16 байт с именами изображений, которые, обычно, заканчиваются на байт 000h (ASCIIZ - файл не текстовый, хоть и содержит строки), после которого могут быть "мусорные" байты, отличные от нуля. При этом нулевой байт завершает строку, только если она короче 16 символов, но ничто не запрещает ей занимать их все (тогда она не будет завершаться нулём - об этом нужно помнить). Таким образом, мы только что разобрались с предназначением *.SDR-файлов - это список имён изображений. И здесь необходимо сразу же заметить важную деталь: если загрузить библиотеку NEWS в программе, то первым изображением там будет отнюдь не Masks, а American Eagle. При внимательном изучении выводимого программой списка становится понятно, что он отсортирован в алфавитном порядке. Это означает, что порядок следования изображений в библиотеке не обязан совпадать со списком вывода в программе, так что при дальнейшем изучении формата опираться следует именно на содержимое *.SDR-файла. Ещё одна важная вещь, которую можно почерпнуть из этого формата и которая позже нам пригодится - это количество изображений:

2400 / 16 = 150 [1]

Замечание. Этого нельзя узнать, разбирая форматы файлов, а только изучая редактор в отладчике или дизассемблере, но если запись имени начинается с байта 000h (конец ASCIIZ-строки) или 01Ah (маркер конца текстового файла), то NewsMaster считает, что файл закончился и завершает чтение. Поэтому в случае, когда в конце находятся подобные записи (встречаются в некоторых версиях программы из Интернета), полагаться на размер подобного файла уже нельзя.

Но имена нам много не дадут, поэтому перейдём к рассмотрению последнего файла - NEWS.SDX (значения, для наглядности, разделены на блоки):

00 00 00 00 | 41 02 00 00 | DD 03 00 00 | 1E 06 00 00
2B 08 00 00 | 04 0A 00 00 | 3A 0C 00 00 | 13 0E 00 00
28 10 00 00 | 69 12 00 00 | AA 14 00 00 | E0 16 00 00
16 19 00 00 | 4C 1B 00 00 | 8D 1D 00 00 | CE 1F 00 00

Здесь хорошо видны последовательности по 4 байта и, судя по всему, это двойные слова, но могут оказаться и парами, тройками или даже группами двойных слов. Однако теперь мы уже вооружены знанием о том, что в библиотеке 150 изображений. Снова применим деление размера файла, но теперь мы должны будем получить размер одной записи (разумеется, при условии, что они фиксированной длинны):

600 / 150 = 4

Значит, на каждую запись действительно отводится по 4 байта. Обычно в двойных словах хранят либо смещения до начала элемента, либо его размер. Подо что-то другое этот файл сложно "подогнать" по смыслу, ибо если представить, что это не двойное слово (4 байта), а, например, пара слов (два по 2 байта) под ширину и высоту изображения, то ни то, ни другое не может быть нулём. А в силу того, что все 150 записей используются, т.е. соответствуют каждому изображению, и размер изображения тоже не может быть пустым (4 нуля - первое значение), то можно сделать предположение, что в *.SDX-файлах действительно хранится таблица смещений на начало каждого из рисунков в NEWS.SHP.

Теперь пришла пора вернуться к рассмотрению файла NEWS.SHP - давайте взглянем на первые 16 байт по указанным в NEWS.SDX смещениям:

OFFS: 00 00 00 00
DATA: 0B 34 58 00 00 3C 00 00 00 00 00 00 00 FF 80 00

OFFS: 00 00 02 41
DATA: 0B 25 52 00 00 00 00 00 00 00 03 80 00 00 00 00

OFFS: 00 00 03 DD
DATA: 0B 34 56 00 00 00 00 00 9B B7 60 00 00 00 00 00

OFFS: 00 00 06 1E
DATA: 0A 34 4E 00 00 00 00 00 00 3E 00 00 00 00 00 06

В начале изображения должны храниться его размеры (потому что, как мы выяснили выше, храниться им просто-напросто больше негде), чтобы знать, сколько читать из файла и как выводить на экран. Выкинув все значения, которые хотя бы в одном столбце данных DATA обращаются в ноль, у нас останутся первые 3 байта. Получим такие значения в десятичном представлении:

11 52 88

11 37 82

11 52 86

10 52 78

Мы помним, что большинство рисунков в программе прямоугольные, так что ширина должна быть больше высоты. Под это отлично подходят вторая (высота) и третья (ширина) величины. А вот первое значение (11 и 10) не может быть ни шириной рисунка, ни его высотой (очевидно, что слишком мало). Также оно не зависит от второго значения - т.к. может быть 11 для 52 и 37 одновременно, но при этом и 52 может быть как для 10, так и для 11. Следовательно, значение, возможно, как-то зависит от третьей величины - ширины. Попробуем поделить ширину на него:

88 / 11 = 8

82 / 11 = 7,(45)

86 / 11 = 7,(81)

78 / 10 = 7,8

Т.е. соотношение этого числа и ширины лежит в пределах от 7 до 8. Здесь нам стоит вспомнить, что все изображения чёрно-белые, следовательно, для хранения одного пикселя хватит одного бита, а, значит, в байте их поместится 8. Но ведь в память нельзя записать, скажем, 1,5 байта, когда нужно сохранить 12 пикселей (8 + 4), поэтому округление всегда идёт в большую сторону. Итого, получаем:

88 / 8 = 11

82 / 8 = 10,25 (округляем) = 11

86 / 8 = 10,75 (округляем) = 11

78 / 8 = 9,75 (округляем) = 10

Всё совпало. Таким образом, мы, предположительно, разобрались с форматом заголовка изображения:

byte rowsize - количество байт в одной строке пикселей;

byte height - высота рисунка;

byte width - ширина рисунка.

Но рисунок может быть сжат. Самый простой способ проверить это - умножить количество байт в одной строке на высоту и прибавить 3 (размер заголовка) - мы должны будем получить точное смещение следующего рисунка. Попробуем для первого:

(rowsize * height) + 3 = (11 * 52) + 3 = 575

Смещение 575 или 023Fh, но это на два байта меньше, чем необходимое 0241h. Где же мы промахнулись? Давайте, для проверки, рассмотрим ещё парочку:

(11 * 37) + 3 = 410 = 019Ah

(11 * 52) + 3 = 575 = 023Fh

Складываем со смещениями:

0241h + 019Ah = 03DBh (+2 = 03DDh)

03DDh + 023Fh = 061Ch (+2 = 061Eh)

Т.е. у нас стабильно теряется 2 байта на каждое изображение, и прибавлять нужно не 3, а 5, тогда мы будем точно попадать по смещениям, что, кстати, заодно и подтвердило нашу гипотезу о том, что изображения хранятся не сжатыми. Но где расположены и за что отвечают пропущенные 2 байта? Здесь индукция уже не поможет - нужно набирать статистику. Для этого достаточно будет взглянуть на начало пятого изображения в NEWS.SHP:

OFFS: 00 00 08 2B
DATA: 09 34 41 00 FF FF FF FF FF FF FF FF 80 84 51 15

Хорошо видно, что оно отчётливо начинается с последовательности байт 0FFh, но незатронутым остаётся четвёртый байт заголовка. Принимая во внимание, что NewsMaster работает в режиме 640x200, можно предположить, что четвёртый байт также относится к ширине рисунка, ибо 200 умещается в байт (максимальное значение байта 0FFh - 255), а 640 - нет. Эта же последовательность, кстати, подсказывает нам, где искать второй пропущенный байт - в конце изображения. Можно взять первое изображение и посмотреть на его начало и конец. В программе видно, что Masks имеет отступы (пустое место) в самой верхней и нижней строках изображения по горизонтали. Т.е. оно должно начинаться и заканчиваться на одинаковые байты. Из приведённого в начале заголовка мы видели, что оно начинается на нули, следовательно, и заканчиваться должно тоже на нули. Однако если мы перейдём на смещение второго рисунка и отступим на 16 (010h) байт назад, чтобы посмотреть на хвост первого, то увидим такую картину:

OFFS: 00 00 02 41 - 10
DATA: 07 FF 00 00 03 00 00 00 00 00 00 00 00 00 00 73

Последний байт 073h, а не ноль, и он здесь явно лишний.

Здесь же обратим внимание, что Masks имеет белый фон, а, значит, в библиотеке клипарта, как и в драйвере для печати из прошлой статьи, цвета обращены: 0 - это белый (пустое место), а 1 - чёрный (цвет напечатанной точки).


640x400 NewsMaster 1.0 Masks clipart
NewsMaster 1.0, изображение Masks из библиотеки клипарта NEWS

Таким образом, окончательный формат одного изображения в библиотеке клипарта выглядит так:

byte rowsize - количество байт в одной строке пикселей;

byte height - высота рисунка;

word width - ширина рисунка;

byte [rowsize * height] - графические данные рисунка;

byte - неизвестно.

Последний байт не используется (NewsMaster пропускает его при чтении, а утилита CAPTURE.EXE, речь о которой пойдёт ниже, оставляет там произвольное значение), поэтому и узнать, за что он отвечает, не представляется возможным. Будем, как того требует хорошая практика, заполнять его нулём и считать зарезервированным.

Разбор форматов файлов библиотек стоит дополнить упоминанием о том, что все они, кроме *.SHP, опциональны. Файлы *.SDX нужны для ускорения загрузки, чтобы программе не приходилось разбирать вручную формат *.SHP в поисках начала изображений. Файлы *.SDR нужны, чтобы ориентироваться среди изображений в алфавитном списке программы, ведь при его отсутствии имена будут отображаться как #001, #002, #003 и т.д. Поэтому и полагаться на существование каких-либо файлов, кроме *.SHP, не стоит. Здесь же можно добавить, что при отсутствии *.SDX NewsMaster начинает самостоятельно искать изображения внутри *.SHP в цикле с примерно таким условием (NewsMaster 1.0):

while (
  (read(file, head, sizeof(head)) == sizeof(head))
  && (((head.width + 7) / 8) == head.rowsize)
) { ... }

Т.е. заголовок (4 байта - первые 3 поля из структуры выше) должен быть успешно и полностью прочитан и количество байт в строке должно совпадать с рассчитанным. Поэтому первое смещение в *.SDX-файле всегда ноль. Но, теоретически, *.SHP можно создать таким образом, чтобы условие выше не срабатывало (например, добавив в начало файла специально созданную "заглушку" в 4 байта), тогда такая библиотека не будет читаться вообще, без *.SDX, указывающего на начало изображений.

Важно: количество записей в *.SDR- и *.SDX-файлах должно совпадать, в противном случае возможно аварийное завершение работы NewsMaster!

Вероятно, расширения файлов были образованы от следующих сокращений:

SHP - SHaPes (рисунки);

SDX - Shapes inDeX (индекс рисунков);

SDR - Shapes DescRiption (описание рисунков).

Теперь разберёмся с граничными условиями *.SHP-формата.

Под ширину отводится слово (2 байта), т.е. теоретически, при представлении без знака, она может принимать максимальное значение в 65535 пикселей (0FFFFh), но нас ограничивает размер строки в один байт (0FFh - 255), из чего получаем: 255 * 8 = 2040. Т.е. рисунок не может превышать 2040 пикселей в ширину. Но и 2040 пикселей много - в прошлой статье мы узнали, что ширина страницы документа фиксирована и составляет 960 пикселей, так что делать изображение больше (хотя NewsMaster и будет с ним работать) смысла тоже нет - не влезет на страницу и часть останется за краем. Отсюда видно, что размер одного рисунка не может превышать 960x255 пикселей или (960 / 8) * 255 = 30600 байт. Этот размер нам будет разумно принять за верхнюю границу.

И здесь же своевременно будет задать вопрос: а какое максимальное количество изображений NewsMaster позволяет хранить в одной библиотеке? Можно попробовать прикинуть эмпирически. Максимальный размер непрерывного блока памяти, который можно выделить стандартными средствами DOS в реальном режиме, равен 65536 - 8 = 65528 байт (0FFFFh + 1 - 8) [2].

Из чего получим:

1) Файл NEWS.SHP больше 65528, следовательно, он не загружается в память целиком и изображения подгружаются оттуда по одному. Т.е. смысла опираться на размер этого файла нет.

2) Файл NEWS.SDX содержит смещения и грузится в память весь. Т.е. максимальный размер будет 65528 / 4 = 16382 записей. Пока что возьмём эту величину за верхнюю границу.

3) Файл NEWS.SDR содержит имена и, при максимальном длине имени в 16 байт, получим 65528 / 16 = 4095 записей (остаток отброшен) - верхняя граница уменьшилась.

Но при попытке создать тестовую библиотеку с таким количеством записей будет выводиться сообщение о нехватке памяти. Это связано с тем, что для работы с описаниями изображений в NewsMaster версии 1.0 отведён статический буфер всего в 5680 байт [3]. При этом 82 байта оттуда используются под нужды самой программы, итого: 5680 - 82 = 5598 байт. Но и это ещё не всё - каждое описание рисунка в буфере размещается динамически в виде структуры, состоящей из 6 байт служебных данных, строки описания изображения, завершающего нуля ASCIIZ и ещё одного байта, если размер всей структуры оказался нечётным (выравнивание).

Т.е. размер структуры у имени в 16 символов будет: 6 + 16 + 1 + 1 = 24 байта.

А размер, скажем, для 7 символов окажется: 6 + 7 + 1 + 0 = 14 байт.

Формула для пересчёта длинны строки в размер структуры:

size = len + 8 - (len & 1)

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

5598 / (6 + 1 + 1 + 0) = 699 (остаток отброшен)

Таким образом, в одной библиотеке не может быть более 699 изображений.

Точно также можно подсчитать и нижнюю границу, когда каждое описание занимает целиком все 16 символов:

5598 / (6 + 16 + 1 + 1) = 233 (остаток отброшен)

Т.е. в три раза меньше, чем верхняя граница.

Дабы не смущать будущих пользователей программы сложными объяснениями, почему в одном случае они могут сохранить практически 700 изображений, а в другом - в три раза меньше, возьмём нижнюю границу в качестве предела - это сократит и упростит код конвертера, т.к. нам не нужно будет суммировать размер всех строк в переводе на структуры. Заодно опустим границу до 200 ровно - как показала практика, чисто психологически, к круглым числам вопросов возникает меньше. Если же для кого-то и 200 изображений окажется мало [4], то всегда можно будет дополнительно создать ещё новых библиотек.

Из установленного нами предела также становится видно что:

200 * 16 = 3200

200 * 4 = 800

Иными словами, файл *.SDR не может превышать 3200, а *.SDX 800 байт в размере.

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

Отдельно стоит сказать про формат, создаваемый утилитой CAPTURE.EXE, поставляющейся вместе с NewsMaster версии II [5], в котором поддержка этого формата и была добавлена. Упомянутая утилита позволяет делать чёрно-белые снимки экрана, сохраняя их в библиотеку с именем CAPTURE.SHP и сопутствующими файлами CAPTURE.SDX и CAPTURE.SDR. Последние два файла ничем не отличаются от уже разобранных выше, а вот формат CAPTURE.SHP совершенно иной. Его полное описание будет дано ниже, потому что, во-первых, он куда более комплексный, а, во-вторых, дополнительную сложность вносит то, что утилита CAPTURE.EXE не заполняет некоторые поля структуры заголовка, оставляя их нулями, но вот NewsMaster использует их при вычислении параметров изображения.

Каждое изображение в CAPTURE.SHP имеет следующий формат:

dword zero - всегда ноль, сигнатура нового формата (подробности ниже);

word coffx - коэффициент масштабирования по ширине;

word coffy - коэффициент масштабирования по высоте;

word left - левая координата рисунка;

word top - верхняя координата рисунка;

word right - правая координата рисунка;

word bottom - нижняя координата рисунка;

word unused - не используется (всегда ноль);

dword size - размер всего рисунка в байтах;

byte [size] - графические данные рисунка.

Все типы слов (word) в данной структуре обрабатываются со знаком.

В новом формате zero (первое двойное слово) всегда 0, т.е. каждый рисунок начинается с четырёх нулевых байт. Это оригинальное, но простое решение позволяет легко отличить старый формат от нового, потому как в старом заголовок целиком состоит из 4-х байт, но ни одно из значений там (ширина, высота, количество байт в строке) не может быть нулём.

Параметры рисунка, записанного в файле, считаются следующим образом:

Ширина:

image_width = right - left;

Высота:

image_height = bottom - top;

Обратите внимание, что, например, в отличие от PCX и ряда других форматов, содержащих координаты изображения, здесь к разности не добавляется единица. После этого к полученным размерам изображения в файле применяются коэффициенты coffx и coffy для масштабирования перед выводом:

scaling_width = (image_width * 360) / coffx;

scaling_height = (image_height * 360) / coffy;

Важные моменты:

1. CAPTURE.EXE всегда заполняет top и left значениями 0, так что в right и bottom, по сути, всегда находятся ширина и высота рисунка.

2. coffx и coffy по умолчанию равны 120 и 72 (078h и 048h соответственно), но их можно изменить на произвольные через ключи командной строки, например вот так, чтобы рисунок не масштабировался, а выводился как есть:

CAPTURE.EXE /X360 /Y360

3. scaling_width и scaling_height вычисляются со знаком, так что если результат деления (целая часть) не умещается в слово со знаком, то NewsMaster будет аварийно завершать работу при загрузке такого изображения с ошибкой деления на ноль (стандартная ошибка при переполнении) - иными словами, результат деления не должен превышать 32767 (07FFFh).

Отсюда видно, что:

32767 >= (image * 360) / coff

coff * 32767 >= image * 360

(coff * 32767) / 360 >= image

coff * (32767 / 360) >= image

coff * 91.019(4) >= image

coff >= image / 91.019(4)

Т.е. ширина или высота, делённая на 91 (дробная часть отброшена), не должны превышать соответствующего коэффициента:

coffx >= image_width / 91

coffy >= image_height / 91

И последнее важное отличие формата - теперь вместо размера одной строки хранится размер всего рисунка.

Для обратной совместимости со старыми версиями NewsMaster, наш конвертер будет поддерживать этот формат только для экспорта в BMP, но не обратно.

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

Утилита CAPTURE.EXE, сохраняющая в этот формат, работает корректно только в графических видеорежимах BIOS из диапазона 00h - 11h (не работает корректно в 13h и почему-то не поддерживает работу в режиме 12h), а т.к. максимальное разрешение при этом 640x480 у режима 11h, то получим:

(640 / 8) * 480 = 38820

38820 больше, чем 30600, так что и верхняя граница буфера под размер рисунка у нас поднялась.


640x400 NewsMaster screen capture
Сохранение рисунка при помощи утилиты CAPTURE.EXE

И несколько слов о том, как происходит сохранение рисунка в CAPTURE.EXE. После запуска этой утилиты необходимо в графическом режиме нажать Alt+G, стрелками курсора указать начало изображения, нажать Enter, указать противоположный угол и снова Enter. После этого нужно ответить на два вопроса и ввести описание рисунка для файла библиотеки CAPTURE.SDR. Вопросы же на испанском языке и звучат так:

Первый вопрос:

Es el fondo de la imagen color <<N E G R O>>? SI / NO

(Цвет фона изображения чёрный? Да / Нет)

Второй вопрос:

Como quiere grabar la Imagen?

(Как необходимо сохранить изображение?)

Первый ответ:

Alterado pero listo para la PRINTER.

(Изменить для печати на принтере)

Второй ответ:

Tal cual aparece en el monitor ....

(Оставить, как выводится на экране)

В первом вопросе нужно ответить отрицательно (нижний вариант - NO), в противном случае в сохраняемом изображении программа обратит цвета. Во втором вопросе также нужен нижний ответ (как на экране), если необходимо сохранить изображение как есть (не забываем про /X360 /Y360, чтобы при открытии в NewsMaster оно также не масштабировалось). Изменения для печати (первый вариант ответа) заключаются в том, что высоту рисунка (ширина всегда остаётся неизменной) специальными коэффициентами пытаются вытянуть или сжать. Массив коэффициентов статически прописан в CAPTURE.EXE и зависит только от текущего видеорежима. Так, например, для разрешений 320x200 и 640x200 коэффициенты будут одинаковыми - 13 и 10, для 640x350 - 74 и 100, а для 640x480 - 54 и 100:

(200 * 13) / 10 = 260

(350 * 74) / 100 = 259

(480 * 54) / 100 = 259 (остаток отброшен)

Т.е. максимальная высота искусственно подгоняется под значение 260. Пусть здесь не смущает 259, потому что для работы с дробными числами в 1988 году был необходим либо сопроцессор, либо специальная библиотека, его эмулирующая. Поэтому решение было сделано приближённо через умножение и деление целыми числами. При этом по указанным коэффициентам пересчитывается любая высота рисунка в режиме для печати. Так, например, рисунок 45x50 превратится в 45x37 в режиме 640x350 ((50 * 74) / 100 = 37).

Но важно здесь другое - хотя коэффициенты и увеличивают 640x200 до 640x260, но не делают того же с 640x480, превращая его в 640x259, что оставляет рассчитанные нами ранее величины граничных условий формата в силе (их не придётся увеличивать ещё больше).


640x400 CAPTURE.EXE video modes list
Список разрешений из CAPTURE.EXE v1.1; заблокированные из них (метка #) содержат ошибки

Разобравшись со всеми форматами и граничными условиями, мы, наконец-то, можем приступить к написанию конвертера клипарта.

Чтобы не сильно усложнять код, но показать основные приёмы работы с библиотеками, наша программа будет состоять из минимального набора функций:

- вывод списка всех изображений с порядковым номером и именем;

- извлечение любого изображения в BMP-файл по порядковому номеру;

- добавление в конец библиотеки изображения из BMP-файла.

Поэтому такие операции, как замена (обновление) какого-либо изображения, лучше делать через создание резервной копии библиотеки, к которой можно будет откатиться, при необходимости изменения последнего рисунка. А удаление придётся делать через полный разбор библиотеки на отдельные BMP-файлы и сборку только нужных изображений назад. Это не сильно удобно для интенсивной работы, поэтому лучше создавать с нуля новую библиотеку и добавлять туда все необходимые BMP-файлы через специальный BAT-файл автоматически. Или, при желании, добавить необходимый функционал в код программы.

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

а) Нет параметров - на экран выводится справка по использованию.

б) Один параметр - имя библиотеки, список изображений которой будет выведен на экран.

в) Два параметра - имя библиотеки и номер изображения оттуда (с единицы), которое будет извлечено в BMP-файл, с автоматически сформированным именем SHAPE???.BMP, где ??? - указанный номер (добивается нулями слева).

г) Наконец, три и более параметра - имя библиотеки, куда необходимо добавить новое изображение и имя входного BMP-файла с изображением, а все остальные параметры, начиная с третьего, это текст слов, которые будут объедены через пробел в строку описания для изображения [6]. При этом только в этом режиме отсутствие исходной библиотеки не будет считаться ошибкой, а намерением создать новую с указанным именем.

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

1) Уже упомянутые статические буферы максимального размера подо все структуры - никакого динамического выделения или освобождения памяти.

2) Для описания структур будут использованы стандартные типы данных uint8_t, int32_t и тому подобные, но, в силу того что заголовочного файла stdint.h ещё не существовало в 1988 году, то подобные вещи будут определены прямо в коде программы, как и типы BITMAPFILEHEADER и BITMAPINFOHEADER, взятые из windows.h.

3) Все важные места будут содержать проверки на ошибки, но только в самом минимальном их количестве, чтобы сильно не загромождать код.

4) Чтобы не разбираться, корректный ли файл *.SDX, при чтении он будет строиться через полный разбор *.SHP-файла, а при изменении - целиком перезаписан с уже построенных данных. Разбор *.SHP-файла будет завершаться при невозможности определить формат изображения, чтобы отсечь мусор в хвосте файлов библиотек (у некоторых версий NewsMaster из Интернета).

5) В файле *.SDR "мусорные" байты после 000h будут "почищены" - заполнены нулями. Также при меньшем количестве записей, чем изображений в *.SHP, недостающие будут заполнены строками "#001", "#002" и т.д. как они выводились бы в списке самого NewsMaster.

6) Экспорт изображений из библиотеки будет производиться сразу в "перевёрнутый" BMP-формат (положительная высота), т.к., в отличие от драйвера для принтера, нам изначально известна конечная высота рисунка и он полностью прочитан в память, что снимает ограничения и позволяет нам работать с изображением как угодно.

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

/*
  NewsMaster / PrintMaster shapes library tool
  (c) SysTools 2019
  http://systools.losthost.org/

  Both shapes format supported: original (extract, append) and
  from CAPTURE.EXE tool (extract only).

  All [u]int<8/16/32>_t types declaration taken out from
  <stdint.h> header file which not existed yet back in 1988
  and also BITMAPFILEHEADER and BITMAPINFOHEADER declarations
  which was taken out from the <windows.h> header file.

  Note that 32bit types may need tweaking for 64bit compilers
  since "long" are the only 32bit type on 16bit compiler.

  This tool utilizes only static buffers for all the work -
  no memory allocated or freed in the code below.

  Borland Turbo C Compiler 2.01 (DOS16)
  TCC -w -g1 -O -Z shapelib.c

  GNU C Compiler 3.2 (Win32)
  GCC -Wall -pedantic -Werror -Os -s -o shapelib shapelib.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <io.h>

/* required types, DOS16 / Win32 compatible */
typedef   signed char   int8_t;
typedef unsigned char  uint8_t;
typedef   signed short  int16_t;
typedef unsigned short uint16_t;
/* note: x32 bit types may need tweaking for x64 compilation */
typedef   signed long   int32_t;
typedef unsigned long  uint32_t;

#define BMP_TYPE_BM 0x4D42
#define SDR_NAME_SZ 16

#pragma pack(push, 1)
typedef struct {
  uint16_t bfType;
  uint32_t bfSize;
  uint16_t bfReserved1;
  uint16_t bfReserved2;
  uint32_t bfOffBits;
} BITMAPFILEHEADER;

typedef struct {
  uint32_t biSize;
  int32_t  biWidth;
  int32_t  biHeight;
  uint16_t biPlanes;
  uint16_t biBitCount;
  uint32_t biCompression;
  uint32_t biSizeImage;
  int32_t  biXPelsPerMeter;
  int32_t  biYPelsPerMeter;
  uint32_t biClrUsed;
  uint32_t biClrImportant;
} BITMAPINFOHEADER;

typedef struct {
  BITMAPFILEHEADER hdr_file;
  BITMAPINFOHEADER hdr_info;
  uint32_t         hdr_cpal[256];
} bmp_head;

typedef struct {
  uint32_t zero;
  int16_t  coffx;
  int16_t  coffy;
  int16_t  left;
  int16_t  top;
  int16_t  right;
  int16_t  bottom;
  int16_t  unused;
  uint32_t size;
} cpt_head;

typedef struct {
  uint8_t  rowsize;
  uint8_t  height;
  uint16_t width;
} shp_head;

typedef struct {
  char name[SDR_NAME_SZ];
} sdr_item;

typedef struct {
  uint32_t offs;
} sdx_item;
#pragma pack(pop)

/* set fixed limit for max images per one library */
#define MAX_ITEM 200
/* set shp max supported width and height */
#define SHP_MAX_WIDTH  960U
#define SHP_MAX_HEIGHT 255U
/* set cpt max supported width and height */
#define CPT_MAX_WIDTH  640U
#define CPT_MAX_HEIGHT 480U
/* max image data size in bytes 960x255 (30600 bytes) */
#define MAX_DATA_SHP (((SHP_MAX_WIDTH + 7) / 8) * SHP_MAX_HEIGHT)
/* cpt format can hold larger images and max resolution where
   CAPTURE.EXE still can be used are 640x480 (38400 bytes) */
#define MAX_DATA_CPT (((CPT_MAX_WIDTH + 7) / 8) * CPT_MAX_HEIGHT)
/* max of two buffers */
#define MAX_DATA MAX_DATA_CPT
/* file types index in 4-char string with extensions */
#define FILE_TYPE_SHP 0
#define FILE_TYPE_SDR 1
#define FILE_TYPE_SDX 2
/* library file extensions */
const static char ext_list[] = ".SHP\0.SDR\0.SDX";
/* buffer for max offs data */
static sdx_item sdx_list[MAX_ITEM];
/* buffer for max names data */
static sdr_item sdr_list[MAX_ITEM];
/* buffer for max image data */
static uint8_t data[MAX_DATA];
/* bitmap file headers */
static bmp_head bmp_data;
/* global library filename for open (DOS 8.3 ASCIIZ) */
static char lib_name[13];
/* max items in shape library */
static int count;

/* calculate BMP image size */
static int32_t CalcBMPSize(int32_t w, int32_t h, uint16_t b) {
  w = ((((w * b) + 31) & ~31) >> 3);
  w *= (h < 0) ? (-h) : h;
  return(w);
}

/* universal bitmap header initialization routine */
static uint32_t InitBMPHeaders(bmp_head *bh, int32_t w, int32_t h, uint16_t b) {
uint32_t l;
  /* header size */
  l =
    /* file hader */
    sizeof(bh->hdr_file) +
    /* bitmap header */
    sizeof(bh->hdr_info) +
    /* palette size for index color images */
    ((b <= 8) ? (sizeof(bh->hdr_cpal[0]) << b) : 0);
  if (bh) {
    memset(bh, 0, sizeof(bh[0]));
    bh->hdr_info.biSize = sizeof(bh->hdr_info);
    bh->hdr_info.biWidth = w;
    bh->hdr_info.biHeight = h;
    bh->hdr_info.biPlanes = 1;
    bh->hdr_info.biBitCount = b;
    bh->hdr_info.biSizeImage = CalcBMPSize(w, h, b);
    bh->hdr_file.bfType = BMP_TYPE_BM;
    bh->hdr_file.bfOffBits = l;
    bh->hdr_file.bfSize = l + bh->hdr_info.biSizeImage;
  }
  return(l);
}

/* merge strings list to the single string with the spaces between */
static void StrListMerge(char *buff, int bmax, char *list[], int lmax) {
int i, n;
char *s;
  memset(buff, 0, bmax * sizeof(buff[0]));
  n = 0;
  for (i = 0; i < lmax; i++) {
    for (s = list[i]; *s; s++) {
      if (n == bmax) { break; }
      buff[n] = *s;
      n++;
    }
    if (n == bmax) { break; }
    buff[n] = ' ';
    n++;
  }
  /* add null at string end */
  if (n) {
    n--;
    buff[n] = 0;
  }
  /* trim space at the end if there is not enough memory for next word */
  if (n && (buff[n - 1] == ' ')) {
    n--;
    buff[n] = 0;
  }
}

/* prepare all global static buffers before using */
static void InitData(void) {
  count = 0;
  memset(data, 0, MAX_DATA);
  memset(&bmp_data, 0, sizeof(bmp_data));
  memset(sdx_list, 0, MAX_ITEM * sizeof(sdx_list[0]));
  memset(sdr_list, 0, MAX_ITEM * sizeof(sdr_list[0]));
}

/* add extension to the base name of the file */
static char *BuildName(char *library, int type) {
int i;
  /* in case of extension - cut it (DOS 8.3 filename) */
  for (i = 0; i < 8; i++) {
    /* end of the string or dot from extension */
    if ((!library[i]) || (library[i] == '.')) { break; }
  }
  /* copy name */
  memcpy(lib_name, library, i * sizeof(library[0]));
  /* add required extension */
  memcpy(&lib_name[i], &ext_list[type * 5], 5 * sizeof(library[0]));
  /* return pointer for easy use in open() since it's global anyway */
  return(lib_name);
}

/* file size helper routine */
static uint32_t FileSize(int fl) {
uint32_t p, l;
  p = tell(fl);
  lseek(fl, 0, SEEK_END);
  l = tell(fl);
  lseek(fl, p, SEEK_SET);
  return(l);
}

/* checks if current position in file points to the start of the shp item */
static int IsItemSHP(int fl, uint32_t sz, shp_head *shp) {
uint32_t l;
  /* save current position if invalid format */
  l = tell(fl);
  /* not enough space in file for header */
  if (read(fl, shp, sizeof(shp[0])) != sizeof(shp[0])) { shp = NULL; }
  /* width or height are zero or width more than max or invalid rowsize */
  if (shp && ((!shp->width) || (!shp->height) ||
    (shp->width > SHP_MAX_WIDTH) ||
    (((shp->width + 7) / 8) != shp->rowsize)
  )) { shp = NULL; }
  /* check that file has enough space to hold the whole image */
  if (shp && sz && ((l + sizeof(shp[0]) + (shp->rowsize * shp->height) + 1) > sz)) {
    shp = NULL;
  }
  /* invalid format - rewind back file postion */
  if (!shp) { lseek(fl, l, SEEK_SET); }
  return(shp ? 1 : 0);
}

/* checks if current position in file points to the start of the cpt item */
static int IsItemCPT(int fl, uint32_t sz, cpt_head *cpt) {
uint32_t l;
  /* save current position if invalid format */
  l = tell(fl);
  /* not enough space in file for header */
  if (read(fl, cpt, sizeof(cpt[0])) != sizeof(cpt[0])) { cpt = NULL; }
  /* must starts with 4 zero bytes to tell apart from the shp header */
  if (cpt && cpt->zero) { cpt = NULL; }
  /* calc width and height, note that there is no +1 here
     like in some other formats (i.e. width = x2 - x1 + 1) */
  if (cpt) {
    cpt->right -= cpt->left; /* width */
    cpt->bottom -= cpt->top; /* height */
    /* just in case */
    cpt->left = 0;
    cpt->top = 0;
  }
  if (cpt && (
    /* width and height can't be negative */
    (cpt->right <= 0) || (cpt->bottom <= 0) ||
    /* or bigger than max */
    (cpt->right > CPT_MAX_WIDTH) || (cpt->bottom > CPT_MAX_HEIGHT) ||
    /* coeffs can't be zero or less */
    (cpt->coffx <= 0) || (cpt->coffy <= 0) ||
    /*
      explanation for this weird check:
      NewsMaster II recalculate image screen dimensions by the next formula:
        width  = ((right - left) * 360) / coffx
        height = ((bottom -  up) * 360) / coffy
      if after division result will be too big to fit in word with sign (07FFFh)
      the program will crash with the fatal error "divide by zero"
      07FFFh = 32767, so let's see what's need to be checked:
        32767 >= (dimension * 360) / coff
        coff * 32767 >= dimension * 360
        (coff * 32767) / 360 >= dimension
        coff * 91 >= dimension
        coff >= dimension / 91
    */
    (cpt->coffx < (cpt->right / 91)) || (cpt->coffy < (cpt->bottom / 91)) ||
    /* check the correct whole image size */
    ((((cpt->right + 7) / 8) * cpt->bottom) != cpt->size)
  )) { cpt = NULL; }
  /* check that file has enough space to hold the whole image */
  if (cpt && sz && ((l + sizeof(cpt[0]) + cpt->size) > sz)) { cpt = NULL; }
  /* invalid format - rewind back file postion */
  if (!cpt) { lseek(fl, l, SEEK_SET); }
  return(cpt ? 1 : 0);
}

static int IsFileBMP(int fl, uint32_t sz, bmp_head *bmp) {
int32_t h;
uint32_t l;
  /* bitmap header size */
  l = InitBMPHeaders(NULL, 0, 0, 1);
  /* not enough space in file for header */
  if (read(fl, bmp, (int) l) != l) { bmp = NULL; }
  /* negative height */
  h = bmp->hdr_info.biHeight;
  h = (h < 0) ? (-h) : h;
  /* check BMP format */
  if (bmp && (
    /* check signature */
    (bmp->hdr_file.bfType != BMP_TYPE_BM) ||
    /* disk file can't be less than size in header (but fine if bigger) */
    (bmp->hdr_file.bfSize > sz) ||
    /* check v3 header */
    (bmp->hdr_info.biSize != sizeof(bmp->hdr_info)) ||
    /* check width and height */
    (bmp->hdr_info.biWidth <= 0) || (!h) ||
    /* and not exceed max allowed image resolution in shp format */
    (bmp->hdr_info.biWidth > SHP_MAX_WIDTH) || (h > SHP_MAX_HEIGHT) ||
    /* planes must be 1 */
    (bmp->hdr_info.biPlanes != 1) ||
    /* BPP must be 1 */
    (bmp->hdr_info.biBitCount != 1) ||
    /* compression unsupported */
    (bmp->hdr_info.biCompression)
  )) { bmp = NULL; }
  /* now the tricky part: since biSizeImage can be 0
     for non-compressed images - recalc it */
  if (bmp) {
    bmp->hdr_info.biSizeImage = CalcBMPSize(
      bmp->hdr_info.biWidth,
      bmp->hdr_info.biHeight,
      bmp->hdr_info.biPlanes
    );
  }
  /* last check for correct offset to the image */
  if (bmp && (
    /* in theory image can start inside header but it's weird */
    (bmp->hdr_file.bfOffBits < l) ||
    /* there should be enough space at image offset to hold image data */
    ((bmp->hdr_file.bfOffBits + bmp->hdr_info.biSizeImage) > sz)
  )) { bmp = NULL; }
  /* no need to rewind file ponter on error since testing standalone BMP file */
  return(bmp ? 1 : 0);
}

/* load data from library file */
static void LoadData(char *library) {
shp_head shp;
cpt_head cpt;
uint32_t sz;
int fl, i, j;
char c;
  /* open shapes library */
  fl = open(BuildName(library, FILE_TYPE_SHP), O_RDONLY | O_BINARY);
  if (fl != -1) {
    /* get file size */
    sz = FileSize(fl);
    while ((tell(fl) < sz) && (count < MAX_ITEM)) {
      sdx_list[count].offs = tell(fl);
      /* read as SHP format first (smaller header) */
      if (IsItemSHP(fl, sz, &shp)) {
        /* skip image data */
        lseek(fl, tell(fl) + (shp.rowsize * shp.height) + 1, SEEK_SET);
      } else {
        /* read as CPT format next (bigger header) */
        if (IsItemCPT(fl, sz, &cpt)) {
          /* skip image data */
          lseek(fl, tell(fl) + cpt.size, SEEK_SET);
        } else {
          /* something wrong - break */
          break;
        }
      }
      count++;
    }
    /* for append */
    if (count < MAX_ITEM) {
      sdx_list[count].offs = tell(fl);
    }
    close(fl);
  }
  /* at least one image exists */
  if (count) {
    /* open descriptions file */
    fl = open(BuildName(library, FILE_TYPE_SDR), O_RDONLY | O_BINARY);
    if (fl != -1) {
      /* calc approximate amount items in file */
      sz = FileSize(fl) / sizeof(sdr_list[0]);
      /* can't be more than total items */
      sz = (sz > count) ? count : sz;
      /* read items descriptions */
      read(fl, sdr_list, ((int) sz) * sizeof(sdr_list[0]));
      close(fl);
    }
    /* fix item names */
    c = 0;
    for (i = 0; i < count; i++) {
      /* end reached if 0x00 or 0x1A as in NewsMaster loader code */
      if ((sdr_list[i].name[0] == 0x00) || (sdr_list[i].name[0] == 0x1A)) { c = 1; }
      /* clear text items from junk bytes after zero tail char */
      if (!c) {
        c = 1;
        for (j = 0; j < SDR_NAME_SZ; j++) {
          sdr_list[i].name[j] *= c;
          c &= sdr_list[i].name[j] ? 1 : 0;
        }
        c = 0;
      } else {
        /* clear tail junk chars if any here from the sdr file */
        memset(sdr_list[i].name, 0, SDR_NAME_SZ * sizeof(sdr_list[0].name[0]));
        /* fill with same default names as in NewsMaster: #001, #002, ... */
        sprintf(sdr_list[i].name, "#%03d", i + 1);
      }
    }
  }
}

/* output item names with thier order number inside library */
static void ListData(void) {
char s[SDR_NAME_SZ + 1];
int i;
  s[SDR_NAME_SZ] = 0;
  for (i = 0; i < count; i++) {
    memcpy(s, sdr_list[i].name, SDR_NAME_SZ * sizeof(s[0]));
    printf("%03u-%s\n", i + 1, s);
  }
}

/* extract shape by number */
static void ExtractShape(char *library, int n) {
char name[SDR_NAME_SZ + 1];
shp_head shp;
cpt_head cpt;
int fl, i;
  /* open shapes library */
  fl = open(BuildName(library, FILE_TYPE_SHP), O_RDONLY | O_BINARY);
  if (fl != -1) {
    lseek(fl, sdx_list[n - 1].offs, SEEK_SET);
    /* detect image format */
    if (!IsItemCPT(fl, 0, &cpt)) {
      if (IsItemSHP(fl, 0, &shp)) {
        /* unification for later usage */
        memset(&cpt, 0, sizeof(cpt));
        cpt.right = shp.width;
        cpt.bottom = shp.height;
        cpt.size = shp.rowsize * shp.height;
      } else {
        /* error */
        cpt.size = 0;
      }
    }
    if (cpt.size <= MAX_DATA) {
      /* read data */
      read(fl, data, (int) cpt.size);
    } else {
      /* error */
      cpt.size = 0;
    }
    close(fl);
    /* no error */
    if (cpt.size) {
      /* output shape name */
      memcpy(name, sdr_list[n - 1].name, SDR_NAME_SZ * sizeof(name[0]));
      name[SDR_NAME_SZ] = 0;
      printf("%03u|%s -> ", n, name);
      /* generate output file name */
      sprintf(name, "SHAPE%03u.BMP", n);
      printf("%s\n", name);
      /* create output file */
      fl = open(name, O_RDWR | O_BINARY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE);
      if (fl != -1) {
        /* invert image colors for BMP format */
        for (i = 0; i < cpt.size; i++) {
          data[i] = ~data[i];
        }
        /* init bitmap headers */
        InitBMPHeaders(&bmp_data, cpt.right, cpt.bottom, 1);
        /* add white palette color */
        bmp_data.hdr_cpal[1] = 0xFFFFFFUL;
        /* actual headers size (include palette) will be in bfOffBits */
        write(fl, &bmp_data, (int) bmp_data.hdr_file.bfOffBits);
        /* row size */
        cpt.unused = cpt.size / cpt.bottom;
        /* for padding */
        cpt.zero = 0;
        /* padding size */
        n = (4 - (cpt.unused % 4)) % 4;
        /* write image data by row (bottom-top image) */
        while (cpt.bottom--) {
          /* write image row */
          write(fl, &data[cpt.bottom * cpt.unused], cpt.unused);
          /* write bmp padding bytes */
          write(fl, &cpt.zero, n);
        }
        close(fl);
        printf("\ndone\n\n");
      } else {
        printf("Error: can't create output file.\n\n");
      }
    } else {
      printf("Error: invalid shape or too big to fit in internal buffer.\n\n");
    }
  }
}

/* append new shape to the end of the library */
static void AppendShape(char *library, char *image, char *name) {
uint16_t lb;
shp_head shp;
int fl, i, h;
  /* copy item name (note that offset in sdx_list[] was init above) */
  memcpy(sdr_list[count].name, name, SDR_NAME_SZ * sizeof(name[0]));
  printf("%03u|%s <- %s\n", count + 1, name, image);
  fl = open(image, O_RDONLY | O_BINARY);
  if (fl != -1) {
    /* check for correct BMP file */
    i = IsFileBMP(fl, FileSize(fl), &bmp_data);
    if (i) {
      /* row length (in bytes) for BMP */
      lb = CalcBMPSize(bmp_data.hdr_info.biWidth, 1, bmp_data.hdr_info.biBitCount);
      /* fill in shape header */
      shp.rowsize = (bmp_data.hdr_info.biWidth + 7) / 8;
      shp.width = bmp_data.hdr_info.biWidth;
      /* negative height */
      h = (int) bmp_data.hdr_info.biHeight;
      shp.height = (h < 0) ? (-h) : h;
      /* seek to the bitmap data start */
      lseek(fl, bmp_data.hdr_file.bfOffBits, SEEK_SET);
      /* read and convert BMP file to SHP */
      while (h) {
        /* instead of disk seeking for bottom-top images
           move pointer to correct address in memory */
        read(fl, &data[((h < 0) ? (shp.height + h) : (h - 1)) * shp.rowsize], shp.rowsize);
        /* skip BMP padding bytes */
        lseek(fl, lb - shp.rowsize, SEEK_CUR);
        /* negative (top-bottom) or positive (bottom-top) BMP file */
        h += (h < 0) ? 1 : (-1);
      }
      /* invert colors if needed */
      if (!bmp_data.hdr_cpal[0]) {
        for (i = 0; i < (shp.rowsize * shp.height); i++) {
          data[i] = ~data[i];
        }
      }
      /* no errors */
      i = 1;
    }
    /* now fl handle can be reused (only one opened file at a time) */
    close(fl);
    /* no errors? */
    if (i) {
      /* open file or create if new library */
      fl = count ?
           /* at least one image exists - open existing library */
           open(BuildName(library, FILE_TYPE_SHP), O_RDWR | O_BINARY) :
           /* no images in library - create new library file */
           open(BuildName(library, FILE_TYPE_SHP),
             O_RDWR | O_BINARY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE);
      if (fl != -1) {
        /* seek to the place after last image */
        lseek(fl, sdx_list[count].offs, SEEK_SET);
        /* write shape header */
        write(fl, &shp, sizeof(shp));
        /* write image data */
        write(fl, data, shp.rowsize * shp.height);
        /* write unknown byte */
        i = 0;
        write(fl, &i, 1);
        close(fl);
        /* always overwrite updated description and offset files */
        count++;
        /* description file */
        fl = open(BuildName(library, FILE_TYPE_SDR),
          O_RDWR | O_BINARY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE);
        if (fl) {
          write(fl, sdr_list, sizeof(sdr_list[0]) * count);
          close(fl);
        }
        /* offsets file */
        fl = open(BuildName(library, FILE_TYPE_SDX),
          O_RDWR | O_BINARY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE);
        if (fl) {
          write(fl, sdx_list, sizeof(sdx_list[0]) * count);
          close(fl);
        }
        printf("\ndone\n\n");
      } else {
        printf("Error: can't create or open for write shape library file.\n\n");
      }
    } else {
      printf(
        "Error: not a BMP file, invalid format or larger than %ux%u pixels.\n\n",
        SHP_MAX_WIDTH, SHP_MAX_HEIGHT
      );
    }
  } else {
    printf("Error: can't open input BMP file for read.\n\n");
  }
}

int main(int argc, char *argv[]) {
char s[SDR_NAME_SZ + 1];
int n;
  printf("NewsMaster / PrintMaster shapes library tool\n\n");
  if (argc < 2) {
    printf(
      "Usage: shapelib <library> [...]\n\n"
      "List images in library TEST.SHP (1 argument):\n"
      "  shapelib test\n\n"
      "Extract 4th image from library TEST.SHP (2 arguments):\n"
      "  shapelib test 4\n\n"
      "Append image to the library TEST.SHP (3 or more arguments):\n"
      "  shapelib test filename.bmp Item Description\n\n"
    );
    return(1);
  }
  /* init global structs */
  InitData();
  /* read library data */
  LoadData(argv[1]);
  /* decide what to do */
  switch (argc - 2) {
    case 0:
      if (count) {
        ListData();
      } else {
        printf("Error: can't open or invalid library file.\n\n");
      }
      break;
    case 1:
      if (count) {
        n = atoi(argv[2]);
        printf("Extract shape number \"%s\" (%d)...\n", argv[2], n);
        if ((n >= 1) && (n <= count)) {
          ExtractShape(argv[1], n);
        } else {
          printf("Error: invalid shape number, must be in range: 1 <= n <= %u.\n\n", count);
        }
      } else {
        printf("Error: can't open or invalid library file.\n\n");
      }
      break;
    /* >= 2 */
    default:
      if (count < MAX_ITEM) {
        /* merge item name from command line arguments */
        StrListMerge(s, SDR_NAME_SZ + 1, &argv[3], argc - 3);
        /* append image to the end of the current library */
        AppendShape(argv[1], argv[2], s);
      } else {
        printf("Error: too much shapes (>=%u) in this library, create a new one.\n\n", MAX_ITEM);
      }
      break;
  }
  return(0);
}

Остался последний важный, но упомянутый пока что только поверхностно момент - это соотношение сторон изображения. Чтобы понять, о чём идёт речь, давайте добавим какой-нибудь квадратный рисунок в новую библиотеку и посмотрим, как он будет отображаться в NewsMaster при использовании в документе. Для примера возьмём логотип Downgrade из шапки с сайта журнала. Его нужно будет перевести из 32 бит в 2 бита ч/б, а также уменьшить в силу ограничения по высоте в 255 пикселей.


254x254 Downgrade Logo
Логотип журнала в 254x254 ч/б

Для удобства сборки библиотеки клипарта сделаем BUILDLIB.BAT-файл такого содержания:

@echo off
del shapelib.shp
shapelib.exe shapelib.shp dglogobw.bmp DGMagLogo

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

После создания библиотеки, открываем её в NewsMaster и вставляем рисунок в документ. Нетрудно заметить, что логотип из квадрата превратился в прямоугольник, вытянутый по вертикали. Происходит это из-за того, что в силу особенности печати на матричных принтерах изображение увеличивается по высоте. Чтобы компенсировать это, весь клипарт в NewsMaster изначально сделан "сплюснутым" [7]. Помните, мы выше разбирались с таблицей коэффициентов из утилиты CAPTURE.EXE? Квадратным пиксель, обычно, выглядит на разрешении с соотношением сторон 4 к 3, какое было на большинстве мониторов того времени [8]. Из списка CAPTURE.EXE там только одно такое разрешение - 640x480 с коэффициентами 54 и 100 или 0,54 (54 / 100). Пересчитываем:

254 * 0,54 = 137 (остаток отброшен)

Т.е. наше изображение будет квадратным, если мы уменьшим его высоту с 254 до 137 пикселей. Однако при этом на рисунке могут исчезнуть мелкие детали. Чтобы этого не происходило, можно сделать наоборот - увеличить ширину, поменяв коэффициенты местами или поделив на 0,54:

254 / 0,54 = 470 (остаток отброшен)

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

Попробуем в любом графическом редакторе изменить размер у изображения логотипа журнала, затем сохраним первый вариант рисунка, с уменьшенной высотой, как DGLOGO_1.BMP, а второй, с увеличенной шириной, как DGLOGO_2.BMP и немного допишем файл BUILDLIB.BAT:

@echo off
del shapelib.shp
shapelib.exe shapelib.shp dglogobw.bmp DGMagLogo
shapelib.exe shapelib.shp dglogo_1.bmp DGMagTiny
shapelib.exe shapelib.shp dglogo_2.bmp DGMagWide

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


640x400 NewsMaster 1.0 preview
NewsMaster 1.0, все три рисунка: оригинальный, сжатый по высоте, растянутый по ширине


640x400 NewsMaster 1.0 zoomed
NewsMaster 1.0, сжатый по высоте рисунок крупным планом

Напоследок стоит заметить, что при извлечении и использовании оригинальных изображений из стандартных библиотек NewsMaster на современных мониторах и разрешениях придётся проделывать обратную операцию - т.е. вытягивать рисунки по вертикали делением высоты на 0,54 (от сокращения ширины сильно пострадает качество и без того мелких рисунков).


P.S. Если говорить об изображениях, то стоит упомянуть и файл NEWS.ICN, содержащий значки интерфейса программы. Он состоит из последовательно записанных друг за другом изображений следующего формата:

word height - высота рисунка;

word width - ширина рисунка;

byte [((width + 7) / 8) * height] - графические данные рисунка.

Этот формат повторяется для каждого изображения до конца файла.

В NewsMaster версии II этот файл начинается с дополнительного слова (2 байта) со значением 0003Dh (61) - вероятно, количество изображений в файле (хотя их там только 60).

Несмотря на параметры перед каждым изображением, они все фиксированного размера 40x20 пикселей, при этом, в отличие от библиотек клипарта и отправляемых на печать данных, хранятся они в представлении для вывода на экран, где бит 0 - чёрный, а 1 - белый.


Завершая данную серию статей, необходимо подвести некоторый итог. За годы реставрации Legacy Software (устаревшее программное обеспечение) удалось вернуть к жизни, починить, оживить, а также сделать более комфортной работу у множества приложений. Нет никаких причин, чтобы отказываться от удобной, привычной и проверенной временем программы в угоду современным аналогам (если таковые вообще имеются), многие из которых, к сожалению, обладают невнятным интерфейсом, неоправданно завышенными требованиями к ресурсам или вообще были лишены необходимого функционала. Как показано в статьях на примере NewsMaster, несколько небольших утилит способны облегчить, упростить и адаптировать работу практически любой программы под современные системы и нужды. Равно как и какие-либо неудобства при работе или даже конфликты с современным оборудованием ещё не повод расставаться с программой или искать ей замену. Потому что все эти вещи решаемы и не являются проблемой.


Исходные коды из этой статьи вместе с готовыми скомпилированными программами доступны по ссылке:

http://systools.losthost.org/files/news_prg.zip


Спасибо Александру "uav1606" за подготовку статей к публикации, а также всему коллективу Downgrade за интересный и познавательный журнал.


Специально для Downgrade N29'2019

© SysTools 2019

http://systools.losthost.org/


[1] В NewsMaster версии II в библиотеку добавили одно изображение, и их стало 151, именно поэтому мы рассматриваем эту библиотеку от версии 1.0.

[2] Безусловно, можно выделять несколько блоков или использовать связанные списки из отдельных небольших элементов, но NewsMaster использует простой подход.

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

[4] Для сравнения: среди всех версий NewsMaster максимальное количество изображений, размещённых в одной библиотеке, не превышает 151 - т.е. наш конвертер будет покрывать в том числе и размеры стандартных библиотек.

[5] Захват изображения ею можно делать и в старом формате (переключается по Ctrl+P - PrintMaster Capture), но почему-то только фиксированного размера в 88x52 пикселя.

[6] В командной строке DOS, в отличие от Windows, ещё не обрабатывались двойные кавычки, так что и обрамлять ими строку, содержащую пробелы, чтобы она считалась единым аргументом, бесполезно.

[7] Наверное, поэтому для работы программы и был выбран режим 640x200, как зрительно наиболее близкий к результату печати.

[8] Иными словами, нарисованный в графическом режиме с таким соотношением сторон четырёхугольник с прямыми углами и одинаковым количеством пикселей по каждой стороне визуально будет выглядеть именно квадратом, а не прямоугольником.


2019.12.31


[ Статьи ]