Конвертация файлов в 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())
}