C# и .NET
Асинхронные методы с CancellationToken, понятные исключения, повторы при перегрузке. Клиент потокобезопасен — регистрируйте его в DI как singleton.
| Пакет | Платформы | Зависимости |
|---|---|---|
PdfApi | .NET 8; .NET Standard 2.0 — .NET Framework 4.6.1+, .NET Core 2.0+, Mono, Unity | нет |
Установка#
dotnet add package PdfApiКлюч берётся из переменной окружения PDFAPI_KEY — выпустите его в кабинете. Можно передать и явно, из секретов приложения:
export PDFAPI_KEY=pdf_live_...Первый PDF#
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, все свойства необязательные. Что они значат — в разделе Параметры страницы.
| Свойство | Пример | По умолчанию |
|---|---|---|
Landscape | true | false |
PaperWidth, PaperHeight | "8.27in", "297mm" | A4 |
MarginTop, MarginBottom, MarginLeft, MarginRight | "20mm" | "0.39in" |
WaitDelay | "2s", "500ms", до 10 секунд | "0s" |
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#
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.
Аккаунт и квота#
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 и сообщением по-русски.
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); }| Исключение | Когда |
|---|---|
AuthenticationException | 401: ключа нет, он неверный или отозван |
PermissionDeniedException | 403: почта не подтверждена или формат недоступен на тарифе |
QuotaExceededException | 402: страницы периода кончились или документ больше остатка |
InvalidRequestException | 400: неверный параметр или адрес во внутреннюю сеть (Code == "url_not_allowed") |
PayloadTooLargeException | 413: тело больше 5 МБ; наследник InvalidRequestException |
RateLimitException | 429: слишком часто; RetryAfter — через сколько можно снова |
RenderException | 502: документ не отрендерился |
RenderTimeoutException | 504: рендер дольше 30 секунд; наследник RenderException |
UnavailableException | 503: рендер временно недоступен |
ApiConnectionException | нет связи с API |
ApiTimeoutException | API не ответил за таймаут; наследник 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.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var pdf = await client.HtmlAsync(html, cancellationToken: cts.Token);Настройки клиента и DI#
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 уже в памяти — отдавайте байты, на диск сохранять незачем:
// 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/ библиотеки:
PDFAPI_KEY=pdf_live_... dotnet run --project examples/PdfApi.Examples -- invoice| Пример | Что показывает |
|---|---|
invoice | счёт из HTML-шаблона с экранированием данных |
from-url | страница по адресу, альбомная, с ожиданием скриптов |
office | docx, xlsx или pptx в PDF рядом с исходным файлом |
batch | пачка документов через SemaphoreSlim, в пределах лимита частоты |
quota | остаток страниц для cron: код выхода 2 при нехватке |
PdfApi.WebExample | ASP.NET Core: клиент в DI, токен запроса, ошибки API — в HTTP-ответы |