Простое объединение PDF-файлов
Файлы
ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.
Подписка PRO — скачивайте любые файлы со скидкой до 85% из Базы знаний
Оформите подписку на компанию для решения рабочих задач
Оформить подписку и скачать решение со скидкойВы можете заказать платную доработку или адаптацию этой разработки под вашу конфигурацию на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.
Разработана на C#. Объем менее 40КБ.
Внешние программы не требуются: нет Adobe Acrobat/Reader, нет Office, нет Ghostscript,
нет сторонних PDF-библиотек (PDFsharp/iText и т. п.). Разбор и сборка PDF реализованы внутри.
Среда. Windows + .NET Framework 4.x. На Windows 10/11 он установлен по умолчанию; 1С:Предприятие 8.3 сама требует CLR 4.x, поэтому на машинах с 1С зависимость уже выполнена.
Интерфейс командной строки
MergePdf.exe /InPath <каталог> /OutPath <файл.pdf | каталог> [/Log <журнал>]
MergePdf.exe /Response <файл параметров>
MergePdf.exe <каталог> <результат> [журнал]
|
Параметр |
Алиасы |
Смысл |
|---|---|---|
|
|
|
каталог с исходными PDF |
|
|
|
файл результата или каталог |
|
|
|
файл журнала (по умолчанию |
|
|
|
файл параметров: строки |
|
|
|
краткая справка (код 2) |
Пример вызова:
MergePdf.exe /InPath "D:\Сканы\Договор" /OutPath "D:\Результат\Договор.pdf" /Log "C:\Temp\mp.log"
Правила обработки
-
Какие файлы. Все файлы с расширением
.pdfв самом каталогеInPath(регистр расширения не важен). Вложенные каталоги не обходятся. -
Порядок. «Естественная» сортировка по имени:
1.pdf,2.pdf,10.pdf(числовые группы сравниваются как числа, буквы — без учёта регистра). -
Файл результата исключается из списка исходных: можно писать результат в тот же каталог, повторный запуск не «съест» предыдущий результат.
-
OutPath. Оканчивается на.pdf— это имя файла; иначе — каталог (создаётся при необходимости), результат кладётся вmerged.pdf. -
Запись. Сначала
*.tmp, затем переименование: недописанный файл не остаётся на месте результата. -
Ошибки. Любая проблема с конкретным файлом возвращается с его именем (
Файл "х.pdf": это не PDF-файл.,Файл "у.pdf" защищён паролем ...).
Собственный PDF-движок
Сторонних PDF-библиотек нет — реализован минимальный, но «честный» набор:
-
лексер/парсер объектов PDF (словари, массивы, имена с
#xx, литеральные и hex-строки с сохранением escape-последовательностей, числа, ссылкиn g R, потоки с косвенной/Length); -
таблица ссылок в двух видах: классическая
xrefи перекрёстные потоки (xref streams, PDF 1.5+) с/W,/Indexи PNG-предиктором; -
объектные потоки (
/ObjStm, сжатые наборы объектов — обычное дело для «офисных» PDF); -
цепочки инкрементальных обновлений
/Prevи гибридные файлы/XRefStm; -
декодеры:
FlateDecode(с автоопределением zlib-заголовка),ASCIIHexDecode,ASCII85Decode,RunLengthDecode; предикторы PNG (10–15) и TIFF (2); -
восстановление структуры линейным сканированием файла, если
startxref/xref повреждены.
Как собирается результат
-
Из каждого файла выбираются страницы обходом дерева
/Pages; наследуемые атрибуты (/Resources,/MediaBox,/CropBox,/Rotate) «протаскиваются» в сами страницы. -
Объекты всех документов перенумеровываются в один диапазон, все косвенные ссылки пересчитываются (ссылка на отсутствующий объект становится
null). -
Служебные объекты (
/Type /XRef,/Type /ObjStm) не переносятся — они не нужны вне исходного файла. -
Строится единый узел
/Pages: у каждой страницы переписывается/Parent, при отсутствии/MediaBoxподставляется A4 (595×842). -
Новый каталог (
/Catalog) и/Infoиз первого файла; содержимое потоков копируется байт в байт с пересчётом/Length. -
Файл пишется с заголовком
%PDF-1.7, классической таблицейxrefи трейлером;
Исполнение, память, производительность
-
Один поток, без фоновых задач и таймеров; работа заканчивается вместе с процессом.
-
Каждый исходный файл читается в память целиком (
File.ReadAllBytes), затем строится граф объектов. Пиковая память ≈ сумма размеров файлов + накладные расходы объектов, поэтому гигабайтные сканы требуют соответствующего объёма ОЗУ. -
Замеры на машине разработки (Windows 11, i5-12400F):
|
Сценарий |
Время |
|---|---|
|
«Пустой» запуск ( |
≈1,01 с |
|
2 файла, 81 КБ → 3 страницы |
≈1,01 с |
|
3 файла, 52,1 МБ → 78 страниц |
≈1,01 с |
|
Собственно объединение 52,1 МБ / 78 страниц (в процессе, через |
0,11 с |
Вывод: время определяется запуском процесса, а не обработкой. Для сравнения, пустая .NET-программа на той же машине стартует за 1,01–1,05 с — столько же, то есть накладные расходы создаёт инициализация CLR и проверка файла антивирусом, а не код утилиты.
Ограничения
-
PDF, защищённые паролем (в том числе с пустым паролем пользователя), не объединяются — расшифровка не реализована, возвращается ошибка с именем файла.
-
Не поддерживаются фильтры
LZWDecode,JBIG2Decode,JPXDecodeпри декодировании. Декодирование нужно только для перекрёстных потоков и объектных потоков (там практически всегдаFlateDecode); содержимое страниц и изображения копируется байт в байт вместе с исходными фильтрами, поэтому сканы сDCTDecode/JBIG2Decode/JPXDecodeобъединяются нормально. -
Закладки (
/Outlines) и интерактивные формы (/AcroForm) не переносятся; страницы, содержимое, шрифты, изображения и ссылки-аннотации — переносятся. -
Метаданные документа (
/Info) берутся из первого файла. -
Вложенные каталоги не обходятся.
-
Нет слияния «по списку файлов» и настройки порядка — только каталог и естественная сортировка по именам.
Поставка
-
Утилита (EXE)
-
Пример обработки на обычных формах (EPF), утилита содержится в макете обработки. Код обработки открыт.
Пример вызова из 1С:
////////////////////////////////////////////////////////////////////////////////
// MergePdf — объединение PDF-файлов каталога в один файл.
//
// Возвращает текст журнала, например:
// OK: файлов 3, страниц 12, файл: D:\Результат\Договор.pdf
// При ошибке — вызывает исключение с текстом причины.
//
// Как работает:
// 1. во временном каталоге разворачивается утилита MergePdf.exe из макета «Утилита»;
// 2. параметры записываются в файл params.txt, утилита запускается как
// MergePdf.exe /Response "<params.txt>"
// поэтому кавычки, пробелы и завершающие «\» в путях не могут сломать разбор
// командной строки;
// 3. читается журнал работы (при ошибке в нём есть «Данные вызова»);
// 4. временный каталог удаляется — и при успехе, и при ошибке.
//
// Командное окно не появляется: утилита собрана как приложение Windows.
// Коды возврата утилиты: 0 — успех, 1 — ошибка объединения, 2 — неверные параметры.
//
// Используются только методы, доступные в клиентском контексте (проверено на 8.3.27).
////////////////////////////////////////////////////////////////////////////////
Функция ОбъединитьPDF(InPath, OutPath) Экспорт
// Пустые параметры отсекаем сразу: иначе ошибка будет выглядеть как проблема разбора.
Если Не ЗначениеЗаполнено(InPath) Тогда
ВызватьИсключение "MergePdf: не задан InPath (каталог с PDF-файлами).";
КонецЕсли;
Если Не ЗначениеЗаполнено(OutPath) Тогда
ВызватьИсключение "MergePdf: не задан OutPath (путь результата).";
КонецЕсли;
КаталогВременных = КаталогВременныхФайлов();
Если Прав(КаталогВременных, 1) <> "\" Тогда
КаталогВременных = КаталогВременных + "\";
КонецЕсли;
ВременныйКаталог = КаталогВременных + "MergePdf_" + Строка(Новый УникальныйИдентификатор());
Попытка
СоздатьКаталог(ВременныйКаталог);
// 1. Извлекаем утилиту из макета во временный каталог.
ПутьКУтилите = ВременныйКаталог + "\MergePdf.exe";
Макет = ПолучитьМакет("Утилита");
Если ТипЗнч(Макет) = Тип("ДвоичныеДанные") Тогда
Макет.Записать(ПутьКУтилите);
Иначе
ВызватьИсключение "Макет ""Утилита"" должен содержать двоичные данные.";
КонецЕсли;
// 2. Параметры — отдельным файлом: в командной строке нет путей пользователя.
ФайлЖурнала = ВременныйКаталог + "\merge.log";
ФайлПараметров = ВременныйКаталог + "\params.txt";
Запись = Новый ЗаписьТекста(ФайлПараметров, КодировкаТекста.UTF8);
Запись.ЗаписатьСтроку("InPath=" + Строка(InPath));
Запись.ЗаписатьСтроку("OutPath=" + Строка(OutPath));
Запись.ЗаписатьСтроку("Log=" + ФайлЖурнала);
Запись.Закрыть();
Команда = """" + ПутьКУтилите + """ /Response """ + ФайлПараметров + """";
КодВозврата = 0;
ЗапуститьПриложение(Команда, ВременныйКаталог, Истина, КодВозврата);
// 3. Читаем то, что записала утилита: отчёт об успехе или причину ошибки.
ТекстЖурнала = "";
Файл = Новый Файл(ФайлЖурнала);
Если Файл.Существует() Тогда
Чтение = Новый ЧтениеТекста(ФайлЖурнала, КодировкаТекста.UTF8);
ТекстЖурнала = Чтение.Прочитать();
Чтение.Закрыть();
ТекстЖурнала = СокрЛП(СтрЗаменить(ТекстЖурнала, Символ(65279), "")); // убрать BOM
КонецЕсли;
Если КодВозврата <> 0 Тогда
Если Не ЗначениеЗаполнено(ТекстЖурнала) Тогда
ТекстЖурнала = "Не удалось объединить PDF (код " + КодВозврата + "). Команда: " + Команда;
КонецЕсли;
ВызватьИсключение ТекстЖурнала;
КонецЕсли;
Результат = ТекстЖурнала;
Исключение
Попытка
УдалитьФайлы(ВременныйКаталог);
Исключение
// Временный каталог уберёт система — на результат это не влияет.
КонецПопытки;
ВызватьИсключение;
КонецПопытки;
Попытка
УдалитьФайлы(ВременныйКаталог);
Исключение
// Временный каталог уберёт система — на результат это не влияет.
КонецПопытки;
Возврат Результат;
КонецФункции
Проверено на следующих конфигурациях и релизах:
- Управление производственным предприятием, редакция 1.3, релизы 1.3.286.2
Вступайте в нашу телеграмм-группу Инфостарт