Generate a Watermarked Multi-Paged PDF

Banner for a MediaJam post

Eugene Musebe

Introduction

One of the ways to present your work is to create a portfolio. In this article, we will create a PDF portfolio with your branding from photos you upload to Cloudinary with a specific tag for instance "Wedding".

Let's get started.

PHPSandbox and Github

The final project can be viewed on PHPSandbox and the entire source code is available on my Github repository.

Prerequisites

Using Cloudinary in your Laravel projects is pretty straightforward. However, for you to be able to easily follow along, you need to have a good command of your terminal, Git, and entry knowledge of PHP specifically with the Laravel framework.

Getting Started

Being that Laravel is a PHP Framework, we will need Composer. Like any modern PHP framework, Laravel uses Composer to manage its dependencies. Before, we can start ensure you have Composer installed on your machine. Follow step 1 below to install Composer and PHP.

  1. Install Composer and PHP on

your development or production machine.

  1. Install Laravel

  2. Via Composer:

composer create-project --prefer-dist laravel/laravel cloudinary-branded-pdf

  1. Via Laravel Installer

composer global require laravel/installer

laravel new cloudinary-branded-pdf

  1. In step 2 above we have installed the Laravel Installer and used it to scaffold a new application in the

folder cloudinary-branded-pdf. With Laravel installed, we should be able to start and test the server ensuring

everything is okay. Change the directory to the project folder and run the local development server by typing the

following commands:

cd cloudinary-branded-pdf

php artisan serve

The Laravel project is now up and running. When you open http://localhost:8000 on your computer, you should see the

image below:

Laravel Server Running

Setting up Cloudinary’s Laravel SDK

We will be using Cloudinary to generate multi-paged PDFs from images we will tag and upload. We will also perform the

overlay transformation to add a watermark to each of the pages. Sounds fun, we will start by creating a

free Cloudinary account.

  1. Sign up for a free Cloudinary account then navigate to the Console page and take note of your Cloud name, API Key and

API Secret.

Cloudinary Dashboard

  1. Install Cloudinary’s Laravel SDK:

composer require cloudinary-labs/cloudinary-laravel

Note: Please ensure you follow all the steps in the #Installation section. Publish the configuration file and add

the Cloudinary credentials you noted in Step 1 to the .env file.

1CLOUDINARY_API_KEY=YOUR_CLOUDINARY_API_KEY
2
3CLOUDINARY_API_SECRET=YOUR_CLOUDINARY_API_SECRET
4
5CLOUDINARY_CLOUD_NAME=YOUR_CLOUDINARY_CLOUD_NAME

Generating a Watermarked Multi-paged PDF

We will use Cloudinary's Upload API with the multi method which will take the images we upload, apply transformations and convert them to a PDF document.

Multiple File Upload with Livewire

To generate a PDF document we will need a user interface, we will use the Laravel package Livewire to build this.

  1. Install Livewire Package by running the following command in your Laravel project:

composer require livewire/livewire

  1. Include Livewire scripts and styles on every page that will be using Livewire. In our case welcome.blade.php:
1...
2
3@livewireStyles
4
5</head>
6
7<body>
8
9...
10
11
12
13@livewireScripts
14
15</body>
16
17</html>
  1. We will then create a Livewire Component to handle our image uploads:

php artisan make:livewire MultipleFileUpload

This will create two files, first app/Http/Livewire/MultipleFileUpload.php and the other one

in resources/views/livewire/multiple-file-upload.blade.php

Now you can use this component anywhere in your Laravel project using the following snippet:

<livewire:multiple-file-upload/>

or

@livewire('multiple-file-upload')

  1. Open resources/views/welcome.blade.php and add the following code within the <body></body> tags as shown below:
1<body class="antialiased">
2
3<div>
4
5@livewire('multiple-file-upload')
6
7</div>
8
9</body>

This includes the Livewire component we created earlier in our welcome.blade.php.

Note: Please ensure you go through the Livewire documentation,

to learn how to install and set it up.

  1. Open the file resources/views/livewire/multiple-file-upload.blade.php and populate it with the following code:
1<form class="mb-5" wire:submit.prevent="uploadImages">
2
3<div class="form-group row mt-5 mb-3">
4
5<div class="input-group mb-5">
6
7<input id="watermark" type="file" class="form-control @error('watermark') is-invalid @enderror"
8
9placeholder="Choose files..." wire:model="watermark">
10
11<label class="input-group-text" for="media">
12
13Choose watermark...
14
15</label>
16
17@error('watermark')
18
19<div class="invalid-feedback">{{ $message }}</div>
20
21@enderror
22
23</div>
24
25<div class="input-group mb-3">
26
27<span class="input-group-text" id="basic-addon1">#</span>
28
29<input class="form-control @error('tag') is-invalid @enderror" placeholder="Portfolio Tag"
30
31aria-label="Portfolio Tag"
32
33aria-describedby="basic-addon1" wire:model="tag">
34
35@error('tag')
36
37<div class="invalid-feedback">{{ $message }}</div>
38
39@enderror
40
41</div>
42
43<div class="input-group">
44
45<input id="files" type="file" class="form-control @error('files'|'files.*') is-invalid @enderror"
46
47placeholder="Choose files..." wire:model="files" multiple>
48
49<label class="input-group-text" for="files">
50
51Choose images for portfolio...
52
53</label>
54
55@error('files'|'files.*')
56
57<div class="invalid-feedback">{{ $message }}</div>
58
59@enderror
60
61</div>
62
63<small class="text-muted text-center mt-2" wire:loading wire:target="files">
64
65{{ __('Uploading') }}…
66
67</small>
68
69<small class="text-muted text-center mt-2" wire:loading wire:target="watermark">
70
71{{ __('Uploading') }}…
72
73</small>
74
75</div>
76
77<div class="text-center">
78
79<button type="submit" class="btn btn-sm btn-primary w-25">
80
81<i class="fas fa-check mr-1"></i> {{ __('Generate PDF') }}
82
83<i class="spinner-border spinner-border-sm ml-1 mt-1" wire:loading wire:target="uploadImages"></i>
84
85</button>
86
87</div>
88
89</form>

This is our Livewire Component view, this basically will display a form with inputs for our watermark, tag, images

files and a button.

Cloudinary PDF UI

You will see the implementation in code shortly.

Implementation in Code

Open the file app/Http/Livewire/MultipleFileUpload.php. Here, we are going to add a method that will handle the

multiple files selected by the user, upload them to Cloudinary and save their public_id's in an array that we will use

later on.

Add the following code to this file.

  1. First, we use Livewires WithFileUploads to help us with file uploads, then create two variables $media

and $optimizedImage which is an array that will contain the image URLs we get back from Cloudinary.

1use WithFileUploads;
2
3
4
5public $files = [];
6
7public $watermark;
8
9public $tag;
  1. Secondly, we will create the uploadImages function which will upload the watermark and image files

to Cloudinary. Cloudinary will apply specific transformations to each one of the images that will make the pages before generating the PDF document.

1public function uploadImages() {
2
3...
4
5}
  1. Let's populate our method in step 2 above:
1public function uploadImages() {
2
3$this->validate([
4
5'files' => [
6
7'required',
8
9'max:10240'
10
11],
12
13'files.*' => 'mimes:jpeg,jpg,png',
14
15'watermark' => [
16
17'required',
18
19'image',
20
21'mimes:png',
22
23'max:100'
24
25],
26
27'tag' => [
28
29'required',
30
31'string',
32
33'max:20'
34
35],
36
37]);
38
39
40
41$watermarkPublicId = cloudinary()->upload($this->watermark->getRealPath(), [
42
43'folder' => 'branded-pdf',
44
45'public_id' => 'watermark',
46
47])->getPublicId();
48
49
50
51foreach ($this->files as $file) {
52
53cloudinary()->upload($file->getRealPath(), [
54
55'folder' => 'branded-pdf',
56
57'width' => '794',
58
59'height' => '1123',
60
61'gravity' => 'auto',
62
63'crop' => 'fill',
64
65'tags' => ["$this->tag"],
66
67]);
68
69}
70
71
72
73cloudinary()->uploadApi()->multi($this->tag, [
74
75'transformation' => [
76
77'overlay' => $watermarkPublicId,
78
79'gravity' => 'north_east',
80
81'x' => 0.02,
82
83'y' => 0.02,
84
85'crop' => 'scale',
86
87'flags' => 'relative',
88
89'width' => 0.15,
90
91'opacity' => 80
92
93],
94
95'format' => 'pdf',
96
97'notification_url' => env('CLOUDINARY_NOTIFICATION_URL')
98
99]);
100
101}

Let's talk about the code.

  • Uploading the watermark

We upload the watermark to Cloudinary and get the public_id which we will use later on.

1$watermarkPublicId = cloudinary()->upload($this->watermark->getRealPath(), [
2
3'folder' => 'branded-pdf',
4
5'public_id' => 'watermark',
6
7])->getPublicId();
  • Uploading the images

We upload the images selected by the user and apply the tag that they specified and some transformations to each one of them using a for loop.

1foreach ($this->files as $file) {
2
3cloudinary()->upload($file->getRealPath(), [
4
5'folder' => 'branded-pdf',
6
7'width' => '794',
8
9'height' => '1123',
10
11'gravity' => 'auto',
12
13'crop' => 'fill',
14
15'tags' => ["$this->tag"],
16
17]);
18
19}
  • The multi method

Through the Upload API, the multi-method allows us to apply transformations and generate the PDF from all the

images with the tag that we pass to Cloudinary. We do this by specifying the format as pdf and applying the

transformations that overlay our watermark on the images with this tag.

1cloudinary()->uploadApi()->multi($this->tag, [
2
3'transformation' => [
4
5'overlay' => $watermarkPublicId,
6
7'gravity' => 'north_east',
8
9'x' => 0.02,
10
11'y' => 0.02,
12
13'crop' => 'scale',
14
15'flags' => 'relative',
16
17'width' => 0.15,
18
19'opacity' => 80
20
21],
22
23'format' => 'pdf',
24
25'notification_url' => env('CLOUDINARY_NOTIFICATION_URL')
26
27]);

You can check out the reference for full

details on the relevant options.

On the notification_url we have specified we will receive a response from Cloudinary once our PDF has been

successfully created. The response will be as follows:

1{
2
3"url": "http://res.cloudinary.com/dgrpkngjn/image/multi/c_scale,fl_relative,g_north_east,l_branded-pdf:watermark,o_80,w_0.15,x_0.02,y_0.02/f_pdf/v1656857502/Wedding%20Photos.pdf",
4
5"secure_url": "https://res.cloudinary.com/dgrpkngjn/image/multi/c_scale,fl_relative,g_north_east,l_branded-pdf:watermark,o_80,w_0.15,x_0.02,y_0.02/f_pdf/v1656857502/Wedding%20Photos.pdf",
6
7"asset_id": "770deef5d2f1285e39565a1a77ebbe3d",
8
9"public_id": "Wedding Photos,pdf,c_scale,fl_relative,g_north_east,l_branded-pdf:watermark,o_80,w_0.15,x_0.02,y_0.02/f_pdf",
10
11"version": 1656857502,
12
13"notification_type": "multi"
14
15}

Tip: Please be sure to change your Cloudinary settings to allow delivery of PDFs and ZIP files, this is disabled by default to prevent dthe istribution of malware.

Handling Cloudinary Responses

Webhooks are one of a few ways web applications can communicate with each other. We can receive Cloudinary's responses

through a webhook and run processes that will do something like notify the user or ban the video.

Create a WebhookController.php by typing the following command:

php artisan make:controller WebhookController

In the file created app/Http/Controllers/WebhookController.php we will add the following code:

1public function cloudinary(Request $request) {
2
3//Verification
4
5$verified = SignatureVerifier::verifyNotificationSignature(json_encode($request), $request->header('X-Cld-Timestamp'), $request->header('X-Cld-Signature'));
6
7
8
9// If the signature is verified and moderation is rejected
10
11if ($verified && $request->notification_type === 'multi') {
12
13// Get Secure URL
14
15$secureUrl = $request->secure_url;
16
17// Notify user
18
19...
20
21}
22
23
24
25return response('Unverified', 401);
26
27}

Tip: A webhook is a mechanism where an application can notify another application that something has happened.

Since the notification from Cloudinary will be an external request we will need to allow it through the VerifyCsrfToken.php middleware to prevent CSRF errors.

1...
2
3protected $except = [
4
5'webhooks'
6
7];
8
9}

Next, we will create the webhook route in routes/api.php.

1...
2
3//webhooks client
4
5Route::post('webhooks/cloudinary', [WebhookController::class, 'cloudinary']);

And finally, update our CLOUDINARY_NOTIFICATION_URL in the environment variables file .env as follows:

1CLOUDINARY_NOTIFICATION_URL=https://<app_url>/api/webhooks/cloudinary

Finally, we can see the results:

Cloudinary Watermarked PDF Created

Conclusion

Cloudinary makes it easy to generate a PDF by automating and applying some transformations. We can use this to create portfolios which we can then present to our clients.

The possibilities are endless, check out Cloudinary for your A to Z media management - upload, storage, administration, manipulation, optimization, and delivery.

Get started with Cloudinary in your Laravel projects for FREE!

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.