pdfapi

Go

Только стандартная библиотека. Контекст в каждом методе, ошибки — через errors.Is, клиент безопасен для горутин.

МодульВерсии GoЗависимости
github.com/Homelander-77/PDF-creator-site/sdk/go1.21+нет

Установка#

bash
go get github.com/Homelander-77/PDF-creator-site/sdk/go

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

bash
export PDFAPI_KEY=pdf_live_...

Первый PDF#

go
package main

import (
	"context"
	"fmt"
	"log"

	pdfapi "github.com/Homelander-77/PDF-creator-site/sdk/go"
)

func main() {
	ctx := context.Background()

	client, err := pdfapi.NewClient() // ключ из PDFAPI_KEY
	if err != nil {
		log.Fatal(err)
	}
	pdf, err := client.HTML(ctx, "<h1>Счёт №42</h1>", &pdfapi.PageOptions{MarginTop: "20mm"})
	if err != nil {
		log.Fatal(err)
	}
	if err := pdf.Save("invoice.pdf"); err != nil {
		log.Fatal(err)
	}
	fmt.Println(*pdf.Pages, "стр.") // сколько страниц списано
}

Создайте один клиент и переиспользуйте его во всех горутинах.

Методы#

Первый аргумент у всех — context.Context: отмена и дедлайны работают как обычно.

МетодЧто делает
HTML(ctx, html, opts)HTML → PDF
URL(ctx, url, opts)публичная страница по адресу → PDF. Тариф Premium и выше; адрес — полный, с https://
Markdown(ctx, text, opts)Markdown → PDF. Тариф Premium и выше
Office(ctx, filename, data, landscape)docx, xlsx, pptx из памяти → PDF. Тариф Premium и выше
OfficeFile(ctx, path, landscape)то же с диска
Convert(ctx, body)POST /v1/convert с телом как есть
Me(ctx)тариф, расход и остаток страниц — GET /v1/me

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

*PageOptions, все поля необязательные; nil — значения по умолчанию. Что они значат — в разделе Параметры страницы.

ПолеПримерПо умолчанию
Landscapepdfapi.Bool(true)false
PaperWidth, PaperHeight"8.27in", "297mm"A4
MarginTop, MarginBottom, MarginLeft, MarginRight"20mm""0.39in"
WaitDelay"2s", "500ms", до 10 секунд"0s"
go
pdf, err := client.URL(ctx, "https://example.com/report", &pdfapi.PageOptions{
	Landscape: pdfapi.Bool(true),
	WaitDelay: "1s",
})

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

Документы Office#

go
pdf, err := client.OfficeFile(ctx, "report.docx", nil)               // с диска
pdf, err = client.Office(ctx, "table.xlsx", data, pdfapi.Bool(true)) // из памяти — имя с расширением

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

Результат#

*Result:

  • Content []byte — байты PDF;
  • Pages *int — сколько страниц списано;
  • Quota *Quota — Limit, Used и Remaining после этой конвертации;
  • Save(path) — записать в файл;
  • WriteTo(w) — записать в io.Writer, например в http.ResponseWriter.

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

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

go
me, err := client.Me(ctx)
if err != nil {
	log.Fatal(err)
}
fmt.Println(me.Plan, me.PagesRemaining, "из", me.PagesLimit, "— обнулится", me.PeriodEnd)

*Account: UserID, Plan, PeriodStart, PeriodEnd (UTC; нулевое время — сервер не сообщил), PagesUsed, PagesLimit, PagesRemaining, RequestsPerSecond, Burst. Запрос страниц не тратит. Как встроить проверку в cron — в руководстве Контроль квоты.

Ошибки#

Ошибки API — *pdfapi.Error с полями Code, Status и Message на русском языке. Вид ошибки проверяется через errors.Is:

go
pdf, err := client.HTML(ctx, html, nil)
if err != nil {
	var apiErr *pdfapi.Error
	switch {
	case errors.Is(err, pdfapi.ErrQuotaExceeded):
		// страницы кончились
	case errors.Is(err, pdfapi.ErrRateLimit) && errors.As(err, &apiErr):
		time.Sleep(apiErr.RetryAfter)
	case errors.Is(err, pdfapi.ErrRender): // и ErrRenderTimeout тоже
		// разметка не отрендерилась
	case errors.As(err, &apiErr):
		log.Println(apiErr.Code, apiErr.Status, apiErr.Message)
	default:
		log.Println(err) // например, context.Canceled — отменили вы сами
	}
	return
}
ЗначениеКогда
ErrAuthentication401: ключа нет, он неверный или отозван
ErrPermissionDenied403: почта не подтверждена или формат недоступен на тарифе
ErrQuotaExceeded402: страницы периода кончились или документ больше остатка
ErrInvalidRequest400: неверный параметр или адрес во внутреннюю сеть (Code == "url_not_allowed")
ErrPayloadTooLarge413: тело больше 5 МБ; это и ErrInvalidRequest
ErrRateLimit429: слишком часто; RetryAfter — через сколько можно снова
ErrRender502: документ не отрендерился
ErrRenderTimeout504: рендер дольше 30 секунд; это и ErrRender
ErrUnavailable503: рендер временно недоступен
ErrConnectionнет связи с API
ErrTimeoutAPI не ответил за таймаут; это и ErrConnection
ErrInvalidArgumentневерный аргумент — до запроса, лимит частоты не тратится

Все коды API — в разделе Ошибки и повторы.

Повторы#

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

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

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

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

  • Таймаут клиента — WithTimeout, по умолчанию 40 секунд на одну попытку.
  • Для одного вызова — context.WithTimeout: действует меньший из двух.
  • Отменили контекст сами — вернётся ctx.Err(), а не ошибка API.

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

go
client, err := pdfapi.NewClient(
	pdfapi.WithAPIKey("pdf_live_..."),           // иначе PDFAPI_KEY
	pdfapi.WithBaseURL("https://pdfapi.ru/api"), // иначе PDFAPI_BASE_URL
	pdfapi.WithTimeout(40*time.Second),
	pdfapi.WithMaxRetries(2),
	pdfapi.WithHTTPClient(&http.Client{Transport: myProxyTransport}),
)
if err != nil {
	log.Fatal(err) // нет ключа или неверные настройки
}

Таймаут не больше нуля или отрицательное число повторов — ErrInvalidArgument из NewClient. Ключа нет ни в client.String(), ни в текстах ошибок.

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

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

go
http.HandleFunc("/invoice", func(w http.ResponseWriter, r *http.Request) {
	// r.Context(): клиент ушёл — запрос к pdfapi тоже отменяется.
	pdf, err := client.HTML(r.Context(), "<h1>Счёт №42</h1>", nil)
	if err != nil {
		http.Error(w, "PDF не получился", http.StatusBadGateway)
		return
	}
	w.Header().Set("Content-Type", "application/pdf")
	_, _ = pdf.WriteTo(w)
})

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

Примеры#

В папке examples/ модуля; запуск — PDFAPI_KEY=pdf_live_... go run ./examples/<имя>.

ПримерЧто показывает
invoiceсчёт из html/template — данные экранируются сами
from-urlстраница по адресу, альбомная, с ожиданием скриптов
officedocx, xlsx или pptx в PDF рядом с исходным файлом
batchпачка документов в горутинах, в пределах лимита частоты
quotaостаток страниц для cron: код выхода 2 при нехватке
serverотдача PDF из net/http: WriteTo, контекст запроса, ошибки API — в HTTP-ответы