pdfapi

C# и .NET

Асинхронные методы с CancellationToken, понятные исключения, повторы при перегрузке. Клиент потокобезопасен — регистрируйте его в DI как singleton.

ПакетПлатформыЗависимости
PdfApi.NET 8; .NET Standard 2.0 — .NET Framework 4.6.1+, .NET Core 2.0+, Mono, Unityнет

Установка#

bash
dotnet add package PdfApi

Ключ берётся из переменной окружения PDFAPI_KEY — выпустите его в кабинете. Можно передать и явно, из секретов приложения:

bash
export PDFAPI_KEY=pdf_live_...

Первый PDF#

csharp
using PdfApi;

using var client = new PdfApiClient();   // ключ из PDFAPI_KEY

var pdf = await client.HtmlAsync("<h1>Счёт №42</h1>", new PageOptions { MarginTop = "20mm" });
await pdf.SaveAsync("invoice.pdf");

Console.WriteLine($"{pdf.Pages} стр., осталось {pdf.Quota?.Remaining}");

Методы#

Все асинхронные и принимают CancellationToken.

МетодЧто делает
HtmlAsync(html, options)HTML → PDF
UrlAsync(url, options)публичная страница по адресу → PDF. Тариф Premium и выше; адрес — полный, с https://
MarkdownAsync(text, options)Markdown → PDF. Тариф Premium и выше
OfficeAsync(path, landscape)docx, xlsx, pptx с диска → PDF. Тариф Premium и выше
OfficeAsync(bytes, filename, landscape)то же из памяти
ConvertAsync(body)POST /v1/convert с телом как есть
MeAsync()тариф, расход и остаток страниц — GET /v1/me

Параметры страницы#

PageOptions, все свойства необязательные. Что они значат — в разделе Параметры страницы.

СвойствоПримерПо умолчанию
Landscapetruefalse
PaperWidth, PaperHeight"8.27in", "297mm"A4
MarginTop, MarginBottom, MarginLeft, MarginRight"20mm""0.39in"
WaitDelay"2s", "500ms", до 10 секунд"0s"
csharp
var pdf = await client.UrlAsync("https://example.com/report",
    new PageOptions { Landscape = true, WaitDelay = "1s" });

Размер — число и единица pt, px, in, mm, cm или pc без пробела. "20" и "20 mm" вызовут ArgumentException сразу, без запроса к API.

Документы Office#

csharp
var fromDisk = await client.OfficeAsync("report.docx");                          // с диска
var fromMemory = await client.OfficeAsync(bytes, "table.xlsx", landscape: true); // из памяти — имя с расширением

Файл — до 3,7 МБ: в base64 он вырастает на треть, а тело запроса ограничено 5 МБ. Библиотека проверяет, что файл — настоящий ZIP-архив, поэтому старый .doc или PDF, переименованный в .docx, отклоняется до отправки.

Результат#

PdfResult:

  • Content — byte[] с PDF;
  • Pages — сколько страниц списано;
  • Quota — Limit, Used и Remaining после этой конвертации;
  • Save(path) и SaveAsync(path) — записать в файл.

Save пишет атомарно: сначала во временный файл рядом, потом переименовывает поверх. Оборвётся запись или кончится место — останется прежний файл, а не обрезанный PDF.

Аккаунт и квота#

csharp
var me = await client.MeAsync();
Console.WriteLine($"{me.Plan}: осталось {me.PagesRemaining} из {me.PagesLimit}, обнулится {me.PeriodEnd:u}");

Account: UserId, Plan, PeriodStart, PeriodEnd (DateTimeOffset в UTC или null), PagesUsed, PagesLimit, PagesRemaining, RequestsPerSecond, Burst. Запрос страниц не тратит. Как встроить проверку в cron — в руководстве Контроль квоты.

Ошибки#

Все ошибки API — наследники PdfApiException с полями Code, Status и сообщением по-русски.

csharp
try
{
    var pdf = await client.HtmlAsync(html);
}
catch (QuotaExceededException) { /* страницы кончились */ }
catch (RateLimitException e) { await Task.Delay(e.RetryAfter ?? TimeSpan.FromSeconds(1)); }
catch (RenderException) { /* и RenderTimeoutException тоже: разметка не отрендерилась */ }
catch (PdfApiException e) { logger.LogWarning("{Code} {Status}: {Message}", e.Code, e.Status, e.Message); }
ИсключениеКогда
AuthenticationException401: ключа нет, он неверный или отозван
PermissionDeniedException403: почта не подтверждена или формат недоступен на тарифе
QuotaExceededException402: страницы периода кончились или документ больше остатка
InvalidRequestException400: неверный параметр или адрес во внутреннюю сеть (Code == "url_not_allowed")
PayloadTooLargeException413: тело больше 5 МБ; наследник InvalidRequestException
RateLimitException429: слишком часто; RetryAfter — через сколько можно снова
RenderException502: документ не отрендерился
RenderTimeoutException504: рендер дольше 30 секунд; наследник RenderException
UnavailableException503: рендер временно недоступен
ApiConnectionExceptionнет связи с API
ApiTimeoutExceptionAPI не ответил за таймаут; наследник ApiConnectionException

Неверные аргументы — пустой HTML, адрес без https://, файл .pdf вместо .docx — ArgumentException ещё до создания Task и до запроса; лимит частоты на них не тратится. Все коды API — в разделе Ошибки и повторы.

Повторы#

Библиотека сама повторяет запрос, когда это безопасно, — по умолчанию до двух раз (MaxRetries):

  • 429 — через Retry-After, но не дольше минуты;
  • 503 или нет соединения — первый повтор примерно через 0,5 секунды, второй — примерно через 1 секунду.
Таймауты не повторяются

После таймаута результат может быть уже сохранён. Повтор с тем же Idempotency-Key и телом запроса защищает от повторного списания на обновлённом сервере. Без этого ключа повтор считается новой конвертацией. Подробнее.

Таймауты и отмена#

  • Таймаут клиента — PdfApiClientOptions.Timeout, по умолчанию 40 секунд на одну попытку; не уложились — ApiTimeoutException.
  • Для одного вызова — свой CancellationToken. Отмена через него — обычный OperationCanceledException, а не ошибка API.
csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var pdf = await client.HtmlAsync(html, cancellationToken: cts.Token);

Настройки клиента и DI#

csharp
services.AddHttpClient("pdfapi");
services.AddSingleton(sp => new PdfApiClient(
    configuration["PdfApi:Key"],                 // иначе PDFAPI_KEY
    new PdfApiClientOptions
    {
        BaseUrl = "https://pdfapi.ru/api",       // иначе PDFAPI_BASE_URL
        Timeout = TimeSpan.FromSeconds(40),
        MaxRetries = 2,
        HttpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient("pdfapi"),
    }));

Timeout не больше нуля или MaxRetries меньше нуля — ArgumentOutOfRangeException сразу. Ключа нет ни в ToString() клиента, ни в текстах ошибок.

PDF в веб-приложении#

PDF уже в памяти — отдавайте байты, на диск сохранять незачем:

csharp
// ASP.NET Core, minimal API: клиент из DI, токен запроса — клиент ушёл, запрос отменится
app.MapGet("/invoice", async (PdfApiClient pdfapi, CancellationToken ct) =>
{
    var pdf = await pdfapi.HtmlAsync("<h1>Счёт №42</h1>", cancellationToken: ct);
    return Results.File(pdf.Content, "application/pdf", "invoice.pdf");
});

Данные пользователя в HTML — только через WebUtility.HtmlEncode или Razor, иначе это HTML-инъекция в документ — см. Безопасность и данные.

Примеры#

В папке examples/ библиотеки:

bash
PDFAPI_KEY=pdf_live_... dotnet run --project examples/PdfApi.Examples -- invoice
ПримерЧто показывает
invoiceсчёт из HTML-шаблона с экранированием данных
from-urlстраница по адресу, альбомная, с ожиданием скриптов
officedocx, xlsx или pptx в PDF рядом с исходным файлом
batchпачка документов через SemaphoreSlim, в пределах лимита частоты
quotaостаток страниц для cron: код выхода 2 при нехватке
PdfApi.WebExampleASP.NET Core: клиент в DI, токен запроса, ошибки API — в HTTP-ответы