Метка redaction, нарисованная без цвета /IC, может выйти из шага применения прозрачной прорехой или полым контуром, пока комментарий рецензента к ней остаётся в файле. PDFlibPas применяет аннотации Redact через ApplyRedactions: сначала уходит закрытый контент, наложение приходит из /RO или из /IC с чёрным как запасным, а словарь метки опустошается
Разметка обратима, применение — нет
ISO 32000-1 §12.5.6.23 делит redaction на опознание контента и удаление контента. На стадии опознания аннотация Redact — лишь предложение, которое пользователи ещё могут видеть, двигать и переопределять, — именно это делает возможным раунд ревью. Удаление оставлено приложению, с твёрдыми требованиями: уходит всё, что аннотация опознала, уходит сама аннотация, а у закрытой части любого изображения уничтожаются данные, потому что путь отсечения или маска изображения поверх пикселей не считаются. Ключи перечислены в Table 192 ISO 32000-1 (Table 195 в ISO 32000-2), и большинство из них описывает, как страница должна выглядеть после удаления, а не что удалять
| Ключ | Роль в спецификации | Что ApplyRedactions с ним делает |
|---|---|---|
QuadPoints | Область удаления как n четырёхугольников; при наличии заменяет Rect как область | Вычищает ограничивающую рамку каждого четырёхугольника отдельно; массив, чья длина не кратна 8, откатывается к Rect |
Rect | Область удаления, когда QuadPoints отсутствует | Используется с нормализованными переставленными углами |
IC | Заливка DeviceRGB после удаления; отсутствие означает прозрачность; игнорируется, когда есть RO | Сохраняется, когда есть; добавляется чёрным, когда отсутствует |
RO | Наложение Form XObject; старше IC, OverlayText, DA и Q | Ставится как обычный appearance и вплющивается на страницу |
OverlayText, Repeat | Подпись, нарисованная поверх области, при желании замощённая | Рисуется в сгенерированное наложение, замощается, когда Repeat истинен |
DA, Q | Шрифт, размер и цвет подписи; выключка | Размер и цвет читаются из DA, выравнивание из Q |
Создание меток через AddRedactAnnotation
TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) добавляет метку на выбранную страницу и возвращает новое число аннотаций страницы, оно же индекс метки с единицы, или 0, когда ничего не добавлено. Координаты следуют текущему началу и единицам, поэтому пример переключается на верхне-левое начало, где Top — верхний край. Вызов пишет /Rect, /IC из компонентов заливки и фиксированную /DA из /F0 10 Tf 1 1 1 rg — белый текст в 10 пунктов; непустой OverlayText добавляет /OverlayText, /Repeat и /Q 1 ради подписи по центру. Аннотации Redact — возможность PDF 1.7, поэтому более старый заголовок поднимается до 1.7. Appearance stream пока не пишется; ожидающая метка выглядит так, как её рисует приложение рецензента. При подписи, зафиксированной белым, сочетайте OverlayText с тёмной заливкой, поскольку на бледно-жёлтом подпись пропадает. Метки создаются в каждой редакции PDFlibPas
uses
System.SysUtils, PDFlibrary;
procedure MarkForReview(const SourceFile, ReviewFile: string);
var
Pdf: TPDFlib;
Index: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile(SourceFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SourceFile]);
Pdf.SetOrigin(1); // верхне-левое начало: Top — верхний край
Pdf.SetMeasurementUnits(0); // пункты
if Pdf.SelectPage(1) <> 1 then
raise Exception.Create('Page 1 not found');
// Имя свидетеля: чёрная заливка, белая подпись по центру
Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
if Index = 0 then
raise Exception.Create('Mark was not added');
// Блок счёта: замостить код освобождения по более высокому боксу
Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
// Только метки; каждый байт исходного контента всё ещё присутствует
Pdf.SaveToFile(ReviewFile);
finally
Pdf.Free;
end;
end;
QuadPoints решают область удаления; ответы сохраняют свой текст
Как только метка несёт /QuadPoints, эти четырёхугольники решают, что удалит ApplyRedactions, а Rect лишь ограничивает их для показа. Метки, сделанные инструментами выделения текста, обычно имеют один четырёхугольник на выделенную строку, поэтому изменение размера такой метки через SetAnnotRect двигает её рамку на экране и оставляет область удаления там, где она была; SetAnnotQuadPoints — тот вызов, который двигает область, а GetAnnotQuadCount возвращает 0 для метки, опирающейся на один Rect. Текст, прикреплённый к метке, следует другому правилу. Инструменты ревью держат заметку в /Contents, а автора в /T, и заметка на метке redaction склонна описывать именно то, что удаляется («страховой номер клиента, подтвердить у юристов»). ApplyRedactions опустошает собственный словарь метки, но ответы, заведённые под ней через /IRT, — отдельные аннотации, удаляемые только когда их прямоугольник пересекается с областью redaction, так что ответ, припаркованный на полях, сохраняет свой текст. Листинг ниже печатает каждую метку с числом её четырёхугольников и ответами, заведёнными под ней
procedure ListPendingMarks(Pdf: TPDFlib);
var
P, I, Parent: Integer;
begin
for P := 1 to Pdf.PageCount do
begin
if Pdf.SelectPage(P) <> 1 then
Continue;
for I := 1 to Pdf.AnnotationCount do
if Pdf.GetAnnotType(I) = 'Redact' then
Writeln(Format('p%d #%d by %s, quads=%d: %s',
[P, I, Pdf.GetAnnotTitle(I), Pdf.GetAnnotQuadCount(I),
Pdf.GetAnnotContentsEx(I)]))
else
begin
Parent := Pdf.GetAnnotReplyTo(I); // 0, когда /IRT отсутствует
if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
Writeln(Format(' reply #%d to #%d: %s',
[I, Parent, Pdf.GetAnnotContentsEx(I)]));
end;
end;
end;
Какое наложение появляется после ApplyRedactions?
Запись /RO побеждает всякий раз, когда она есть, как того требует правило старшинства из Table 192. ApplyRedactions ставит тот Form XObject как обычный appearance аннотации (/AP /N), сбрасывает любое состояние appearance и вплющивает его, так что /IC, /OverlayText, /DA и /Q на той метке не действуют. Размещение — единственная тонкая часть. Спецификация говорит, что начало наложения совпадает с нижне-левым углом Rect, тогда как вплющивание отображает BBox формы через её Matrix на Rect — то же размещение по BBox и Matrix, которое вплющивание применяет к виджетам полей форм. Наложение, чей BBox — [0 0 w h], размером с прямоугольник, с единичной Matrix, при обоих прочтениях выглядит одинаково, поэтому запрашивайте такую форму, когда ваши собственные инструменты генерируют метки /RO
Без /RO ApplyRedactions нормализует словарь и строит appearance сам. Он удаляет /C и /BS и выставляет /Border в [0 0 0]: те записи описывают ожидающую метку, обычно цветной контур, и вжигание её в финальную страницу сделало бы завершённый redaction выглядящим незавершённым. С исчезнувшей рамкой заливка покрывает весь прямоугольник. Подпись рисуется внутри сгенерированной формы — с размером и цветом из /DA, выключкой из /Q, замощением из /Repeat — а имя шрифта F0 привязано в собственных ресурсах той формы к стандартной Helvetica. Эта привязка важна, когда файлы путешествуют между инструментами: ISO 32000-1 §12.7.3.3 определяет имя шрифта в DA как запись в ресурсах по умолчанию интерактивной формы (/DR), документ без полей форм их не имеет, и другое приложение, применяющее ту же метку, может подставить запасной шрифт. PDFlibPas этот поиск пропускает, и с Helvetica подпись подходит латинице вроде REDACTED или кода освобождения
Отсутствующий /IC становится чёрным ящиком
Дефолт спецификации для отсутствующего /IC — прозрачная внутренность, и PDFlibPas от него нарочно отступает. После удаления прозрачная внутренность показывает бумагу — белую прореху там, где раньше было имя, — и та прореха выглядит как вёрстка, где никогда ничего не было. Ридер не отличит удалённое от пустого, а рецензент не подтвердит факт удаления, не сравнив извлечённый текст. Полая рамка — то, что выдаёт шаг применения, вплющивая ожидающий appearance метки с одним только контуром, — проваливается иначе: читается как предложение, на которое никто не отреагировал. Мы взвесили буквальное следование дефолту и выбрали чёрный ящик, потому что видимый знак того, что что-то изъято, — часть того, что удалённый документ сообщает своему читателю, а выбор затрагивает лишь метки, чей автор никогда не называл заливку. /IC, уже лежащий в словаре, используется при любом значении, поэтому производитель, попросивший белый, получает белый
Что исчезает до отрисовки наложения
ApplyRedactions работает страница за страницей и завершает всё удаление прежде, чем вплющить какое-либо наложение, поэтому наложение никогда не может быть накрыто собственной областью. Для каждой страницы он собирает один прямоугольник на четырёхугольник или Rect меток без четырёхугольников и вычищает каждый слой содержимого против каждого из них:
- Текст режется глиф за глифом, с измерением по шрифтам страницы и полному текстовому состоянию, а остаток частично накрытой строки остаётся на исходной позиции
- Векторные пути, инлайн-изображения и затенения, рисующие внутри области, удаляются из потока содержимого
- Form XObjects проходятся рекурсивно; форма, которую рисуют также другие страницы или размещения, подвергается redaction через приватную копию, поэтому те страницы сохраняют оригинал
- Image XObjects теряют только накрытые пиксели, перезаписанные чёрным (полная краска K для CMYK); изображения JPEG, JPEG 2000, JBIG2 и CCITT декодируются и сохраняются обратно как потоковые Flate без потерь
- В тегированном PDF
/Altи/ActualText, прикреплённые к удалённому размеченному контенту, очищаются и в содержимом страницы, и в дереве структуры - Другие аннотации, чей прямоугольник задевает область, удаляются, а поля форм, чьи виджеты сидят там, теряют свои значения
Шаг с пикселями — практическое воплощение пункта спецификации об изображениях. Чёрный прямоугольник поверх отсканированного паспорта удовлетворяет глаз и всё ещё валит §12.5.6.23, потому что исходные отсчёты декодируются, как только кто-нибудь извлечёт изображение. За RedactRegion, удаляющим контент из прямоугольника, который вы передаёте напрямую, и парным к SanitizeDocument, стоит тот же движок, без чтения каких-либо аннотаций. Когда контент ушёл, каждая метка вплющивается, а её словарь чистится до /Type и /Subtype. Вплющивание убирает аннотацию из массива /Annots страницы, но словарь остаётся объектом в документе, и сохранение, которое его запишет, вручит получателю /Contents, /RC, /T и весь остальной след ревью
Сохранение без неудалённой ревизии
Инкрементальное сохранение перечёркивает всё вышеописанное. По §7.5.6 (тот же номер в ISO 32000-1 и ISO 32000-2) инкрементальное обновление дописывает изменённые объекты в конец файла и оставляет исходные байты нетронутыми, поэтому исходная ревизия — с содержимым страниц, изображениями и размеченными метками — остаётся читаемой для любого, кто отрежет файл по более раннему %%EOF; ExtractDocumentRevision в издании PDFlibPas Small Business делает это одним вызовом. После ApplyRedactions пишите новый файл через SaveToFile, никогда SaveChanges, который дописывает. Перед сохранением GarbageCollectObjects выметает таблицу объектов и бросает объекты, на которые больше никто не ссылается, — например appearance-потоки, которые метки несли до шага применения; полное сохранение пропускает этот вымет, если его не включил SetGarbageCollect или поле GarbageCollect записи TPDFlibSaveOptions, которую вы передаёте в SaveToFileOptions. Метаданные — оставшаяся прореха. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) ослепляет Author, Subject, Keywords, Creator и Producer и удаляет поток XMP, оставляя Title на вашу проверку. Избегайте здесь SANITIZE_ALL: он включает SANITIZE_ANNOTATIONS и удаляет каждую аннотацию, оставшуюся в документе, включая заметки, которые вы намеревались сохранить
ApplyRedactions, как и RedactRegion, доступен в редакции Enterprise. На простом TPDFlib он возвращает 0 и выставляет LastErrorCode в PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), поэтому релизный шаг использует TPDFlibEnterprise из юнита PDFlibEnterprise
uses
System.SysUtils, PDFlibrary, PDFlibEnterprise;
procedure ApplyAndRelease(const ReviewFile, ReleaseFile: string);
var
Pdf: TPDFlibEnterprise;
Applied, Orphans: Integer;
begin
Pdf := TPDFlibEnterprise.Create;
try
if Pdf.LoadFromFile(ReviewFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [ReviewFile]);
Applied := Pdf.ApplyRedactions; // каждая метка Redact на каждой странице
if Applied = 0 then
raise Exception.CreateFmt('No marks applied (error %d)',
[Pdf.LastErrorCode]);
// Info и XMP всё ещё могут называть то, чего страницы больше не показывают
Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
Orphans := Pdf.GarbageCollectObjects;
// Полная перезапись в новый файл; никогда SaveChanges после redaction
if Pdf.SaveToFile(ReleaseFile) <> 1 then
raise Exception.Create('Save failed');
Writeln(Format('%d marks applied, %d unreferenced objects dropped',
[Applied, Orphans]));
finally
Pdf.Free;
end;
end;
Ожидается: ApplyRedactions возвращает число меток Redact, которые он вплющил по всем страницам, — 2 для ревизионной копии, написанной MarkForReview, — а GarbageCollectObjects возвращает, сколько нессылаемых объектов вымет бросил
Какой бы инструмент ни нарисовал метку, её записи /RO и /IC решают, что получатель видит на странице, а шаг применения в PDFlibPas Enterprise for Delphi решает, что исчезло под ними