Краткое неформальное введение в графику Windows
Вежневец Владимир1
Аннотация
Целью этого документа является ознакомление новичков с тем, как
устроена 2D графика в Windows и как с помощью имеющихся средств
можно делать простые, но полезные вещи.
К данному тексту прилагаются три программки с исходным кодом под
Visual C++ 6.0, Delphi 5 и CBuilder 5, в которых дан "скелет"
приложения умеющего загрузить, показать, обработать и сохранить
изображение в формате bmp.
Содержание
1. Цель этого документа
2. Устройство графики в Windows
2.1 Graphics Device Interface и Device Context
2.2 Как рисовать в Device Context?
2.3 Как рисовать в окно приложения?
2.4 Когда рисовать в окно приложения? WM_PAINT - что это?
2.5 Как БЫСТРО рисовать в Device Context?
2.6 Как загрузить и вывести на экран изображение?
2.7 Как нарисовать что-либо на изображении?
Надстройки над GDI
3.1 MFC надстройка над GDI
3.2 VCL надстройка над GDI
4. Как (относительно) быстро обрабатывать изображения?
4.1 Быстрый способ доступа к пикселям в GDI и MFC
4.2 Быстрый способ доступа к пикселям в VCL
1 Цель этого документа
Целью этого документа, как и первого (и частично второго) задания
в курсе "Машинная графика" для студентов второго курса ВМиК МГУ
является ознакомление новичков с тем, как устроена 2D графика в
Windows и как с помощью имеющихся средств можно делать простые, но
полезные вещи.
Чего мы коснемся:
- Устройство графики в Windows;
- Как рисовать простые вещи (линии,
геометрический фигуры, текст) с помощью функций WinAPI 2;
- Как загрузить, отобразить и обработать изображение с помощью функций WinAPI;
- MFC надстройка над WinAPI;
- VCL надстройка над WinAPI;
В качестве приложения к данному тексту выступают три программки с
исходным кодом под Visual C++ 6.0 (MFC_GML3), Delphi 5
(DelphiBasis) и CBuilder 5 (SDIApp), в которых дан "скелет"
приложения, умеющего загрузить, показать, обработать и сохранить
изображение в формате bmp.
2 Устройство графики в Windows
2.1 Graphics Device Interface и Device Context
Не стану углубляться в теорию строения Windows и ее графической
подсистемы (литературы на эту тему написано вполне достаточно),
постараюсь коротко изложить некий минимум знаний, который
понадобится при программировании простейшей графики в Windows. При
этом я постараюсь также дать понимание что и как устроено (пускай
на простом уровне).
Во-первых, в Microsoft Windows существует несколько средств для
вывода графической информации, включая DirectDraw, OpenGL, GDI и
т.д. Мы рассмотрим GDI (Graphics Device Interface) - подсистему
Windows, ответственную за вывод графики и текста на дисплей и
принтер. Именно она занимается выводом большинства "окошек",
которые и составляют то, что видит пользователь Windows на экране.
Она является базовым и, пожалуй, простейшим способом вывода
графики в Windows.
С графикой Windows с помощью GDI неразрывно связано понятия
контекста устройства (device context). Контекст устройства (DC) -
это структура данных, содержащая информацию о параметрах и
атрибутах вывода графики на устройство (например, дисплей или
принтер). Такая информация, в частности, включает в себя: палитру
устройства, определяющую набор доступных цветов; параметры пера
для черчения линий; параметры кисти для закраски и заливки;
параметры шрифта, использующегося для вывода текста.
В GDI существуют пять типов контекста устройства - связанный с
дисплеем (Display DC), принтером (Printer DC), контекст
виртуального устройства в памяти (Memory DC), контекст метафайла
(Metafile DC) и специальный вид контекста - информационный
(Information DC).
Первые четыре типа контекста устройства - display, printer, memory
и metafile предоставляют унифицированный интерфейс для вывода
графической информации на разнотипные устройства, освобождая
приложение (и его разработчика) от необходимости заботится о том,
куда именно производится вывод графики. Информационный контекст
для вывода графики не используется, он служит исключительно для
получения информации о параметрах и поддерживаемых режимах
устройства, с которым связан.
В чем отличие первых четырех типов контекста? Это можно понять из
их названий - Display DC служит для вывода на экран, Printer DC
для печати на принтер или графопостроитель, Memory DC служит для
создания растровых изображений в памяти с возможностью быстрого их
копирования в другие типы контекстов (и обратно), Metafile DC
нужен для вывода графики в метафайл. Метафайл - это
хранилище последовательности команд GDI, каждая из которых
описывает одну графическую функцию. В отличие от растровых файлов,
хранящих графическую информацию непосредственно в виде массива
пикселов, метафайл ее хранит в виде последовательности команд,
которая создает результирующий рисунок.
2.2 Как рисовать в Device Context?
Для вывода графической информации существует набор функций,
которые можно разделить на несколько категорий:
- Методы рисования линий:
LineTo, MoveTo, Polyline, Arc, ArcTo, PolyBezier, и др.
- Методы рисования замкнутых фигур:
Ellipse, Rectangle, Polygon, Pie, Chord и др.
- Методы вывода текста:
TextOut, DrawText и т.д.
- Функции работы с растровым изображением: GetPixel, SetPixel, FloodFill, BitBlt и т.д.
Существует отдельная категория функций работы с DC по переключению
режимов и установке параметров вывода графической информации.
Часть из них устанавливается напрямую через определенные функции
(например, SetBkColor), часть - с помощью специальных графических
объектов:
- перо (pen)
- - задает режим вывода линий (цвет, толщина,
стиль);
- кисть (brush)
- - регулирует режим закраски фигур (цвет,
стиль);
- шрифт (font)
- - задает свойства шрифта, которым выводится
текст;
- палитра (palette)
- - задает набор используемых в DC
цветов;
- область (region)
- - используются для задания clipping regions - областей
отсечения, вне которых вывод графики блокируется.
Работа с графическими объектами производится с помощью их
дескрипторов (handles) - HDC, HPEN, HBRUSH, HFONT и т.д. Создание
и удаление объектов производится с помощью соответствующих функций
- например, объект pen создается с помощью CreatePen, удаляется с
помощью DeleteObject. Режимы, задающиеся через графические
объекты, переключаются с помощью создания новых объектов и
указания контексту (DC) использовать их для вывода графики. Это
делается помощью функции SelectObject:
//hdc - дескриптор контекста устройства
HPEN hWhitePen, hBlackPen, hOldPen;
HBRUSH hBlackBrush, hOldBrush;
hWhitePen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
hBlackPen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
hBlackBrush = CreateSolidBrush(RGB(0, 0, 0));
// нарисовать белый квадрат
hOldPen = SelectObject(hdc, hWhitePen);
MoveTo(hdc, 10, 10);
LineTo(hdc, 100, 10);
LineTo(hdc, 100, 100);
LineTo(hdc, 10, 100);
LineTo(hdc, 10, 10);
// нарисовать черную окружность
SelectObject(hdc, hBlackPen);
hOldBrush = SelectObject(hdc, hBlackBrush);
Ellipse(hdc, 10, 10, 100, 100);
// вернуть старый объекты pen и brush в DC
SelectObject(hdc, hOldPen);
SelectObject(hdc, hOldBrush);
// освободить ресурсы
DeleteObject(hWhitePen);
DeleteObject(hBlackPen);
DeleteObject(hBlackBrush);
При выборе нового объекта через SelectObject в качестве
возвращаемого значения передается дескриптор объекта, бывшего в
использовании в DC раньше. Нужно иметь ввиду, что все создаваемые
объекты нужно не забывать удалять их после использования. Более
того, сам DC всегда создается с некоторыми объектами по умолчанию
и при использовании определенных пользователем объектов через
SelectObject нужно в конце работы произвести select объектов,
которые были в DC изначально (см. пример выше).
2.3 Как рисовать в окно приложения?
Для того чтобы выводить графику в определенное окно вашего
приложения нужно сделать буквально следующее:
Получить дескриптор DC, связанный с окном, в которое вы
собираетесь рисовать с помощью функции GetDC(). Нарисовать все,
что вы хотите, с помощью функций DC и в конце "освободить"
контекст с помощью функции ReleaseDC().
Пример:
//hwnd - дескриптор окна, в которое будем рисовать
HDC hdc;
hdc = GetDC(hwnd);
if ¯(hdc)
{
// рисуем что требуется
...
// освобождаем контекст
ReleaseDC(hwnd, hdc);
}
else
{
// обработка ошибки получения контекста
}
Иным образом производится получение/освобождение дескриптора DC
при обработке сообщения WM_PAINT - об этом в следующем разделе.
2.4 Когда рисовать в окно приложения? WM_PAINT - что это?
При выводе графики в Windows есть некоторая тонкость, не всегда
очевидная новичкам в программировании под среды с графическим
интерфейсом. Казалось бы, если нужно что-то отрисовать в окне -
получай его контекст и рисуй. Но не все так просто. Стоит свернуть
окно или закрыть его часть другим окном - все, что было
нарисовано, пропадет.
Дело в том, что Windows не хранит содержимое клиентской части
окна. К клиентской части окна относится ВСЕ, кроме заголовка окна
и управляющих элементов (controls): меню, панелей инструментов
(toolbar), кнопок и т.д. Приложение само должно позаботиться о
том, чтобы отрисовывать свои данные в клиентской области, Windows
лишь посылает ему уведомление когда это нужно сделать. Делается
это посредством посылки окну сообщения WM_PAINT.
Все необходимые действия по полной перерисовке информации
клиентской части окна должны вызываться при обработке события
WM_PAINT. Важным понятием при обработке этого сообщения является
invalid rectangle. Windows определяет invalid rectangle как
наименьшую прямоугольную часть окна, которая была "испорчена" и
должна быть перерисована заново. Когда система обнаруживает
invalid rectangle в клиентской области окна, она генерирует
сообщение WM_PAINT. В ответ на сообщение окно может получить
структуру PAINTSTRUCT, которая среди прочего содержит координаты
invalid rectangle. Это может пригодиться, если есть желание
перерисовывать не все окно, а только ту область, что требуется.
При обработке WM_PAINT должна быть вызвана функция BeginPaint,
которая снова делает invalid rectangle `нормальным'. Также
BeginPaint возвращает дескриптор DC, который должен быть
использован для перерисовки клиентской части окна. Нужно иметь в
виду, что при обработке WM_PAINT дескриптор DC окна должен быть
получен именно с использованием BeginPaint, а освобожден EndPaint,
в то время как во всех других случаях отрисовки нужно использовать
другие функции (например, GetDC/ReleaseDC). Если invalid
rectangle не делается "нормальным" во время обработки этого
события (с помощью BeginPaint или ValidateRect), Windows будет
слать WM_PAINT окну постоянно.
Пример обработки WM_PAINT:
//hwnd - дескриптор окна, в которое будем рисовать
HDC hdc;
PAINTSTRUCT PaintStruct;
hdc = BeginPaint(hwnd, &PaintStruct);
if ¯(hdc)
{
// рисуем что требуется
...
// освобождаем контекст
EndPaint(hwnd, &PaintStruct);
}
else
{
// обработка ошибки получения контекста
}
2.5 Как БЫСТРО рисовать в Device Context?
С каждым DC, предназначенным для графического вывода, связан
графический объект bitmap (растровое изображение), который хранит
массив пикселей, выводимых на устройство. Для того, чтобы быстро
переместить графические данные с одного контекста на другой, можно
не повторять все действия по отрисовке, а просто скопировать
данные связанного с контекстом bitmap. Для этого даны специальные
функции быстрого копирования пикселей (BitBlt, StretchBlt).
Зачем это может быть нужно? Дело в том, что если вы часто рисуете
достаточно сложную изменяющуюся картинку средствами GDI, сами
операции рисования начинают занимать заметное для пользователя
время и возникает неприятный эффект мерцания изображения - когда
часть картинки уже перерисовалась, а часть еще осталась старой.
Для того, чтобы избежать подобного эффекта новая картинка может
создаваться в виртуальном DC в памяти, и потом быстро переносится
на экран функциями копирования bitmap.
Пример:
// hdc - дескриптор контекста устройства для вывода
// iWidth, iHeight - размеры окна вывода
HDC hMemDC;
hMemDC = CreateCompatibleDC(hdc);
if ¯(hMemDC)
{
// рисуем все что требуется
...
// быстро копируем результат отрисовки
BitBlt(hdc, 0, 0, iWidth, iHeight, hMemDC, 0, 0, SRCCOPY);
// освобождаем контекст
DeleteDC(hMemDC);
}
else
{
// обработка ошибки получения контекста
}
2.6 Как загрузить и вывести на экран изображение?
Пользуясь базовыми функциями WinAPI, это к сожалению не так-то
просто. Никаких встроенных функций по загрузке изображения из bmp
файла не предусмотрено, поэтому требуется самостоятельно писать
функцию загрузки. Эта функциональность уже тысячу раз реализована,
одна из реализаций предлагается вам в примере, который прилагается
к данному тексту.
В принципе, если вы не собираетесь выводить загружаемое растровое
изображение на экран (а, скажем, только обрабатывать и сохранять),
то его можно хранить в совершенно произвольных собственных
структурах данных. Однако, если вы хотите иметь возможность быстро
вывести ваше изображение на экран, или рисовать в нем средствами
GDI, придется хранить его определенным образом. Потребуется
создать графический объект bitmap, соответствующий параметрам
файла bmp, и загрузить в него данные из файла (пиксели). Пример,
как это сделать, содержится в классе DSimpleBitmap в примере
MFC_GML3.
Для того, чтобы уметь быстро выводить загруженное изображение на
экран, требуется сделать следующее - с помощью функции
SelectObject привязать к созданному заранее memory DC загруженный
bitmap (вместо default bitmap, создающегося вместе с контекстом) и
затем функцией копирования битов вывести в дисплейный контекст,
связанный с вашим окном.
Пример:
// hdc - дескриптор контекста устройства для вывода
// iWidth, iHeight - размеры окна вывода
// hBitmap - дескриптор изображения для отрисовки
HDC hMemDC;
HBITMAP hOldBitmap;
hMemDC = CreateCompatibleDC(hdc);
if (hMemDC)
{
// рисуем все что требуется
hOldBitmap = SelectObject(hMemDC, hBitmap);
// копируем биты
BitBlt(hdc, 0, 0, iWidth, iHeight, hMemDC, 0, 0, SRCCOPY);
// возвращаем старый bitmap
SelectObject(hMemDC, hOldBitmap);
// освобождаем контекст
DeleteDC(hMemDC);
}
else
{
// обработка ошибки получения контекста
}
Не забудьте уничтожить все временные объекты, которые создавались
(в данном случае - это memory DC). Не забудьте также перед тем как
будете уничтожать memory DC, выбрать в него (через SelectObject)
объект bitmap, который был создан вместе с контекстом, в противном
случае произойдет утечка ресурсов.
2.7 Как нарисовать что-либо на изображении?
Есть как минимум два способа. Первый - это получить указатель на
пиксели растрового изображения (вариант как это сделать см. секцию
4.1) и менять их напрямую. Второй - это рисовать
на изображении с помощью функций GDI. Для реализации второго
варианта нужно создать DC, связать с ним bitmap, на котором хотите
рисовать, и затем использовать стандартные функции вывода графики.
Пример:
// hdc - дескриптор некоторого контекста устройства
// hBitmap - дескриптор изображения
HBITMAP hOldBitmap;
// связываем bitmap с контекстом
hOldBitmap = SelectObject(hdc, hBitmap);
// рисуем круг
Ellipse(hdc, 10, 10, 100, 100);
// возвращаем старый bitmap
SelectObject(hdc, hOldBitmap);
Имейте в виду, объект bitmap может быть одновременно связан только
с одним DC.
3 Надстройки над GDI
Для облегчения программирования под WinAPI было создано некоторое
количество объектно-ориентированных надстроек для него. В числе
самых распространенных - Microsoft Foundation Class Library (MFC)
от Microsoft (используемая в MS Visual Studio) и Visual Components
Library (VCL) от Borland (используемая в Delphi и C++ Builder).
Обе этих библиотеки уже достаточно пожилые, но тем не менее все
еще широко распространенные.
С появлением этих (и других) надстроек, люди крайне редко
по-прежнему программируют чисто под WinAPI (что в общем-то
понятно).
3.1 MFC надстройка над GDI
Для облегчения работы с функциями и
структурами GDI в MFC создан набор классов, являющихся обертками
для WinAPI структур и дескрипторов.
К их числу относятся CDC, CPen, CBitmap, CFont, CBrush и т.д.
Работа с ними практически идентична работе с дескрипторами этих
объектов, но несколько удобнее.
Что значит обертками? Это значит, что CPen внутри себя содержит
HPEN (доступный как свойство класса) и просто берет на себя
некоторые заботы по его созданию, удалению и работе с ним. Похожим
образом организованы все обертки.
CDC - это абстрактный базовый класс, у которого есть несколько
реализаций - CPaintDC, CClientDC, CWindowDC, CMetaFileDC, каждая
должна использоваться в определенных ситуациях.
Работа с графическими фукнциями GDI с использованием MFC несколько
упрощается (сравните с примером в разделе 2.2):
//pDC - указатель на CDC (обертку дескриптора контекста устройства)
CPen WhitePen(PS_SOLID, 1, RGB(255, 255, 255)),
BlackPen(PS_SOLID, 1, RGB(0, 0, 0)),
*pOldPen;
CBrush BlackBrush(RGB(0, 0, 0)),
*pOldBrush;
// нарисовать белый квадрат
pOldPen = pDC->SelectObject(&WhitePen);
pDC->MoveTo(10, 10);
pDC->LineTo(100, 10);
pDC->LineTo(100, 100);
pDC->LineTo(10, 100);
pDC->LineTo(10, 10);
// нарисовать черную окружность
pDC->SelectObject(&BlackPen);
pOldBrush = pDC->SelectObject(&BlackBrush);
pDC->Ellipse(10, 10, 100, 100);
// вернуть старый объекты pen и brush в DC
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrush);
// ресурсы будут освободены при уничтожении объектов CPen
К сожалению, никаких средств для загрузки bmp файлов в CBitmap и
для простой отрисовки CBitmap в DC в MFC не предоставлено -
приходится пользоваться теми же средствами, что и при работе с
WinAPI. Обработка WM_PAINT производится практически идентично, за
исключением того, что в MFC существует специальный тип CPaintDC, в
конструктор и деструктор которого инкапсулированы (встроены)
вызовы BeginPaint/EndPaint. Обработка события выглядит следующим
образом:
void CImageView::OnPaint()
{ ¯
// Подразумевается, что это функция-член окна,
тогда this
// указывает на CWnd - обертку дескриптора данного окна
CPaintDC dc(this);
// рисуем что требуется
...
// контекст освободится сам при выходе из функции
// (при уничтожении объекта dc)
}
3.2 VCL надстройка над GDI
Visual Components Library (VCL) от Borland делает гораздо более
длинный шаг в сторону упрощения работы с графикой.
В этой библиотеке введен класс TCanvas, также являющийся оберткой
для HDC (HDC доступен через свойство Handle), но представляющий
более высокоуровневый интерфейс для работы с графикой.
Переключение режимов производится путем модификации свойств класса
TCanvas - Pen, Font, Brush, TextFlags и т.д., что делает
переключение режимов рисования значительно проще и прозрачнее и
избавляет разработчика от чехарды с
SelectObject/GetCurrentObject/DeleteObject. Операции
GetPixel/PutPixel реализованы как доступ к двумерному массиву
Pixels (что не делает работу с ними более быстрой).
Canvas связан со всеми компонентами VCL, у которых есть клиентская
часть, а также с классом TBitmap. Стандартные компоненты Windows
такие как кнопки, списки и т.д. Canvas не имеют, так как их
полностью отрисовывает Windows. Рисование на Canvas происходит
путем вызова соответствующих функций-членов.
Пример (сравните с 2.2, 3.1):
// AppForm - класс окна (TForm), в котором мы собираемся
// рисовать
// Нарисовать белый квадрат
AppForm->Canvas->Pen->Color = clWhite;
AppForm->Canvas->MoveTo(10, 10);
AppForm->Canvas->LineTo(100, 10);
AppForm->Canvas->LineTo(100, 100);
AppForm->Canvas->LineTo(10, 100);
AppForm->Canvas->LineTo(10, 10);
// Нарисовать черную окружность
AppForm->Canvas->Pen->Color = clBlack;
AppForm->Canvas->Brush->Color = clBlack;
AppForm->Canvas->Ellipse(10, 10, 100, 100);
Обработка сообщения WM_PAINT происходит без дополнительной заботы
о создании DC особым образом (CPaintDC или BeginPaint), просто
нужно работать с Canvas перерисовываемого объекта.
Быстрое копирование из Canvas в Canvas осуществляется путем
использования функции CopyRect, аналогичной BitBlt, StretchBits.
Загрузка изображения из файла и отображение на экране с
использованием VCL значительно упрощается. Растровые изображения,
иконки и метафайлы хранятся в соответствующих классах (TBitmap,
TIcon, TMetaFile) - наследниках базового класса изображений
TGraphic. Для облегчения работы с этими классами в VCL добавлен
класс-контейнер TPicture, который может работать с любым из
наследников TGraphic, реализуя функциональность
загрузки/сохранения и копирования объекта в буфер обмена
(clipboard).
В VCL существует еще один класс, облегчающий вывод графического
изображения в окно - TImage. TImage - это компонент, содержащий
некоторые свойства и параметры, задающие как именно будет
отрисовываться изображение в окне приложения. Само изображение
хранится в свойстве Picture класса TImage. Стоит иметь в виду, что
при использовании TImage VCL полностью берет на себя обработку
сообщения WM_PAINT. То есть все, что нарисовано на Canvas
компонента TImage, автоматически отображается на экране, когда это
требуется - достаточно нарисовать все что нужно один раз.
Загрузка и отображение растровой картинки с помощью TImage
показана в примере SDIApp и DelphiBasis, прилагаемых к данному
тексту.
4 Как (относительно) быстро обрабатывать изображения?
При написании фильтров для изображения требуется способ доступа к
отдельным пикселям. Самый простой способ - сделать это с помощью
функций GetPixel/SetPixel в WinAPI и MFC и с помощью двумерного
массива TCanvas->Pixels в VCL. Однако так поступать не стоит,
поскольку такой способ является чрезвычайно медленным.
4.1 Быстрый способ доступа к пикселям в GDI и MFC
При работе с функциями GDI напрямую, наиболее удобным
представляется создание объекта bitmap, к пикселям которого можно
обращаться напрямую. Делается это с помощью функции
CreateDIBSection. Одним из выходных параметров этой функции
является указатель на переменную, куда при создании bitmap будет
помещен указатель на массив пикселей - ppvBits. Запомнив этот
указатель, приложение получает прямой доступ к пикселям
изображения. Обычно использующиеся true color изображения с
глубиной цвета 24 bit хранят данные попиксельно в виде массива
троек `BGR' (каждый пиксель - три байта).
Адрес пикселя с координатами (x, y) для изображений такого типа
рассчитывается следующим образом:
ppvBits + y * iBytesPerLine + x * 3 |
| (1) |
Здесь iBytesPerLine - это длина строки изображения в байтах,
которая отнюдь не всегда равна ширине изображения, умноженной на
три. Для увеличения производительности работы с изображением
адреса начал строк выравниваются по границе процессорного слова (4
байта), поэтому если ширина, умноженная на 3, не кратна четырем,
каждая из строк дополняется несколькими дополнительными байтами .
Рассчитать длину строки в байтах можно по следующей формуле:
iBytesPerLine = (iWidth * 3 + 3) & -4; |
| (2) |
Именно таким образом быстрый доступ к пикселям изображения
реализован в классе DSimpleBitmap в примере MFC_GML3.
4.2 Быстрый способ доступа к пикселям в VCL
Для того чтобы получить прямой доступ к указателю на пикселы
изображения, хранящегося в TBitmap, нужно использовать свойство
ScanLine. Это массив указателей на строки пикселей изображения.
Доступ к пикселю с координатами (x, y) осуществляется следующим
образом:
// pBitmap - указатель на TBitmap обрабатываемого изображения
pBitmap->ScanLine[y][x * 3]
Формат хранящихся в изображении данных задается свойством
PixelFormat объекта TBitmap. Для полноцветных изображений
(PixelFormat = pf24bit) каждому пикселю соответствует три байта,
задающие интенсивности каждого из цветовых каналов - 'BGR'.
Как подобным образом реализовать фильтрацию изображения, показано
в примерах SDIApp и DelphiBasis.
Сноски:
1mailto:vvp@graphics.cs.msu.su
2Windows Application Programming Interface -
программный интерфейс системы Windows (набор функций,
предоставляемый пользовательским приложениям)
File translated from
TEX
by
TTH,
version 3.33. On 10 Feb 2004, 20:49.
|