PDF API is a contributed module in Drupal that provides wrapper plugins for some of the popular vendors that are basically used in PDF generation. These vendors include DOMPDF, TCPDF, and others, which offer a range of features for creating PDFs, such as support for various fonts, images, and page layouts.

Those wrapper plugins are called PDF generator plugins and we will utilize one of the generator plugins to print a certificate. 

In our example, we will use the certificate image below and print a name on it. Then, we will download it as a PDF. The same method can be applied to add a date and a signature to the certificate.

Demo Certificate

Okay, let's review the steps for printing a certificate using the PDF_API in Drupal.

Step 1. Add a route in your routing.yml

Add a route in your module's routing.yml for handling certificate generation.

mymodule.certificate:
  path: '/user/{user}/certificate'
  defaults:
    _controller: '\Drupal\mymodule\Controller\CertificateController::generate'
    _title: 'Get Certificate'
  requirements:
    _permission: 'access content'
  options:
    no_cache: TRUE
    parameters:
      user:
        type: entity:user

Step 2. Add a controller to generate the certificate.

<?php

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ExtensionList;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\pdf_api\PdfGeneratorPluginManager;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * An certificate controller.
 */
class CertificateController extends ControllerBase {

  /**
   * Pdf generator plugin manager.
   *
   * @var \Drupal\pdf_api\PdfGeneratorPluginManager
   */
  protected $pdfGeneratorPluginManager;

  /**
   * Renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * Extension list service.
   *
   * @var \Drupal\Core\Extension\ExtensionList
   */
  protected $extensionList;

  /**
   * RouteMatch service.
   *
   * @var \Drupal\Core\Routing\RouteMatchInterface
   */
  protected $routeMatch;

  /**
   * Logger service.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * CertificateController constructor.
   *
   * @param \Drupal\pdf_api\PdfGeneratorPluginManager $pdfGeneratorPluginManager
   *   PdfGenerator plugin manager.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   Drupal renderer service.
   * @param \Drupal\Core\Extension\ExtensionList $extensionList
   *   Extension list manager.
   * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
   *   Route match service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerChannelFactory
   *   Logger channel factory.
   */
  public function __construct(PdfGeneratorPluginManager $pdfGeneratorPluginManager,
                              RendererInterface $renderer,
                              ExtensionList $extensionList,
                              RouteMatchInterface $routeMatch,
                              LoggerChannelFactoryInterface $loggerChannelFactory) {
    $this->pdfGeneratorPluginManager = $pdfGeneratorPluginManager;
    $this->renderer = $renderer;
    $this->extensionList = $extensionList;
    $this->routeMatch = $routeMatch;
    $this->logger = $loggerChannelFactory->get('cert_error');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('plugin.manager.pdf_generator'),
      $container->get('renderer'),
      $container->get('extension.list.module'),
      $container->get('current_route_match'),
      $container->get('logger.factory')
    );
  }

  /**
   * Returns a render-able array for a test page.
   */
  public function generate() {
    global $base_url;

    // Certificate Url.
    $path = $this->extensionList->getPath('mymodule');
    $image = file_get_contents("{$base_url}/{$path}/assets/images/certificate.jpg");

    // Load user.
    $user = $this->routeMatch->getParameter('user');

    try {

      if ($user instanceof UserInterface) {

        // Set generator.
        $pdfgenerator = $this->pdfGeneratorPluginManager->createInstance('dompdf');

        // Build render array.
        $build = [
          '#theme' => 'certificate',
          '#image' => base64_encode($image),
          '#name' => $user->get('field_full_name')->value ?? $user->getAccountName(),
        ];
        $html = $this->renderer->renderPlain($build);

        // Generate certificate.
        $pdfgenerator->addPage($html);
        $pdfgenerator->send();
        exit;
      }
      else {
        $this->logger->error(
          $this->t('No submission for user id %label', [
            '%label' => $user->id(),
          ])
        );
      }
    }
    catch (\Exception $e) {
      $this->logger->error($e->getMessage());
    }

    return [
      '#markup' => $this->t('There is some error in certificate generation, Please contact administrator.'),
    ];
  }

}

Step 3: Define your custom theme.

As you can see, we are using a custom theme to generate the HTML for certificates, so you can define your own custom theme using hook_theme.

/**
 * Implements hook_theme().
 */
function mymodule_theme($existing, $type, $theme, $path)
{
  return [
    'certificate' => [
      'variables' => [
        'image' => NULL,
        'name' => NULL,
        'type' => NULL,
      ],
    ],
  ];
}

Step 4: Twig template for our custom theme.

Under the "templates" directory of your module, create a new Twig template.

{#certificate.html.twig#}
<style>
  @page { size: 300mm 212mm;}
  body,html {
    margin: 0;
  }
  .certificate-wrapper {
    position: relative;
  }
  .certificate {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    width: 100%;
  }
  .certificate img {
    object-fit: cover;
  }
  .text {
    z-index: 9999;
    position: absolute;
    top: 48%;
    width: 100%;
    text-align: center;
    font-weight: bold;
    font-size: 25px;
  }
</style>
<div class="certificate-wrapper">
    <div class="certificate">
        <img src="data:image/jpeg;base64,{{ image }}" width="100%"/>
    </div>
    <div class="text">{{ name }}</div>
</div>

That's it, we're good now. I've used the above code in some of my projects, and it's working quite well. The only thing is that I have to adjust the x and y coordinates of the text (user's name) to fit properly if we change the certificate image.

Further Improvement.

As you can see, most of the things in the above example are hardcoded, such as the certificate image and coordinates of text. We can make the code dynamic by implementing a configuration form, which would allow us to change the certificate image and text coordinates from the backend without having to modify the underlying code.