У электронной таблицы два слоя идентичности. Есть сетка ячеек, а есть метаданные документа, едущие рядом: заголовок, автор, компания, ключевые слова, отметки времени. Excel никогда не показывает этот второй слой в сетке, и всё же именно его индексирует поиск Windows, именно его читает SharePoint, чтобы озаглавить документ, и именно по нему раскладывает бумаги система управления записями. Когда сгенерированная книга наследует Author и Title от шаблона, из которого её собрали, каждая система ниже по течению записывает автором четырёх тысяч выписок клиентов проектировщика шаблона. Метаданные неверны нигде и запрашиваются везде
HotXLS выводит этот слой обычными свойствами уровня книги в обоих своих движках: фасаде BIFF для .xls и фасаде OOXML для .xlsx. Вы читаете поле после открытия файла и пишете поле перед сохранением. Библиотека сама решает, в какой физический контейнер ляжет значение. Что стоит понять до написания генератора, так это какие поля реально поддерживает каждый формат, где эти поля физически живут и какое единственное правило-вентиль определяет, запишет ли .xlsx метаданные вообще
Два формата, две модели хранения
Причина, по которой табличной библиотеке нужны две реализации метаданных, и причина, по которой недоделанные инструменты штампуют один формат верно и забывают про другой, в том, что .xls и .xlsx держат свои свойства в никак не связанных местах. Книга BIFF пишет их в потоки составного файла OLE, прежде всего в набор свойств SummaryInformation, который старше самого Excel, рядом с записью WRITEACCESS внутри потока, называющей того, кто сохранял файл последним. Книга OOXML держит их частями XML внутри zip-пакета, разделёнными по назначению: docProps/core.xml хранит поля Dublin Core (заголовок, создатель, тема, ключевые слова, даты), а docProps/app.xml — поля уровня приложения вроде компании и создавшей программы, согласно ECMA-376 Part 1
HotXLS уплощает обе эти модели хранения в прямые свойства объекта книги. Вы никогда не открываете поток набора свойств и не правите часть XML вручную. Вы присваиваете книге строки и даты, а нужный контейнер материализуется под тот формат, в котором вы сохраняете
Штамповка сгенерированных книг по деловой записи
На стороне XLSX TXLSXWorkbook предоставляет Title, Subject, Author, Keywords, Description, Category, LastModifiedBy, Company, Application и AppVersion строками, плюс Created и Modified значениями TDateTime, где ноль означает «не задано». Правило, закрывающее дыру наследования, умещается в одно предложение: присваивайте каждое поле при каждом прогоне, беря значения из деловой записи, а не доверяя тому, что случайно нёс шаблон
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('statement-template.xlsx') <> 1 then
raise Exception.Create('Template not available');
// Перезаписываем каждое поле: всё нетронутое
// наследуется от того, кто проектировал шаблон.
Book.Title := 'Account Statement 2026-06 / ACME Corp';
Book.Subject := 'Monthly account statement';
Book.Author := 'Billing Service 4.2';
Book.LastModifiedBy := 'Billing Service 4.2';
Book.Company := 'Northwind Financial';
Book.Category := 'Customer Delivery';
Book.Keywords := 'statement;billing;2026-06;acct-10024';
Book.Description := 'Generated document - manual edits are not retained';
Book.Created := Now;
Book.Modified := Now;
Book.SaveAs('statement-10024.xlsx');
finally
Book.Free;
end;
end;
Поле Keywords окупает больше размышлений, чем ему обычно достаётся. Поисковая инфраструктура индексирует его дословно — и поиск Windows, и SharePoint, и большинство систем управления документами, — поэтому соглашение с разделением точкой с запятой, несущее номер счёта и период, превращает каждую доставленную книгу в находимую запись без единого обращения к базе. Тот же охват и есть подвох. Свойства путешествуют с каждой копией файла далеко за пределы прав доступа той системы, что их записала, поэтому персональным данным там не место
Пара отметок времени несёт семантику, которую стоит закрепить политикой, а не оставлять на привычку. Created должно отмечать момент, когда ваш конвейер породил документ, и дальше стоять замороженным. Modified — то поле, которое Excel обновляет всякий раз, когда получатель сохраняет файл, поэтому расхождение между ними после доставки служит прямым свидетельством, что кто-то правил книгу ниже по течению, а это решает не один спор о том, чьи же числа несёт пересланная таблица. Одна ловушка прячется в незаданном состоянии: это буквальный ноль, а не исключение и не null, поэтому код аудита обязан явно проверять ноль. Отформатируйте незаданный TDateTime без такой страховки — и ваши журналы наполнятся уверенно неверной датой из декабря 1899 года
DocPropsTouched: книга, которая уходит без docProps
Писатель свойств XLSX закрыт вентилем — флагом только для чтения DocPropsTouched. Книга, в которой не присваивалось ни одно свойство, вообще не порождает частей docProps; HotXLS отказывается писать пустой скелет метаданных. Поведение опрятное, и у него два следствия, вокруг которых стоит проектировать
Приёмный код на стороне потребителя не должен считать, что core.xml есть в каждом пакете. Инструмент, жёстко его требующий, отвергнет совершенно корректные минимальные файлы. А если ваша позиция по комплаенсу требует, чтобы каждый исходящий документ нёс хотя бы идентичность генератора, это требование становится кодом, а не свойством формата: присваивайте Application и Author безусловно на пути сохранения, поскольку нетронутая книга совершенно законна по спецификации, но тихо нарушает вашу политику
Поверхность устаревшего XLS и ловушка Comments
Фасад BIFF несёт более старый и меньший набор полей: Title, Subject, Author, Keywords, Comments, Company и Manager, плюс LastSavedBy, псевдоним UserName, который пишет запись WRITEACCESS, показываемую Excel, когда файл заблокирован другим пользователем
var
Legacy: IXLSWorkbook; // интерфейс с подсчётом ссылок: Free вручную не нужен
begin
Legacy := TXLSWorkbook.Create;
if Legacy.Open('archive-1999.xls') <= 0 then
raise Exception.Create('Cannot open archive file');
Legacy.Title := 'FY1999 ledger (migrated copy)';
Legacy.Author := 'Archive Migration Batch';
Legacy.Company := 'Northwind Financial';
Legacy.Comments := 'Migrated 2026-06-11; source retained in cold storage';
Legacy.LastSavedBy := 'migration-svc'; // запись BIFF WRITEACCESS
Legacy.SaveAs('archive-1999-stamped.xls');
end;
Одно совпадение имён порождает повторяющуюся путаницу. Свойство Comments уровня документа здесь — это свободная текстовая пометка, показываемая в диалоге свойств файла. Оно не имеет ничего общего с примечаниями к ячейкам, которые являются объектами слоя рисунков, прикреплёнными к диапазонам через совершенно отдельный API. Ревью кода, принявшее «мы уже пишем Comments» без проверки, о каком из двух речь, приняло утверждение не о той возможности, и случается это чаще, чем можно предположить по общему имени. У этих двух общего восемь букв и ни одного байта хранилища
Чтение метаданных на приёмке и пробел в прощупывании
Чтение симметрично. После Open те же свойства возвращаются заполненными из файла, что превращает аудит метаданных входящих книг в короткий цикл
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) = 1 then
begin
Writeln(Format('%s | title="%s" author="%s" created=%s',
[ExtractFileName(FileName), Book.Title, Book.Author,
FormatDateTime('yyyy-mm-dd', Book.Created)]));
if Book.Created = 0 then
Writeln(' no creation date recorded');
end;
finally
Book.Free;
end;
end;
Планируйте при этом с оглядкой на одно ограничение. Пробы только по свойствам не существует. GetSheetNames умеет перечислить листы, не загружая книгу, но чтение Title или Author означает полный Open, поэтому сортировка по метаданным в большом архиве платит полную цену разбора на каждом файле. На стороне BIFF эту цену для аудитов только на чтение можно урезать, установив _DisableGraphics в true перед открытием, что напрочь пропускает слой рисунков. Это подходит циклу, который лишь читает свойства и статистику ячеек, и это ровно неверно в тот миг, когда тот же экземпляр может сохранить файл, потому что пропущенное содержимое рисунков было бы потеряно. Когда предварительно отфильтровать набор способна одна лишь структура листов — очевидный кандидат на пропуск здесь выгрузки в один лист, — дешёвые приёмы из нашей статьи о перечислении листов и лёгкой инспекции сокращают число файлов, доходящих до дорогого прохода. А в массовых задачах штамповки, где тысячи результатов записываются, а не изучаются, шаблоны пропускной способности на стороне записи из нашей статьи о потоковой записи для пакетных заданий переносятся без изменений, поскольку присваивание свойств не добавляет ко времени сохранения ничего измеримого
Переход между форматами и удержание утечки
Внутри одного фасада свойства проходят обратный цикл чисто: откройте .xlsx, отредактируйте, сохраните — и набор вернётся целым. Переход между форматами — то место, где предположение о равенстве ломается, потому что наборы полей BIFF и OOXML не совпадают один к одному. У BIFF есть Manager и нет отметок времени; у OOXML есть Category, Description и пара Created/Modified. Конвертер, копирующий вслепую, теряет всё, чего формат-получатель вместить не может, поэтому сопоставляйте поля явно и внесите это сопоставление в свой чек-лист конвертации рядом со всем остальным, что переезда не переживает
Утечка, которую открывает наследование от шаблона, течёт в обратную сторону: наружу уходят сведения, которые вы отправлять не собирались. Имена авторов, внутренние метки проектов, припаркованные в ключевых словах, черновой заголовок, который никто не согласовывал. Дисциплина «перезаписать всё» из генератора выше и есть вся защита, и проверять её стоит так, как это сделал бы посторонний: открыв диалог свойств, доступный любому клиенту, или распаковав .xlsx и прочитав docProps/core.xml прямо из пакета. То, что вы там увидите, ровно то же самое видит каждый индексатор ниже по течению
Эта видимость ниже по течению и есть причина, по которой несколько полей заслуживают большей заботы, чем остальные. Title, Author, Keywords (всплывающие как Tags) и Comments или Description несут основной вес индексации в SharePoint и поиске Windows. По-настоящему уникальный для каждого документа Title, несущий период и счёт, даёт для находимости больше, чем любая схема именования папок, надстроенная сверху, и стоит он одного присваивания на сохранение
Свойства документа — самый дешёвый профессиональный лоск, который может нести сгенерированная книга, и самый частый отгружаемый дефект, когда за них никто не отвечает. Обе описанные здесь поверхности свойств принадлежат HotXLS Delphi Component, который пишет их нативно для XLS и XLSX без автоматизации Excel