Технічна стаття

HotPDF у Free Pascal: Deflate, AES і межі кодеків

HotPDF компілюється та працює під Free Pascal 3.2.2 з Lazarus, і чесне резюме того порту — два речення. Створення, завантаження, збереження, стискання, розстискання, шифрування та розшифрування документів усе працює на бекендах лише з Pascal, тож застосунок Lazarus може створювати та споживати справжній PDF без жодної залежності від C. Необов'язкові власні кодеки зображень — ні, бо наперед зібрані об'єкти Win64 використовують варіант COFF, який не може споживати жоден компонувальник Free Pascal, тож на тому інструментарії точки входу розв'язуються в заглушки, що безпечно відмовляють

Карта можливостей HotPDF у Free Pascal: працюючі стиснення Deflate, AES і документні бекенди Pascal поруч із заглушками кодеків зображень, що безпечно відмовляють
Функції документів, стискання та шифрування працюють на бекендах лише з Pascal, тоді як власні кодеки зображень розв'язуються в заглушки, що безпечно відмовляють

Шлях від «компілюється» до «працює» потребував конкретного набору виправлень, і кожне з них — пастка, що знайде будь-яку іншу кодову базу Delphi, яка рухається на Free Pascal. Їх варто записати в тому порядку, в якому вони боліли

Чому те, що модуль компілюється, нічого не доводить?

Бо модуль Pascal може посилатися на символ, який ніколи не зробить нічого корисного, і все ж задовольнити компілятор. У момент, коли всі 113 модулів бібліотеки зібралися чисто під Free Pascal, обробники контейнерів архівів справді працювали, перевірені димовим тестом, що відкрив CBZ і перетворив його на PDF. Сплощування форм XFA взагалі не працювало, бо сплощування має розстиснути стиснутий потік пакета /XFA, а точка входу deflate досі була заглушкою. Ніщо у виводі збірки не розрізняло тих двох випадків

Правило, що звідси випливає, коротке. Перед тим, як написати в примітках до випуску, що можливість працює на новому інструментарії, напишіть виконавчу пробу, яка вправляє цю можливість з кінця в кінець на тому інструментарії. Покриття компіляцією — це передумова, аніколи доказ. Ширша картина того, що покриває порт, — у примітках про підтримку Free Pascal і Lazarus Win64

Виняток усередині заглушки cdecl не доходить до викликача

Це заслуговує власного розділу, бо симптом такий оманливий. Модулі заглушок відкривають точки входу C так, як це робила б статична бібліотека, тож заглушка виглядає так

// Виглядає розумно. Ним не є.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

На Free Pascal для Win64 той виняток не поширюється до викликача. Немає обробника try..except, який його побачив би, бо розмотування через межу cdecl, оголошену так, не несе кадру винятку Pascal; процес завершується з кодом виходу 217. Зі сторони застосунку немає ані помилки, ані повідомлення, ані рядка журналу — просто програма, що зникає. Це строго гірше за неправильну відповідь, бо неправильну відповідь можна обробити

Чому виняток, викинутий усередині заглушки cdecl, завершує процес Free Pascal з кодом виходу 217 і як перепона на точці входу Pascal це виправляє
Кадр винятку Pascal не може розмотатися через межу cdecl, тож процес мовчки гине; виправлення ставить перепону до того, як заглушку взагалі досягнуто

Спокусливе виправлення — зробити так, щоб заглушка натомість повертала код відмови, і для inflate це правильно, бо zlib має чітко визначене повернення помилки. Загалом це неправильно: заглушка для jpeg_read_header, що повертає нуль, каже викликачу продовжити зі структурою, яку ніхто не ініціалізував. Тривке виправлення — ставити перепону на точці входу Pascal, а не всередині заглушки C-подібної форми, використовуючи ту угоду відмови, яку цей API уже має

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Відмовте до того, як заглушку досягнуто, власною угодою
  // відмови цього API, а не винятком через межу cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib — не zlib, і різниця — два класи документів

Доступна на Free Pascal реалізація deflate на Pascal обробляє два кадрування: обгортку zlib і сирий deflate. Вона не обробляє кадрування gzip, яке zlib вибирає значеннями windowBits від 16 до 31, і не обробляє режим автоматичного виявлення, який вибирають значення від 32 до 47. HotPDF потребує обидва. Безпечний шлях імпорту SVG просить 31, а завантажувач має драбину резервів, що просить 47, коли кадрування потоку неоднозначне. Пропустіть будь-що з цього — і ціла родина документів перестає відкриватися з помилкою декодування, що вказує на потік, а не на відсутнє кадрування

Покриття windowBits у paszlib проти zlib: діапазони кадрування gzip і авто-виявлення відсутні для імпорту SVG та резерву завантажувача HotPDF
paszlib обробляє обгортку zlib і сирий deflate, але HotPDF потрібні ще windowBits 31 і 47, тож шар сумісності мусить сам постачати кадрування gzip

Є друга, гостріша несумісність. Запис z_stream, який оголошує paszlib, не має такої розкладки пам'яті, як C: його поле msg — короткий рядок, а не вказівник, а total_in та total_out — 64-бітові там, де ABI C має машинні слова. Запис викликача тому не можна передавати прямо. Робочий устрій — тримати стан paszlib за вказівником state, який публічний запис уже резервує, і копіювати публічні поля туди й назад навколо кожного виклику. CRC gzip та восьмибайтовий кінцевик довжини враховані в тому самому шарі сумісності, що й природне для них місце, бо він уже володіє рішенням про кадрування

Передавання динамічного масиву в нетипізований параметр var

Це вада, яка найімовірніше сидить у вашому коді просто зараз. Коли ви передаєте динамічний масив у нетипізований параметр var, отримувач отримує адресу змінної масиву, тобто адресу вказівника, а не адресу корисного навантаження. Тож читання в нього перезаписує саму змінну та все, що сидить поруч

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Неправильно: віддає адресу змінної FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Правильно: віддає адресу першого байта даних
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

На Delphi неправильна форма часто виглядає робочою, бо те, що вона пошкоджує, — суміжна комірка стека, яку ніхто надалі не читає. На Free Pascal той самий рядок падає з помилкою сегментації при першому використанні. Те, що робить це важким для виявлення на око: статичні масиви не мають такої проблеми, бо змінна статичного масиву є власним навантаженням, тож обидві написи правильні в одному файлі залежно від оголошення за кількасот рядків звідси

Контейнери ZIP без System.Zip

Free Pascal не має еквівалента модуля zip RTL, а доступна альтернатива має і іншу поверхню API, і відсутність підтримки застарілого шифрування, яким досі користуються старіші формати контейнерів, тож маленький власний читач бібліотеки виявився коротшим за адаптацію до неї. Дві деталі формату коштували часу, і в них легко помилитися

Перша — перевірний байт заголовка шифрування. Його дванадцятий байт зазвичай є старшим байтом CRC, але коли встановлено біт 3 загального призначення, тобто розміри живуть у кінцевому дескрипторі даних і CRC ще не відомий, перевірний байт походить натомість зі старшого байта часу модифікації. Реалізуйте лише форму CRC — і кожен архів, записаний у потоковому режимі, відхилить правильний пароль. Друга — додаткове поле ZIP64: його три 64-бітові поля з'являються у фіксованому порядку, але записуються лише тоді, коли відповідне 32-бітове поле насичене, тож читання їх за фіксованими зміщеннями працює на архівах, які ви тестували, і зазнає невдачі на наступному. Розбирайте їх позиційно щодо того, які 32-бітові поля насичені

Одна зручність, варта знання: потік розстискання Free Pascal приймає другий аргумент конструктора, що пропускає заголовок zlib, — саме те, що потрібно записам ZIP, бо ті зберігають сирий deflate. Той шлях взагалі не торкається шару сумісності zlib бібліотеки, тож він не зачеплений відсутністю бекенда C

Прозорість кольорових гліфів під LCL

Читання альфа-каналу растеризованого кольорового гліфа — та сама єдина графічна деталь без прямого перекладу. Клас PNG у LCL не має аксесора рядків розгортки, що відкриває альфу, а присвоєння PNG бітовій карті відкидає її, тож кольоровий емодзі приходить повністю непрозорим і композитується з чорною коробкою позаду. Робочий маршрут — інтерфейсне зображення: створити його з PNG, а тоді читати пікселі через аксесор кольору, пам'ятаючи, що його компоненти 16-бітові й потребують зсуву вниз на вісім, щоб стати байтами. Та поверхня також використовує природний порядок рядків зверху вниз, тож інверсію Height - 1 - Y, яка потрібна коду рядків розгортки VCL, треба вилучити, а не переносити

Дві примітки про систему збірки перед поданням звіту про ваду

Повна перезбірка часом зазнає невдачі з невизначеним символом, чия назва закінчується суфіксом $crc і шістнадцятковим значенням. Той суфікс обчислюється з типів параметрів, і він не збігається, коли одна збірка компілює модуль проти двох різних версій інтерфейсу в одному проході. Повторний запуск збірки це прибирає; сигнатура не неправильна

Друге: Free Pascal 3.2.2 не має анонімних методів, тож скрізь, де бібліотека використовувала замикання для з'єднання паралельного конвеєра, збірка Free Pascal бере натомість детермінований послідовний резерв. Результат ідентичний, пропускна здатність — ні; якщо ви залежите від паралельного рендерингу сторінок, це причина лишатися на Delphi наразі, а конструкцію конвеєра описано в статті про паралельний конвеєр рендерингу. Ситуація з кодеками зображень — інше місце, де вибір інструментарію змінює можливості, а не лише швидкість, тож розгортанню Lazarus варто планувати формати зображень відповідно; поточна матриця за інструментаріями — на сторінці продукту HotPDF Delphi PDF component