Мне поставили задачу: сформировать документ через Adobe API. Требований было минимум - главное сформировать документ из шаблона. Шаблон я мог отформатировать в нужный мне формат без проблем. Но сам формат, как его использовать и как это связать с API я не знал. Поэтому нырнул в документацию.

После пары часов изучения, я отчаялся. Информации было море, но нужной информации не нашел. Но зато нашел несколько вариантов документации, пару статей и несколько видео-уроков. Всё это не приблизило меня к цели ни на шаг.

Но спустя несколько дней, огромное количество нервов и желания сдаться, я всё-таки решил задачу. И буду рад поделиться решением тут.

Adobe Document Generation API

Первая задача: какой из Adobe сервисов нужен. 

Adobe Document Generation API. Он отвечает за генерацию, сжатие, конвертирование и иную работу с документами. Но нас интересует только генерация.

Чтобы его использовать, необходимо выполнить следующие шаги:

  1. Создать приложение

  2. Получить токен доступа (access token)

  3. Подготовить шаблон

  4. Загрузить шаблон

  5. Сформировать итоговый документ

Создаем приложение

Переходим по ссылке https://developer.adobe.com/console/projects, затем:

  1. Нажимаем "Create new Project"

  2. Нажимаем "Add to project" - выбираем API и ищем там PDF Services API

  3. Выбираем автоматическую генерацию приватных ключей, жмем "Continue" кучу раз

  4. Ждем пока Adobe сгенерирует доступы и предложит скачать их

Переходим в приложение во вкладку "Credentials" где будут данные для генерации JWT.

Если Adobe не прислал Client Secret, пересоздаем его тут же и сохраняем себе.

Получаем токен доступа (access token)

Первым делом мы создаем JWT строку, параметры которой указаны тут: https://developer.adobe.com/developer-console/docs/guides/authentication/JWT/

<?php
use Firebase\JWT\JWT;

$organizationID = 'Organization ID с вкладки Credentials';
$accountID = 'Technical Account ID с вкладки Credentials';
$clientID = 'Client ID с вкладки Credentials';


$privateKey  = file_get_contents('путь до файла private.key из архива');

$payload = [
    'exp'                                                    => time() + 60 * 60 * 2,
    'iss'                                                    => $organizationID,
    'sub'                                                    => $accountID,
    'aud'                                                    => 'https://ims-na1.adobelogin.com/c/' . $clientID,
    'https://ims-na1.adobelogin.com/s/ent_documentcloud_sdk' => true
];

// Используется firebase/php-jwt
$jwt = JWT::encode($payload, $privateKey, 'RS256');

Далее запрашиваем access token. Используем запрос, который я нашел в официальной коллекции Adobe в Postman(Adobe Target Admin APIs/Exchange JWT for Access token):

<?php
// JWT получен выше

$clientID = 'Client ID с вкладки Credentials';
$clientSecret = 'Client Secret с вкладки Credentials';

$query    = [
    'client_id'     => $clientID,
    'client_secret' => $clientSecret,
    'jwt_token'     => $jwt,
];

// Используется фасад Http из Laravel. Но суть его в формировании обычного POST запроса. Да, без тела. Так надо
$response = Http::post('https://ims-na1.adobelogin.com/ims/exchange/jwt?' . http_build_query($query));

$token = $response->json('access_token');

В ответе будет поле access_token с нашим токеном. Либо ошибка и её описание.

Подготавливаем шаблон

Шаблон заполняется JSON данными. А в самом документе необходимо указать ключи этого самого JSON.

Пример JSON:

{
  "name": "Dmitry",
  "meta":{
    "age": 23
  }
}

В шаблоне можно указать ключи {{name}} и {{meta.age}}

Загружаем шаблон

Adobe не хранит у себя файлы. Он использует сервера Amazon.

Поэтому сперва получаем через Adobe доступ к Amazon, а затем загружаем наш шаблон.

Получаем доступ к Amazon:

<?php
// access token был получен выше
$token = '';

$clientID = 'Client ID с вкладки Credentials';
// У docx mime тип "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
$mime = 'mime тип шаблона. Обычно это docx'

// Используется фасад Http Laravel:
// - POST запрос
// - Тело запроса raw application/json
// - Авторизация Bearer с access token
// - Доп. заголовок "x-api-key" с вашим ClientID
$response = Http
    ::withToken($token)
    ->asJson()
    ->withHeaders([
        'x-api-key' => $clientID
    ])
    ->post('https://pdf-services.adobe.io/assets', [
        "mediaType" => $mime
    ]);

$assetID   = $response->json('assetID');
$uploadUri = $response->json('uploadUri');

// В ответе будет assetID и uploadUri. Второй нужен для загрузки файла

Загружаем шаблон:

<?php
// $uploadUri был получен выше
$mime = 'mime тип загружаемого шаблона'
  
// Тут привычный всем Guzzle:
// - PUT запрос
// - Тело запроса файл в байтах
// - Доп. заголовок "Content-Type" с вашим mime типом загружаемого файла
$request  = new \GuzzleHttp\Psr7\Request(
            'PUT',
            $uploadUri,
            ['Content-Type' => $mime],
            file_get_contents('путь до шаблона')
        );
$client   = new Client();
$response = $client->send($request);

// Если не 200 код, то явно была ошибка
if ($response->getStatusCode() != 200) {
  // Обработка ошибки, иначе использовать assetID полученный выше
}

Шаблон загружен

Формируем документ из шаблона

Отправляем запрос на формирование документа:

<?php
// assetID был получен выше
$clientID = 'Client ID с вкладки Credentials';
// access token был получен выше
$token = '';

$data = [
  "name" => "Dmitry",
  "meta" => ["age" => 23]
];

// Используется фасад Http Laravel:
// - POST запрос
// - Тело запроса raw application/json
// - Авторизация Bearer с access token
// - Доп. заголовок "x-api-key" с вашим ClientID
$response = Http
    ::withToken($token)
    ->asJson()
    ->withHeaders([
        'x-api-key' => $clientID
    ])
    ->post('https://pdf-services.adobe.io/operation/documentgeneration', [
        "assetID"          => $assetID,
        "outputFormat"     => "pdf",
        "jsonDataForMerge" => $data
    ]);

// Если статус не 201, то произошла ошибка
if ($response->status() != 201) {
  // Обработка ошибки
}

// Необходимо забрать заголовок x-request-id
$requestID = $response->header('x-request-id')

После отправки запроса, необходимо проверить статус запроса:

<?php
// x-request-id был получен выше
$requestID = '';
// access token был получен выше
$token = '';

$clientID = 'Client ID с вкладки Credentials';

// Используется фасад Http Laravel:
// - GET запрос
// - Авторизация Bearer с access token
// - Доп. заголовок "x-api-key" с вашим ClientID
$response = Http
    ::withHeaders([
        'x-api-key' => $clientID
    ])
    ->withToken($token)
    ->get("https://pdf-services.adobe.io/operation/documentgeneration/{$requestID}/status");
$status   = $response->json('status');

// Если статус не done, то либо ошибка, либо ещё формируется
if ($status != 'done') {
    // Обработка ошибки
}

// Если всё успешно, то можно получить ID документа и ссылку на скачивание
$assetID     = $response->json('asset.assetID');
$downloadUri = $response->json('asset.downloadUri');

Документ сформирован и доступен по ссылке $downloadUri

Ссылки

Комментарии (5)


  1. Neverfan
    30.12.2022 13:40
    +1

    Здравствуйте, спасибо за статью. Интересует, почему поставили задачу именно так? Почему решили реализовать именно через Adobe API, а не классическим способом через wkhtmltopdf или node.js (мы используем обертку spatie/browsershot). В шаблоне были какие-то чертежи куда нужно добавить подписи?


    1. dmtrbskkv Автор
      30.12.2022 15:20
      +1

      На то были 2 причины:

      1. у клиента была подписка на Adobe и он работал там

      2. я работаю на PHP

      Но тут стоит добавить, что это первая половина работы - генерация документа. Вторая - отправка документа на подпись через Adobe Sign API. Про вторую часть я потихоньку тоже пишу статью, чтобы хоть немного облегчить жизнь тем кто начнет с этим всем работать

      А сам по себе шаблон был документом, в котором необходимо заполнить детали сделки и данные клиента. И было важно соблюсти формат в котором был прислан документ


    1. rsmike
      31.12.2022 19:54

      классическим способом через wkhtmltopdf или node.js

      Есть вариант проще - темплейты в ворде, замены в PHPOffice, конвертация через Gutenberg


      1. dmtrbskkv Автор
        01.01.2023 05:16

        Да, вполне. Может в следующий раз предложу такой вариант клиенту


      1. Neverfan
        01.01.2023 13:29

        Мы генерируем PDF для законотельных актов всех видов, думаю темплейты нам не помогут, но про такой способ не слышал, поизучаю информацию