Конвертация файлов в PDF
Конвертация файлов в PDF¶
Рассмотрим задачу по конвертации файлов офисных форматов (doc, docx, xls, xlsx и др.) в файлы pdf.
Конвертация файла выполняется посредством создания и отслеживания отдельных процессов unoconv и LibreOffice. Хостинг процессов и управление их жизненным циклом выполняется фоновыми службами внутри веб-сервиса jinni. Запросы к jinni могут формироваться, основным сервисом web, так и плагином FileConverterPlugin сервиса chronos. Таким образом для конвертации файлов необходимо иметь установленные и запущенные сервисы chronos и jinni.
Из основного сервиса web можно вызвать API, которое отдаст операцию и опционально дождётся её завершения. При конвертации по умолчанию используются кэш файлов, и если запрошенный файл уже есть в кэше, то вызов API конвертации будет на 100% синхронный (мгновенный), в этом сценарии конвертация файла не выполняется.
Конвертация с ожиданием результата¶
Для выполнения конвертации файла с ожиданием результата необходимо:
-
Получить зависимость
Tessa.FileConverters.IFileConverterиз Unity-контейнера, в котором требуется инициировать конвертацию вpdf. -
Вызвать метод-расширение для создания объекта-запроса на конвертацию (не создаёт операций):
fileConverter.GetRequestOrThrowAsync(eventName, FileConverterFormat.Pdf, versionRowID), гдеeventName- строковая константа вашего события (например,"АлиасВашегоСобытия"илиFileConverterEventNames.Unknown, если событие неизвестно). -
В полученном запросе можно установить флаги с настройками конвертации файлов (см. описание флагов в
Tessa.FileConverters.FileConverterRequestFlags):request.Flags = .... -
Вызвать метод
ConvertFileAsync, который получает запрос на конвертацию, созданный методомGetRequestOrThrowAsync, и возвращает результат сразу же, как только операция будет обработана. -
Вызвать метод
GetStreamOrThrowAsyncдля полученного результата, чтобы получить содержимое сконвертированного файла:response.GetStreamOrThrowAsync(cancellationToken)).
// требуется только идентификатор версии файла
Guid versionRowID = ...;
// обычно запрос из Unity выполняют через конструктор
var fileConverter = unityContainer.Resolve<IFileConverter>();
var request = await fileConverter.GetRequestOrThrowAsync(FileConverterEventNames.Unknown, FileConverterFormat.Pdf, versionRowID, info: null, cancellationToken);
var response = await fileConverter.ConvertFileAsync(request, cancellationToken);
await using (var stream = await response.GetStreamOrThrowAsync(cancellationToken))
{
// stream содержит сконвертированный в PDF файл, копируем его в карточку, выгружаем и др.
}
Конвертация без ожидания результата¶
Есть также механизмы для полностью асинхронной конвертации, при этом не задействует кэш файлов. Для выполнения конвертации файла с ожиданием результата необходимо:
-
Получить зависимость
Tessa.FileConverters.IFileConverterиз Unity-контейнера, в котором требуется инициировать конвертацию вpdf; -
Вызвать метод-расширение для создания объекта-запроса на конвертацию (не создаёт операций):
fileConverter.GetRequestOrThrowAsync(eventName, FileConverterFormat.Pdf, versionRowID), гдеeventName- строковая константа вашего события (например,"АлиасВашегоСобытия"илиFileConverterEventNames.Unknown, если событие неизвестно). -
В полученном запросе установите флаги с настройками конвертации файлов для отключения кэша (см. описание флагов в
Tessa.FileConverters.FileConverterRequestFlags):IgnoreCacheBeforeConversion,DoNotWaitResult,DoNotCacheResult,WithoutResponse. -
Напишите расширение-наследник
FileConverterExtension, которое будет вызвано из плагинаchronosпри завершении конвертации. Регистрация расширения похожа на обычную регистрацию, используется интерфейсIFileConverterExtension, методы.WithOrder(AfterPlatform)и.WhenFileConverterEventNames("АлиасВашегоСобытия"). -
В расширении получите контент сконвертированного файла и его размер из контекста и обработайте его необходимым образом:
context.GetOutputContentAsync(cancellationToken).
В этом случае файл будет сконвертирован, помещён в контекст IFileConverterContext. Сразу после успешной конвертации операция удаляется из “активных операций”. Далее будет вызвано расширение IFileConverterExtension на указанное событие. Расширение получает файл из своего контекста и обрабатывает должным образом. Т.е. это сценарий, когда надо сконвертировать, выполнить постобработку и “забыть” об операции до момента её завершения, при этом конвертация может выполняться неопределённо длительное время.
Большой плюс этого подхода - отсутствует поток, ожидающий окончания конвертации и постоянно выполняющий проверки статуса операции. Этот подход несколько сложнее подхода с ожиданием операции, поскольку требует дополнительного расширения. Также не используется кэш, т.е. повторная конвертация того же файла будет выполняться заново.
Important
Расширения будут выполнены только если в запросе на конвертацию файла не был установлен флаг ExecutesSynchronously.
Расширение, выполняемое в веб-сервисе (например, CardStoreExtension), выглядит следующим образом:
// требуется только идентификатор версии файла
Guid versionRowID = ...;
// обычно запрос из Unity выполняют через конструктор
var fileConverter = unityContainer.Resolve<IFileConverter>();
var request = await fileConverter.GetRequestOrThrowAsync(FileConverterEventNames.Unknown, FileConverterFormat.Pdf, versionRowID, info: null, cancellationToken);
request.Flags = request.Flags
| FileConverterRequestFlags.IgnoreCacheBeforeConversion
| FileConverterRequestFlags.DoNotWaitResult
| FileConverterRequestFlags.DoNotCacheResult
| FileConverterRequestFlags.WithoutResponse;
// любую дополнительную (сериализуемую) информацию можно передать через Info
request.Info["CardID"] = Guid.NewGuid();
await fileConverter.ConvertFileAsync(request, cancellationToken);
Расширение для обработки результатов конвертации располагается в сборке Tessa.Extensions.Server. В зависимости от флага ExecutesSynchronously расширение может быть выполнено как в основном сервисе web, так и в сервисе chronos, когда конвертация будет завершена. Поэтому не забудьте также обновить файл со сборкой Tessa.Extensions.Server.dll в папке расширений для chronos.
public sealed class MyFileConverterExtension : FileConverterExtension
{
public override async Task AfterRequest(IFileConverterContext context)
{
if (!context.RequestIsSuccessful)
{
return;
}
// переданная дополнительная информация доступна из контекста
var cardID = context.Request.Info.TryGet<Guid?>("CardID");
var (stream, length) = await context.GetOutputContentAsync(context.CancellationToken);
await using (stream)
{
// stream содержит сконвертированный в PDF файл, копируем его в карточку, выгружаем и др.
}
}
}
Регистрация расширения в классе Registrator:
extensionContainer
.RegisterExtension<IFileConverterExtension, MyFileConverterExtension>(x => x
.WithOrder(ExtensionStage.AfterPlatform, 1)
.WhenFileConverterEventNames("MyCustomConversionToPdf"))
;
Если всё же требуется сохранить результат конвертации в кэш, то не устанавливайте в запросе на конвертацию флаг DoNotCacheResult и модифицируйте расширение.
// Получение контента сконвертированного файла
var (stream, length) = await context.GetOutputContentAsync(context.CancellationToken);
await using (stream)
{
// Обработка stream необходимым образом и запись его во временный файл ...
var modifiedConvertedTempFile = TempFile.Acquire("MyFile");
// Так как результат может быть получен только один раз, то необходимо переопределить делегат для получения результата и вернуть другой поток
var length = new FileInfo(modifiedConvertedTempFile.Path).Length;
var stream = FileHelper.OpenRead(modifiedConvertedTempFile.Path);
context.GetOutputContentAsync = _ => new ValueTask(stream, length);
// Если необходимо выполнить какой-либо метод обратного вызова при завершении конвертации, то его можно добавить в context
context.FinalizationQueue.Enqueue(_ => modifiedConvertedTempFile.Dispose())
}