SysTools Logo SysTools


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


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


В прошлой статье (см. Downgrade N27) мы разобрали создание драйвера для клавиатуры, чтобы позволить "Журналисту" корректно печатать русские буквы на QWERTY-раскладке. Теперь же давайте рассмотрим создание своего драйвера для принтера, чтобы иметь возможность печатать документы из программы в BMP-файлы на диске. Помимо теоретического, у такого драйвера есть и вполне практическое применение: официальная версия DOSBox не работает с принтером, да и не у каждого он есть, а посмотреть результат работы на широком экране, вместо мелкого режима предварительного просмотра в "Журналисте", тоже хочется. Это не говоря уже о том, что таким образом можно собрать в электронном виде набранную там аутентичную статью или даже целый номер журнала. Здесь же стоит заметить, что создание драйвера и печать на диск наименее трудозатратный способ из возможных - весь объём работ по формированию изображения на странице будет проделан самим "Журналистом", так что нам не придётся разбирать форматы файлов статей, шрифтов и т.д.


960x720 NewsMaster printing example
Пример печати в BMP-файл (начало прошлой статьи)

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

Как уже было сказано в предыдущей статье, с программой поставлялся большой комплект драйверов для принтеров, пополнявшихся с каждой новой версией. При этом печать осуществлялась через работу с прерыванием 017h, которое, как и в случае с клавиатурным драйвером, можно было бы перехватить, а уже в коде перехватчика решать, что делать с отправленными на печать данными. Однако такой подход создаёт целый ряд проблем, таких, например, как:

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

- т.к. данные, отправляемые прерыванию 017h, предваряются ESC/P-последовательностями (о которых мы поговорим чуть позже), да ещё и отсылаются по одному символу-байту, то в драйвере придётся обрабатывать текущее состояние работы всевозможных режимов (или сохранять всё в файл, затем писать ещё одну отдельную программу, которая будет разбираться, что со всем этим делать);

- одни и те же ESC/P-последовательности могут различаться у разных моделей принтеров как по формату (количеству пересылаемых байт в одной последовательности), так и по способу действия, а т.к. ничто не запрещает пользователю выбрать любой драйвер принтера из доступных в программе, то создание универсального перехватчика, рассчитанного на произвольную модель принтера, окончательно превращается в практически невыполнимую задачу.

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

Нужно заметить, что не существует каких-то стандартов для написания драйверов к принтерам в программах под DOS - у каждой такой программы, использующей драйвера, будет свой формат, и NewsMaster здесь не исключение. Поэтому нам придётся дизассемблировать какой-то из уже существующих драйверов, разобраться с тем, как он работает, и на его основе сделать свой. В качестве такого драйвера удобнее всего будет взять файл EPSON-FX.PRN по целому ряду причин:

- поставляется с самой первой версии NewsMaster в неизменном виде (т.е. совместим со всеми версиями программы - 1.0, 1.5 и II);

- практически самый маленький из всех по размеру - всего 361 байт (чем меньше драйвер, тем он проще для разбора и изучения);

- этот драйвер для принтеров Epson, а фирма Epson для принтеров являлась в то время флагманом в технологии, на который все ровнялись.

Поэтому давайте дизассемблируем этот драйвер и посмотрим, как же он устроен.

В силу того, что драйвер достаточно простой, а его дизассемблирование выходит за рамки данной статьи, то код уже готового дизассемблированного драйвера будет добавлен в приложение к статье. Этот драйвер компилируется тоже в 361 байт и полностью повторяет на уровне команд работу оригинального, но будет различаться в 13 байтах из-за того, что Flat Assembler слегка иначе записывает некоторые команды, в отличие от того компилятора, который использовался разработчиками NewsMaster. Так, например, команда "xor al, al" в оригинальном драйвере записывалась как байты 032h 0C0h, в то время как Flat Assembler записывает как 030h 0C0h. На работу эти различия никак не влияют.

; FASM source code
; http://flatassembler.net/
;
; epson_fx.asm
; (c) SysTools 2019
; http://systools.losthost.org/
;
; NewsMaster / PrintMaster Epson *X compatible printer driver source codes.
; Source codes restored by disassembling original driver file called "EPSON-FX.PRN".
; This source code has no optimization of any kind because it sole purpose
; is to be instruction-exact to the original printer driver file.
; Not byte-exact since there are difference in 13 bytes with the original file
; due to fact that some assembler instruction can be written in a different ways,
; for example command "xor al, al" can be 32 C0 (original file) or 30 C0 (FASM).

; no "org 0100h" here - it's an overlay file
use16

; these jumps MUST be short jumps (2 bytes)
jmp @LnPitch8 ; 1
jmp @LnPitch6 ; 2
jmp @PrintRow ; 3
jmp @ResetAll ; 4
jmp @SkipPage ; 5
jmp @SkipLine ; 6

@LnPitch8:
  lea  si, [_SeqSpacing8]
  mov  cx, 4d
  nop
  call @PrintSeq
  retf

@LnPitch6:
  lea  si, [_SeqSpacing6]
  mov  cx, 4d
  nop
  call @PrintSeq
  retf

; input:
; es:si - image buffer address
; cx - size for image buffer in bytes
; al - non-zero if printing 0x0A at the end required after 0x0D
@PrintRow:
  mov  [_BufSeg], es
  mov  [_BufOfs], si
  mov  [_BufLen], cx
  mov  [_PrnFlg], al
  mov  di, si
  add  di, cx
  dec  di
  ; al = 0
  xor  al, al
  std
  ; find non-al byte at es:di
  repe scasb
  ; if line empty - skip it
  jz   @f
  ; print only non-empty data (trim empty tail)
  inc  cx
  mov  word [_Seq960LCols + 2], cx
  call @PrintHead
  jnz  @prnt_data_ret
  mov  si, [_BufOfs]
  mov  cx, word [_Seq960LCols + 2]
  call @PrintBuf
  jnz @prnt_data_ret
  @@:
  call @PrintLine
  jnz  @prnt_data_ret
  call @CheckKey
  or   ax, ax
  @prnt_data_ret:
  retf

@ResetAll:
  lea  si, [_SeqPrnReset]
  mov  cx, 7d
  nop
  call @PrintSeq
  retf

@SkipPage:
  mov  al, 00Ch
  call @PrintChr
  retf

@SkipLine:
  mov  al, 00Ah
  call @PrintChr
  retf

@PrintSeq:
  cld
  @@:
  lodsb
  call @PrintChr
  loope @b
  retn

@PrintBuf:
  cld
  @@:
  lods byte [es:si]
  call @PrintChr
  loope @b
  retn

@PrintChr:
  push dx
  xor  dx, dx
  xor  ah, ah
  int  017h
  pop  dx
  test ah, 029h
  retn

@PrintLine:
  mov  al, 00Dh
  call @PrintChr
  jnz  @f
  test [_PrnFlg], 0FFh
  nop
  jz   @f
  mov  al, 00Ah
  call @PrintChr
  @@:
  retn

@PrintHead:
  lea  si, [_Seq960LCols]
  mov  cx, 4
  nop
  call @PrintSeq
  retn

@CheckKey:
  push dx
  mov  ah, 001h
  int  016h
  jz   @f
  xor  ah, ah
  int  016h
  cmp  al, 01Bh
  jz   @CheckKey_ret
  call @PrintPause
  xor  ah, ah
  int  016h
  push ax
  call @PrintClear
  pop  ax
  cmp  al, 01Bh
  jz   @CheckKey_ret
  @@:
  xor  ax, ax
  pop  dx
  retn
  @CheckKey_ret:
  mov  ax, 1
  pop  dx
  retn

@PrintPause:
  push bx
  mov  dx, 01814h
  xor  bh, bh
  mov  ah, 002h
  int  010h
  lea  dx, [_StrWaitText]
  mov  ah, 009h
  int  021h
  pop  bx
  retn

@PrintClear:
  push bx
  mov  dx, 01814h
  xor  bh, bh
  mov  ah, 002h
  int  010h
  lea  dx, [_StrWaitFill]
  mov  ah, 009h
  int  021h
  pop  bx
  retn

_SeqSpacing8 db 01Bh, 041h, 008h, 00Dh
_SeqSpacing6 db 01Bh, 041h, 006h, 00Dh
_Seq960LCols db 01Bh, 04Ch, 0C0h, 003h
_SeqPrnReset db 01Bh, 040h, 01Bh, 039h, 01Bh, 04Fh, 00Dh
_StrWaitText db 'Pausing... Press a key to continue.$'
_StrWaitFill db '                                   $'
_BufSeg dw 0
_BufOfs dw 0
_BufLen dw 0
_PrnFlg db 0

Просмотрев код драйвера, можно заметить, что начинается он с 6-ти коротких jmp (это важно, они должны быть именно короткими - по два байта) на следующие участки кода:

LnPitch8

LnPitch6

PrintRow

ResetAll

SkipPage

SkipLine

Чтобы понять, за что же именно отвечает каждая из этих подпрограмм, нам придётся заглянуть в справочник фирмы Epson по ESC/P-последовательностям. И здесь хочется ещё раз напомнить о том, что попытка разобрать любой другой драйвер (а есть и короче, пусть и всего на пару байт) окажется неуспешной из-за того, что ESC/P-последовательности, использующиеся там, будут несовместимы со стандартом Epson, а документацию на малоизвестные принтеры того времени найти сейчас практически нереально.

Но сначала стоит немного рассказать о самих ESC/P-последовательностях.

ESC/P (Epson Standard Code for Printers) - язык команд, разработанный фирмой Epson, для управления принтерами. Он используется преимущественно в матричных и некоторых струйных принтерах. ESC/P является фактическим стандартом для матричных принтеров и используется другими производителями, иногда в несколько расширенном или, наоборот, сокращённом виде.

© Wikipedia https://ru.wikipedia.org/wiki/ESC/P

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

Итак, каждая последовательность в этом языке команд начинается с символа ESC - 01Bh (десятичное 27), затем следует код команды и параметры (если есть).

Вооружённые этим знанием, загружаем по ссылке из английской Wikipedia справочное руководство:

- за 1997 год: https://files.support.epson.com/pdf/general/escp2ref.pdf

- или за 2004: http://www.epson.ru/upload/iblock/057/esc-p.pdf

Вот теперь можно предметно рассмотреть оригинальный драйвер для принтера Epson от разработчиков NewsMaster.

Подпрограмма по адресу LnPitch8 отправляет на печать последовательность 01Bh, 041h, 008h, 00Dh (ESC A). Ищем её по первым двум байтам в справочнике Epson и из описания узнаём, что эта команда устанавливает межстрочный интервал в n/60 от дюйма (1 дюйм = 2.54 см). Т.е. данная последовательность устанавливает этот интервал в 8/60 дюйма. Последний символ (00Dh - возврат каретки), вероятно, нужен на тот случай, если принтер сдвинул головку при обработке этой последовательности, так что далее не будем обращать на него внимание.

Подпрограмма по адресу LnPitch6 делает практически то же самое, но устанавливает интервал в 6/60 дюйма.

Подпрограмма PrintRow непосредственно выводит данные на печать. На вход она получает три параметра в регистрах:

ES:SI - указатель на начало буфера с данными для печати;

CX - количество байт в буфере;

AL - параметр, отвечающий за необходимость посылать (если AL не ноль) дополнительно символ 00Ah (перевод строки) в дополнение к 00Dh (возврат каретки) в конце напечатанной строки, если принтер не делает этого автоматически; этот параметр настраивается через текстовый конфигурационный файл NEWS.CFG (LineFeed 0 или LineFeed 1) или программу выбора и настройки принтера NMCONFIG.EXE (последний вопрос: "Does your printer automatically print a linefeed when it prints a carriage return?").


640x400 NewsMaster 1.0 printer setup
Настройка принтера для NewsMaster 1.0 в NMCONFIG.EXE

Подпрограмма по адресу ResetAll посылает на печать целую пачку ESC/P-последовательностей (разбиты на отдельные строки для удобства):

01Bh, 040h - ESC @ - сброс настроек принтера к значениям по умолчанию.

01Bh, 039h - ESC 9 - включить датчик отсутствия бумаги.

01Bh, 04Fh - ESC O - отменяет настройки верхнего и нижнего полей.

Подпрограмма по адресу SkipPage отправляет на печать всего один символ - 00Ch (конец страницы), который прогоняет бумагу до следующей страницы (это тоже, кстати, описано в документации Epson).

Наконец, подпрограмма по адресу SkipLine тоже отправляет на печать один символ - 00Ah (перевод строки), который пропускает текущую и переходит на новую строку (также отражено в документации Epson).

Перед вызовом любой из этих подпрограмм регистр DS устанавливается равным значению сегмента, в который загружен драйвер, т.е. адрес DS:0 будет указывать на первый из тех самых 6 коротких jmp, что очень удобно при работе с данными, т.к. регистр не придётся инициализировать вручную.

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

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

Важный момент: файл драйвера будет загружен через функцию DOS прерывания 021h под номером 04B03h - Load Overlay - в заранее выделенный статический буфер в программе. Размер этого буфера очень небольшой, всего 1200 байт в NewsMaster версий 1.0 и 1.5, поэтому и драйвер не должен превышать этого размера, в противном случае последствия будут катастрофичными, вплоть до фатальных ошибок при запуске программы. В NewsMaster II этот буфер был увеличен примерно в 5 раз, но универсальный драйвер, как поддерживающий все версии упомянутый выше EPSON-FX.PRN, должен умещаться в минимальный размер. Для com-формата этот размер будет равен размеру файла на диске (под все статические буферы место обязательно должно быть распределено заранее, чтобы учитываться в размере файла). Но вот exe-формат может занимать немного больше за счёт заголовка MZ и таблицы размещения, однако, загруженный в память, опять же, не должен превышать указанного размера. Здесь же стоит добавить, что функция Load Overlay не подготавливает PSP, не инициализирует регистры и не делает многое другое, что происходит при обычном запуске программы. Поэтому нельзя полагаться и рассчитывать на эти вещи [1].


640x400 NewsMaster II printing menu
Различные режимы печати в NewsMaster II будут отличаться только при работе со специальными драйверами нового типа

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

; FASM source code
; http://flatassembler.net/
;
; logdmprn.asm
; (c) SysTools 2019
; http://systools.losthost.org/
;
; NewsMaster / PrintMaster virtual printer driver logs dumper
; This printer driver tested and compatible with NewsMaster version 1.0, 1.5 and II.
; Set up any printer driver in NMCONFIG.EXE and replace NEWS.PRN file in the program
; directory with compiled binary file of this virtual printer driver.
; LOGDMPRN.BIN => NEWS.PRN
;
; WARNING!
; Printer driver will be loaded via DOS Fn 04B03H (Load Overlay) to tiny static buffer.
; This buffer size only 1200 bytes long in NewsMaster 1.0.
; Because of that the driver must be as small as possible.
; For example:
; 1. Largest driver CGP220.PRN size is 1192 bytes and it's safe to use.
; 2. Driver in 1384 bytes long are already too big and will cause the program to crash.
; 3. Note that size of the driver IBMCMPCT.PRN is 1664 bytes (bigger than 1200) but it's
;    an .EXE file and amount of bytes it takes in the memory will be less than the file size.

; no "org 0100h" here - it's an overlay file
use16

; these jumps MUST be short jumps (2 bytes)
jmp @LnPitch8 ; 1
jmp @LnPitch6 ; 2
jmp @PrintRow ; 3
jmp @ResetAll ; 4
jmp @SkipPage ; 5
jmp @SkipLine ; 6

; call order for each page:
; [4] ResetAll
; [1] LnPitch8
; [3] PrintRow (for each row)
; [5] SkipPage

; -----------------------------------------------------------------------------

; and here long calls can be used

@LnPitch8:
  mov  dx, _LnPitch8
  mov  cx, 10d
  call @DumpLogs
  xor  ax, ax
  retf

@LnPitch6:
  mov  dx, _LnPitch6
  mov  cx, 10d
  call @DumpLogs
  xor  ax, ax
  retf

@PrintRow:
  ; save al for later usage
  push ax
  ; print cx (data size)
  mov  ax, cx
  mov  cx, 4d
  mov  di, _PrintRow + 9d
  call @ToHex
  ; print al (line flag)
  pop  ax
  mov  cx, 2d
  mov  di, _PrintRow + 14d
  call @ToHex
  ; print current row
  inc [_rows]
  mov ax, [_rows]
  mov cx, 4d
  mov di, _PrintRow + 18d
  call @ToHex
  ; and now output
  mov  dx, _PrintRow
  mov  cx, 25d
  call @DumpLogs
  xor  ax, ax
  retf

@ResetAll:
  mov  dx, _ResetAll
  mov  cx, 10d
  call @DumpLogs
  xor  ax, ax
  ; reset rows count
  mov [_rows], ax
  retf

@SkipPage:
  mov  dx, _SkipPage
  mov  cx, 10d
  call @DumpLogs
  xor  ax, ax
  retf

@SkipLine:
  mov  dx, _SkipLine
  mov  cx, 10d
  call @DumpLogs
  xor  ax, ax
  retf

; -----------------------------------------------------------------------------

@DumpLogs:
  ; save for later usage
  push dx
  push cx
  ; try to open file
  mov  dx, _name
  mov  al, 002h
  mov  ah, 03Dh
  int  021h
  jnc  @f
  ; failed to open - try to create
  mov  dx, _name
  xor  cx, cx
  mov  ah, 03Ch
  int  021h
  ; failed to create - exit
  jc   @DumpLogs_ret
  @@:
  ; save file handle
  push ax
  ; seek to file end
  mov  bx, ax
  xor  cx, cx
  xor  dx, dx
  mov  al, 002h ; from file end
  mov  ah, 042h
  int  021h
  ; append data to file
  pop  bx
  ; saved before
  pop  cx
  pop  dx
  push dx
  push cx
  push bx
  mov  ah, 040h
  int  021h
  ; close file
  pop  bx
  mov  ah, 03Eh
  int  021h
@DumpLogs_ret:
  pop cx
  pop dx
  retn

; -----------------------------------------------------------------------------

@ToHex:
  test cx, cx
  jz   @ToHex_ret
  add  di, cx
@conv:
  dec  di
  mov  dl, al
  shr  ax, 004h
  and  dl, 00Fh
  cmp  dl, 9d
  jbe  @f
  add  dl, 'A' - '0' - 10d
@@:
  add  dl, '0'
  mov  [di], dl
  loop @conv
@ToHex_ret:
  retn

; -----------------------------------------------------------------------------

; data area
_rows dw 0
_name db 'LOGSDUMP.LOG',0
_LnPitch8 db 'LnPitch8',13,10
_LnPitch6 db 'LnPitch6',13,10
_PrintRow db 'PrintRow ???? ?? (????)',13,10
_ResetAll db 'ResetAll',13,10
_SkipPage db 'SkipPage',13,10
_SkipLine db 'SkipLine',13,10

Установка этого драйвера, как и будущего драйвера для виртуальной печати, очень простая - достаточно заменить файл NEWS.PRN в корне каталога программы. На всякий случай, если программа ещё ни разу не настраивалась, то нужно вызвать NMCONFIG.EXE, выбрать там любой принтер, а уже затем заменить NEWS.PRN новым драйвером.

Итак, после компиляции отладочного драйвера, установки и печати в NewsMaster 1.0, получим файл LOGSDUMP.LOG примерно такого содержания:

ResetAll

LnPitch8

PrintRow 03C0 01 (0001)

PrintRow 03C0 01 (0002)

<пропущены строки с 0003 до 005E>

PrintRow 03C0 01 (005F)

SkipPage

Отсюда видно, что вызов происходит в следующем порядке:

ResetAll

LnPitch8

PrintRow (для каждой строки)

SkipPage

И всё это повторяется для каждой страницы в документе.

Интересно, что подпрограммы LnPitch6 и SkipLine не вызываются никогда. Здесь нужно заметить, что рассматриваемый драйвер для Epson поставляется со всеми версиями NewsMaster, которые удалось найти: 1.0, 1.5 и II, но, возможно, эти функции "остались в наследство" со времён раннего этапа разработки или используются в PrintMaster (схожая программа от тех же разработчиков).

Нам же для записи печати в файл необходимо:

- создать файл (открыть на запись);

- записывать в него отправляемые на печать данные;

- и закрыть по завершении печати.

Для этого как нельзя лучше подходят подпрограммы ResetAll, PrintRow и SkipPage, а подпрограмму LnPitch8 лучше опустить как малозначимую. Потому что при подготовке к печати для инициализации принтера обязательно будет вызываться ResetAll, при отправке данных на печать PrintRow, а при завершении SkipPage (справочник Epson прямо рекомендует прогонять страницу после окончания печати). А все остальные подпрограммы могут оказаться опциональными, поэтому их лучше избегать.

Следующая важная вещь, на которую стоит обратить внимание при использовании отладочного драйвера, это то, что в NewsMaster II подпрограмма PrintRow вызывается не 95 (05Fh) раз, а уже 126 (07Eh) раз - т.е. высота страницы была увеличена, и в разных версиях программы она разная. Мы к этому вернёмся чуть позже, а, пока что, давайте разберём подпрограмму PrintRow из оригинального драйвера более подробно.

Сперва с конца входного буфера ищется хотя бы один не нулевой байт [2]. Если все байты нули, то строка считается пустой, никакой графики не печатается, а принтер всего лишь переходит на следующую строку. Это не только позволяет более бережно относиться к технике, а не гонять зазря головку принтера туда-сюда, но и увеличивает скорость печати. А вот полученное таким образом, но не нулевое, количество байт отправляется на печать, предваряясь следующей ESC/P-последовательность:

01Bh, 04Ch, 0C0h, 003h - ESC L - печать строки графики.

Где последние два байта будут перезаписаны драйвером на необходимое количество вместо 960 (003С0h). Здесь также стоит обратить внимание, что и регистр CX, содержащий размер буфера, всегда равен 960 на входе в подпрограмму (что видно из логов работы отладочного драйвера).

Если сейчас начать писать данные в файл и попытаться просмотреть их в какой-нибудь программе (например, через Hexapad - View -> Sprite Search, width: 960, format: 1bpp), то вместо изображения там будет маловразумительная каша.

Попробуем разобраться, почему же так происходит. Размер рисунка, действительно, 960 точек, но т.к. это чёрно-белый режим, следовательно, одна точка должна кодироваться 1 битом, а в байте их 8. Отсюда: 960/8 = 120. Но размер буфера в байтах, как мы помним, в 8 раз больше этого - 960. Как же так? Дело в том, что в матричных принтерах иголки для печати на головке принтера расположены не горизонтально, а вертикально. Поэтому один байт кодирует не 8 точек по горизонтали, как это обычно принято в графических форматах, а 8 точек по вертикали. Таким образом, головка принтера печатает не одну строку в 960 точек, а сразу целую полосу из 8 таких строк [3]. Поэтому посылаемые на печать данные придётся переставлять перед сохранением в другом порядке, чтобы получить осмысленное изображение для графического формата.

Обладая этими знаниями, мы можем легко подсчитать размер рисунка в пикселях - помните, чуть выше было показано, сколько раз вызывается подпрограмма PrintRow? Будет 960x760 (95*8) и 960x1008 (126*8) соответственно.

И давайте разберёмся, откуда взялась ширина в 960 точек. Странно, но этой величины в явном виде нет в справочниках Epson по ссылкам выше с официального сайта. В описании команды ESC L упоминается только про 120 на 60 dpi и всё. Так что здесь здорово спасает бумажная документация к принтерам - удивительно, но, например, руководство от фирмы HYUNDAI к матричным принтерам моделей HDP-910/920 (Epson-совместимые) за 1989 год описывает пусть и не все ESC/P-последовательности, но зато более детально. Так, про интересующую нас последовательность там указано, что в этом режиме можно напечатать 960 точек на 80-колоночных принтерах и 1872 на 156-колоночных. Если же взять русское руководство от самой Epson для LX-1050+ за 1994 год, то про ESC L там всё так же скудно, как и в PDF-документации, зато подробно описано вертикальное расположение иголок и их программирование, в том числе всякие нюансы о том, что в этом режиме из 9 иголок используются только верхние 8 (LX-1050+ - это 9-ти иголочный принтер), а также зачем устанавливать межстрочный интервал в 8/60 дюйма перед выводом изображений (см. про LnPitch8 выше) - оказывается, это убирает отступ между строками, чтобы печать графики шла неразрывно [4].

Теоретически, подойти к решению этой проблемы можно было и с другой стороны: dpi - dots per inch, т.е. точек на дюйм. Бумага формата A4 согласно Wikipedia имеет ширину в 8.27 дюйма. Печать графики, как мы помним из документации Epson, идёт для ширины в 120 dpi. Отсюда 8*120 = 960, хотя и не совсем очевидно, что дробную часть дюйма (под поля?) нужно было отбросить. Поэтому поиск точных числовых величин в документации - наиболее надёжный способ.

Фото документации


1990x1430 Epson LX-1050+ manual
Epson LX-1050+ (1994)


1990x1360 HYUNDAI HDP-910/920 manual
HYUNDAI HDP-910/920 (1989)

Теперь несколько слов и о формате BMP, выбранном для хранения изображений.

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

Из существующих тогда форматов можно было бы взять PCX от фирмы ZSoft Corporation, благо, что и разработан он был за год до этого (в 1985). Да и сжатие RLE, которое используется в этом формате, конечно, не самое лучшее, но, во-первых, место экономит, а, во-вторых, достаточно простое в реализации.

Но всё же экономить мы не будем в силу следующих причин:

- чем драйвер проще, тем он легче для изучения и дальнейшего изменения;

- алгоритм RLE в PCX хоть и достаточно прост в реализации, однако нужно помнить про ограничение драйвера в 1200 байт - на добавление туда сжатия может просто не хватить места;

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

Так что для сохранения изображений будем использовать формат BMP версии 3, потому что первая версия хоть и появилась в 1985, но именно третья версия получила самое широкое распространение и поддержку, хоть и вышла на 5 лет позже [5].

Подробное описание формата BMP можно найти в Интернете, нам же, чтобы не загромождать статью, стоит уделить особое внимание трём важным вещам.

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

Во-вторых, каждая строка пикселей в BMP-формате, опять же, согласно документации, должна быть выровнена на 32-х битную границу. Это значит, что если число байт в строке пикселей не кратно 4 (32/8 = 4), то необходимо дописать от 1 до 3 байт. У нас же, как было упомянуто выше, 960/8 = 120 байт, а 120 прекрасно делится на 4. Поэтому в данном (частном) случае выравнивание не потребуется, но упомянуть о нём всё же стоит.

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

А вот теперь, зная, как устроена внутренняя архитектура драйверов для принтеров в NewsMaster и описание формата BMP, можно начать работу над своим драйвером.

; FASM source code
; http://flatassembler.net/
;
; prntodsk.asm
; (c) SysTools 2019
; http://systools.losthost.org/
;
; NewsMaster / PrintMaster virtual printer driver
; This printer driver tested and compatible with NewsMaster version 1.0, 1.5 and II.
; Set up any printer driver in NMCONFIG.EXE and replace NEWS.PRN file in the program
; directory with compiled binary file of this virtual printer driver.
; PRNTODSK.BIN => NEWS.PRN
;
; WARNING!
; Printer driver will be loaded via DOS Fn 04B03h (Load Overlay) to tiny static buffer.
; This buffer size only 1200 bytes long in NewsMaster 1.0.
; Because of that the driver must be as small as possible.
; For example:
; 1. Largest driver CGP220.PRN size is 1192 bytes and it's safe to use.
; 2. Driver in 1384 bytes long are already too big and will cause the program to crash.
; 3. Note that size of the driver IBMCMPCT.PRN is 1664 bytes (bigger than 1200) but it's
;    an .EXE file and amount of bytes it takes in the memory will be less than the file size.

; no "org 0100h" here - it's an overlay file
use16

; these jumps MUST be short jumps (2 bytes)
jmp @LnPitch8 ; 1
jmp @LnPitch6 ; 2
jmp @PrintRow ; 3
jmp @ResetAll ; 4
jmp @SkipPage ; 5
jmp @SkipLine ; 6

; call order for each page:
; [4] ResetAll
; [1] LnPitch8
; [3] PrintRow (for each row)
; [5] SkipPage

; -----------------------------------------------------------------------------

; and here long calls can be used

; stubs since this driver don't need it
@LnPitch8:
@LnPitch6:
@SkipLine:
  ; set zero flag as no error
  xor  ax, ax
  retf

@PrintRow:
  call @DumpFile
  retf

@ResetAll:
  call @OpenFile
  retf

@SkipPage:
  call @FreeFile
  retf

; -----------------------------------------------------------------------------

@OpenFile:
  ; init .BMP header
  xor  ax, ax
  ; file size = HDRLEN for now
  mov  [_bfSize_lo], HDRLEN
  mov  [_bfSize_hi], ax
  ; image height = 0
  mov  [_biHeight_lo], ax
  mov  [_biHeight_hi], ax
  ; image size = 0
  mov  [_biSizeImage_lo], ax
  mov  [_biSizeImage_hi], ax
  ; find first non-existent file
@OpenFile_find:
  ; next number
  inc  ax
  ; save it in the file handle variable
  mov  [_file], ax
  ; convert decimal number to text string
  mov  di, _name + 7
  mov  bx, 10d
  mov  cx, 4
@@:
  xor  dx, dx
  div  bx
  add  dl, '0'
  mov  [ds:di], dl
  dec  di
  loop @b
  ; try to get file attribute
  ; it's a check if file or a directory with that name already exists
  mov  dx, _name
  mov  al, 000h
  mov  ah, 043h
  int  021h
  ; error if carry flag is set (probably file not found)
  jc   @f
  xor  ax, ax
  ; make file handle invalid in case of overflow
  xchg ax, [_file]
  cmp  ax, 9999d
  ; if 9999 overflow - do not print anything
  jz   @OpenFile_ret
  ; or else - continue checking
  jmp  @OpenFile_find
@@:
  ; clear file handle for error - will be used as invalid handle
  ; since file handle 0 already preserved as standard input handle
  ; also cx - file attributes
  ; create new file
  xor  cx, cx
  mov  [_file], cx
  mov  dx, _name
  mov  ah, 03Ch
  int  021h
  jnc  @f
  ; zero flag will be non-empty for error
  mov  al, 001h
  jmp  @OpenFile_ret
@@:
  ; no error - save file handle for future use
  mov  [_file], ax
  ; write .BMP header
  mov  dx, _head
  mov  cx, HDRLEN
  mov  bx, ax
  mov  ah, 040h
  int  021h
  ; no error
  xor  ax, ax
@OpenFile_ret:
  or   ax, ax
  retn

; -----------------------------------------------------------------------------

@FreeFile:
  ; set as error
  mov  ax, 001h
  ; check for valid file handle
  cmp  [_file], 000h
  jz   @f
  ; seek to file start
  xor  cx, cx ; offset hi = 0
  xor  dx, dx ; offset lo = 0
  mov  bx, [_file] ; file handle
  mov  al, 000h ; seek from start
  mov  ah, 042h
  int  021h
  ; write updated .BMP header
  mov  dx, _head
  mov  cx, HDRLEN
  mov  bx, [_file]
  mov  ah, 040h
  int  021h
  ; close file
  mov  bx, [_file]
  mov  ah, 03Eh
  int  021h
  ; no error
  xor  ax, ax
  ; file handle not valid anymore
  mov  [_file], ax
@@:
  or   ax, ax
  retn

; -----------------------------------------------------------------------------

; input:
; es:si - image buffer address
; cx - size for image buffer in bytes
; al - non-zero if printing 0x0A at the end required after 0x0D
@DumpFile:
  ; set as error
  mov  ax, 001h
  ; check for valid file handle
  cmp  [_file], 000h
  jz   @DumpFile_ret
  ; revert image data
  ; note that each byte in B/W screen mode or a .BMP file represents horizonal line of 8 pixels
  ; but the data of a matrix printer is different: each byte represents a VERTICAL line of 8 dots
  ; because of that binary image data needs to be rearranged before it can be saved as bitmap
  mov  cl, 7
  @for_j: ; y = 0..7 also used as shift index
    ; save data pointer original value
    push si
    mov  di, _data
    mov  ch, 120d
    @for_i: ; x = 0..119
      xor  al, al
      mov  bl, 8
      @for_k: ; index in 8 byte block
        mov  ah, [es:si]
        ; since only cl register can be used as shift index value use it as for_j loop index too
        shr  ah, cl
        and  ah, 1
        shl  al, 1
        or   al, ah
        inc  si
        dec  bl
      jnz  @for_k
      ; note that in printer driver bit 1 means print (black ink) and 0 means skip (white paper)
      ; but in the .BMP file 0 are black and 1 are white, so bits in byte needs to be inverted
      not  al
      mov  [ds:di], al
      inc  di
      dec  ch
    jnz  @for_i
    ; save cx
    push cx
    mov  cx, 120d
    xor  ax, ax
    ; increment height
    ; do a decrement instead because this must be a negative value
    ; (see explanation below in .BMP header)
    sub  [_biHeight_lo], 001h
    sbb  [_biHeight_hi], ax
    ; update .BMP file headers
    add  [_bfSize_lo], cx
    adc  [_bfSize_hi], ax
    add  [_biSizeImage_lo], cx
    adc  [_biSizeImage_hi], ax
    ; write row of pixels to the file
    ; also cx - number of bytes to write
    mov  dx, _data
    mov  bx, [_file]
    mov  ah, 040h
    int  021h
    ; check for any key pressed
    call @CheckKey
    ; restore cx
    pop  cx
    ; rewind input data pointer to start
    pop  si
    ; test zero flag from CheckKey
    jnz  @DumpFile_ret
    ; next row
    dec  cl
  jns  @for_j
@DumpFile_ret:
  or  ax, ax
  retn

; -----------------------------------------------------------------------------

@CheckKey:
  ; check key in buffer
  mov  ax, 0001h
  call @ReadKey
  ; no key in buffer
  jz   @f
  ; ESC pressed - exit
  cmp  al, 01Bh
  jz   @f
  ; show pause text
  mov  dx, _wait
  call @ShowText
  ; wait for key
  mov  ax, 000h
  call @ReadKey
  push ax
  ; clear pause text
  mov  dx, _fill
  call @ShowText
  pop  ax
  ; ESC pressed - exit
  cmp  al, 01Bh
  jz   @f
  ; no error - continue printing
  xor  ax, ax
@@:
  ; if ESC pressed - close file
  cmp  al, 01Bh
  jnz  @f
  push ax
  ; close file handle since printing aborted (@SkipPage will be never called)
  call @FreeFile
  ; and delete partially saved file
  mov  dx, _name
  mov  ah, 041h
  int  021h
  pop  ax
@@:
  or   ax, ax
  retn

; -----------------------------------------------------------------------------

@ReadKey:
  ; ax = 0 - read, do not check
  test ax, ax
  jz   @f
  ; check for key in buffer
  mov  ah, 001h
  int  016h
  jnz  @f
  xor  ax, ax
  jmp  @ReadKey_ret
@@:
  ; read key from buffer
  mov  ah, 000h
  int  016h
@ReadKey_ret:
  or   ax, ax
  retn

; -----------------------------------------------------------------------------

@ShowText:
  push dx
  ; go to screen center
  mov  dx, 01814h
  xor  bh, bh
  mov  ah, 002h
  int  010h
  ; output string text
  pop  dx
  mov  ah, 009h
  int  021h
  retn

; -----------------------------------------------------------------------------

; data area
_name db 'PAGE0000.BMP',0
_wait db 'Pausing... Press a key to continue.$'
_fill db '                                   $'
; make everything below word-aligned
align 2
; DOS file handle
_file dw 0
; BITMAPFILEHEADER
_head            dw 04D42h ; _bfType
_bfSize_lo       dw 0      ; whole file size
_bfSize_hi       dw 0
_bfReserved1     dw 0
_bfReserved2     dw 0
_bfOffBits       dd HDRLEN ; offset to the bitmap data
; BITMAPINFOHEADER
_biSize          dd 40d    ; header size
_biWidth         dd 960d   ; page width
_biHeight_lo     dw 0      ; 760 / 1008 (NewsMaster 1.x / NewsMaster II)
_biHeight_hi     dw 0      ; if biHeight is negative, the bitmap is a top-down DIB
_biPlanes        dw 1
_biBitCount      dw 1
_biCompression   dd 0      ; 0 - BI_RGB - uncompressed
_biSizeImage_lo  dw 0      ; whole bitmap image data size
_biSizeImage_hi  dw 0
_biXPelsPerMeter dd 0
_biYPelsPerMeter dd 0
_biClrUsed       dd 0
_biClrImportant  dd 0
; palette
_palette dd 00000000h, 00FFFFFFh
HDRLEN = $ - _head
; buffer to hold one line of pixels (960 / 8 = 120)
_data db 120d dup(0)

Код драйвера достаточно простой, с подробными комментариями, но объёмный, так что ниже будут кратко описаны только важные моменты.

1) Чтобы в начальной таблице было 6 коротких jmp (по 2 байта) и они не превратились в длинные (по 3 байта), были сделаны небольшие подпрограммы, находящиеся сразу за этой таблицей, а уже из них вызывается необходимый код.

2) Подпрограммы LnPitch8, LnPitch6 и SkipLine указывают на один и тот же код - заглушку, которая ничего не делает и лишь возвращает статус для zero flag, что всё в порядке, т.к. в данном драйвере они не используются.

3) Подпрограмма ResetAll вызывает OpenFile, где выполняется целая последовательность действий:

- инициализируется заголовок будущего BMP-файла;

- предпринимается попытка найти ещё несуществующий файл перебором по маске PAGE####.BMP, где #### - порядковый номер от 0001 до 9999; делается это через получение атрибутов файла - если атрибуты удалось получить, то файл существует, поэтому ищем следующий; такой способ лучше, чем попытка открыть файл, потому что, во-первых, файл потом не нужно закрывать (код короче), а, во-вторых, это также позволяет отсечь ситуацию с каталогами вместо файлов (например, каталог с именем PAGE1234.BMP нельзя открыть как файл, но и файл такой нельзя будет потом создать, потому что имя элемента файловой системы уже занято каталогом); если все 9999 файлов существуют, то возвращается ошибка - печать невозможна;

- предпринимается попытка создать файл, а если не получилось, то точно так же возвращается ошибка и печать останавливается;

- в случае удачной попытки создания файла, его дескриптор (описатель / handle), сохраняется для дальнейшего использования, а также сразу пишется начальный вариант BMP-заголовка, т.к. потом его в начало не запишешь (мы обновим его актуальными данными позже - перед закрытием файла);

4) Подпрограмма PrintRow вызывает DumpFile, где происходит сохранение присланных графических данных - перестановка битов и построчная запись в файл. Отдельно стоит заметить, что сейчас драйвер занимает 660 байт в скомпилированном виде. Для записи строки он использует статический буфер в 120 байт (120*8 = 960 точек) [7]. Можно было бы, конечно, не писать по одной строке, а сразу сформировать в памяти все 8 и записать их разом, но 660+(120*7) = 1500 - иными словами, это невозможно, ибо тогда код драйвера не уместился бы в отведённые под него 1200 байт. Динамическое же выделение памяти не рассматриваем, т.к. статический буфер удобен ещё и тем, что память для него есть всегда, если драйвер успешно загрузился. Также в этой подпрограмме происходит перерасчёт высоты рисунка (отрицательной) и полей BMP-заголовков под размеры файла и изображения. Ну и, конечно, как и в оригинальном драйвере, выполняется проверка на нажатие любой клавиши, кроме Esc, что приостанавливает печать до повторного нажатия, в то время как Esc отменяет печать совсем, при этом закрывается и удаляется текущий недописанный BMP-файл. Обратите внимание, что в случае отмены печати подпрограмма SkipPage не вызывается, поэтому закрывать и удалять недописанный файл нужно именно здесь, чтобы не произошла утечка файлового дескриптора.

5) Наконец, подпрограмма SkipPage вызывает FreeFile, где происходит перезапись начального BMP-заголовка на корректный обновлённый, а затем и закрытие файла - запись рисунка окончена.


P.S. NewsMaster позволяет вставлять текст из текстовых файлов (имена должны соответствовать маске "*.TXT") - они грузятся в буфер обмена, откуда их потом необходимо вставлять в текст. Но русские символы так вставить не получится, поэтому для загрузки именно русского текста в редактор "Журналист" нужно будет перед этим обработать входной файл, и произвести замену букв соответственно таблице символов (русские => английские), которую можно построить, например, на основе данных из прошлой статьи:

; было
db 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'
db 'абвгдежзийклмнопрстуфхцчшщъыьэюя'
db 'Ёё'
; стало
db 'ABWGDEVZIJKLMNOPRSTUFHC^[]_YX\@Q'
db ' abwgdevzijklmnoprstufhc~{}',07Fh,'yx|`q'
db 'Ee'

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

И пара слов об оригинальных драйверах для принтера. Как уже было сказано в прошлом постскриптуме, в NewsMaster II все они были упакованы в файл PRINTERS.PRG. Формат этого архива простой, без сжатия, так что драйвера оттуда можно легко достать при помощи вот такого скрипта:

Get Count Short
For I = 1 To Count
  GetDString Name 14
  Get Offs Long
  Get Size Long
  Log Name Offs Size
Next I

Этот код необходимо сохранить в текстовый файл с именем UNPAKPRG.BMS, поместить рядом упомянутый файл PRINTERS.PRG, а также программу QuickBMS.exe, затем вызвать:


quickbms.exe -d UNPAKPRG.BMS PRINTERS.PRG


Программу QuickBMS можно взять здесь:

http://quickbms.aluigi.org/


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

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


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

© SysTools 2019

http://systools.losthost.org/


[1] См. подробности, например, в TECH Help! "DOS Fn 4b03H: Load Overlay".

[2] Один из немногих моментов, когда заглядывать в справку TECH Help! вредно, потому что в описании команд языка Assembler для работы со строками, к сожалению, допущены ошибки даже в последней, шестой, версии справочника - в частности у scasb указаны регистры DS:DI, в то время как из кода драйвера видно, что работа идёт с ES:DI. Справочник хороший, но содержит ошибки - будьте осторожны.

[3] Что, вообще говоря, логично, т.к. увеличивает скорость печати (по сути 8 строк за один проход), но не совсем очевидно, особенно если никогда с матричными принтерами на низком уровне работать до этого не приходилось.

[4] Т.е. установку интервала можно было бы поместить в ResetAll, но, видимо, в силу уже сложившейся архитектуры драйвера, всё оставили как есть.

[5] Версия 1 появилась вместе с Windows 1.0 (1985), а версия 3 - вместе с Windows 3.0 (1990), согласно этому сайту.

[6] Можно поменять чёрный и белый цвета палитры местами в BMP-файле, но тогда некоторые программы при сохранении файла будут насильно менять цвета назад, и обращать биты в байтах (тот же Paint в Windows, впрочем, он и высоту делает положительной, но так хотя бы затронут будет только порядок строк). Здесь же, справедливости ради, стоит заметить, что в BMP тоже есть некое RLE-сжатие, однако его поддерживают ещё меньше программ, чем отрицательную высоту.

[7] Из-за того, что весь блок в 1200 байт используется только драйвером для принтера, то необходимости в статическом буфере на диске нет, и его можно заменить с "_data db 120d dup(0)" на "_data rb 120d", сократив размер файла. Однако когда все переменные объявлены явно в статическом виде, то это позволяет легко и наглядно контролировать ситуацию с выходом размера драйвера за отведённые ему границы.


2019.09.30


[ Статьи ]