---
title: Distill Thumbnail from .afphoto and .afdesign
date: 2021-02-14T13:30:00
---

[Nextcloud](https://en.wikipedia.org/wiki/Nextcloud) does not support generating thumbnails from [Affinity Photo](https://en.wikipedia.org/wiki/Affinity_Photo) and [Affinity Designer](https://en.wikipedia.org/wiki/Affinity_Designer). Fine, I'll do it myself!

# Digging Binary

Glancing at `.afphoto` and `.afdesign` in Finder, I noticed that it has a QuickLook support and an ability to show the thumbnail image. Meaning there's a chance that these files contain **pre-generated thumbnail images** somewhere inside its binaries, meaning I don't have to reverse-engineer their format from ground up.

To verify this, I wrote a piece of Node.js script to seek for [PNG blob](https://www.w3.org/TR/PNG/) inside an .afphoto/.afdesign file and save it as a normal PNG file.

In the [11.2.1 General](https://www.w3.org/TR/PNG/#11Chunks) of the PNG spec, they stated a valid PNG image should begin with a PNG signature and end with an `IEND` chunk.

> A valid PNG datastream shall begin with a PNG signature, immediately followed by an `IHDR` chunk, then one or more `IDAT` chunks, and shall end with an `IEND` chunk. Only one `IHDR` chunk and one `IEND` chunk are allowed in a PNG datastream.

Conveniently, it is also guaranteed that there should be **only one IEND chunk** in a PNG file, so greedy search would just work.

```js gen_thumbnail.js
const fs = require("fs");

// png spec: https://www.w3.org/TR/PNG/
const PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const IEND_SIG = Buffer.from([73, 69, 78, 68]);

function extractPngBlob(buf) {
  const start = buf.indexOf(PNG_SIG);
  const end = buf.indexOf(IEND_SIG, start) + IEND_SIG.length * 2; // IEND + CRC
  return buf.subarray(start, end);
}

function extractThumbnail(input, output) {
  const buf = fs.readFileSync(input);
  const pngBlob = extractPngBlob(buf);
  fs.writeFileSync(output, pngBlob);
}

extractThumbnail(process.argv[2], process.argv[3] || "output.png");
```

That's right. This script just do `indexOf` on a `Buffer` and distill a portion of which starts with `PNG` signature and ends with `IEND` (+ CRC checksum).

# CRC (Cyclic Redundancy Code)

You may have wondered about `IEND_SIG.length * 2` part. It was to include [32-bit CRC](https://en.wikipedia.org/wiki/Cyclic_redundancy_check#CRC-32_algorithm) (Cyclic Redundancy Code) for `IEND` to the resulting blob.

Here, the byte-length of `IEND` chunk and its `CRC` checksum are coincidentally the same (4 bytes), so I just went with that code.

Now I can generate a thumbnail image from arbitrary `.afphoto` and `.afdesign` file. Let's move on delving into Nextcloud source code.

# Tweaking Nextcloud

At this point, all I have to do is to rewrite the above code in PHP and make them to behave as a Nextcloud Preview Provider.

```php lib/private/Preview/Affinity.php
<?php

namespace OC\Preview;

use OCP\Files\File;
use OCP\IImage;
use OCP\ILogger;

class Affinity extends ProviderV2 {
	public function getMimeType(): string {
		return '/application\/x-affinity-(?:photo|design)/';
	}

	public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
		$tmpPath = $this->getLocalFile($file);

		$handle = fopen($tmpPath, 'rb');
		$fsize = filesize($tmpPath);
		$contents = fread($handle, $fsize);
		$start = strrpos($contents, "\x89PNG");
		$end = strrpos($contents, "IEND", $start);
		$subarr = substr($contents, $start, $end - $start + 8 );

		fclose($handle);
		$this->cleanTmpFiles();

		$image = new \OC_Image();
		$image->loadFromData($subarr);
		$image->scaleDownToFit($maxX, $maxY);

		return $image->valid() ? $image : null;
	}
}
```

Also make sure my component to be auto-loaded on startup.

```patch lib/private/PreviewManager.php
@@ -363,6 +365,8 @@
 		$this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/');
 		$this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg/');
 		$this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/');
+		$this->registerCoreProvider(Preview\Affinity::class, '/application\/x-affinity-(?:photo|design)/');

 		// SVG, Office and Bitmap require imagick
 		if (extension_loaded('imagick')) {
```

```patch lib/composer/composer/autoload_static.php
@@ -1226,6 +1226,7 @@
         'OC\\OCS\\Result' => __DIR__ . '/../../..' . '/lib/private/OCS/Result.php',
         'OC\\PreviewManager' => __DIR__ . '/../../..' . '/lib/private/PreviewManager.php',
         'OC\\PreviewNotAvailableException' => __DIR__ . '/../../..' . '/lib/private/PreviewNotAvailableException.php',
+        'OC\\Preview\\Affinity' => __DIR__ . '/../../..' . '/lib/private/Preview/Affinity.php',
         'OC\\Preview\\BMP' => __DIR__ . '/../../..' . '/lib/private/Preview/BMP.php',
         'OC\\Preview\\BackgroundCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Preview/BackgroundCleanupJob.php',
         'OC\\Preview\\Bitmap' => __DIR__ . '/../../..' . '/lib/private/Preview/Bitmap.php',
```

```patch lib/composer/composer/autoload_classmap.php
@@ -1197,6 +1197,7 @@
     'OC\\OCS\\Result' => $baseDir . '/lib/private/OCS/Result.php',
     'OC\\PreviewManager' => $baseDir . '/lib/private/PreviewManager.php',
     'OC\\PreviewNotAvailableException' => $baseDir . '/lib/private/PreviewNotAvailableException.php',
+    'OC\\Preview\\Affinity' => $baseDir . '/lib/private/Preview/Affinity.php',
     'OC\\Preview\\BMP' => $baseDir . '/lib/private/Preview/BMP.php',
     'OC\\Preview\\BackgroundCleanupJob' => $baseDir . '/lib/private/Preview/BackgroundCleanupJob.php',
     'OC\\Preview\\Bitmap' => $baseDir . '/lib/private/Preview/Bitmap.php',
```

![](afphoto.png)

Voilà! Now I can see beautiful thumbnails for my drawings in Nextcloud web interface.

This is exactly why I love FOSS. It allows me to materialize **any niche things** I want in the FOSS without bothering its developers. This fact not only gives me confidence that I can control the functionality of the software, but it also makes me have more trust in the developers for giving me such freedom to make changes to their software.

# Finalized Solution

Enough talking, I've pushed my Nextcloud Docker setup with the above patches included on [GitHub](https://github.com/uetchy/docker-nextcloud). You can see the actual patch [here](https://github.com/uetchy/docker-nextcloud/blob/master/patches/lib.patch). Note that it also contains the patches for PDF thumbnail generator described below, and this particular patch _may_ pose security implications because of the usage of Ghostscript against PDF.

# Bonus: PDF thumbnail generator

Install `ghostscript` on your server to make it work.

```php lib/private/Preview/PDF.php
<?php

namespace OC\Preview;

use OCP\Files\File;
use OCP\IImage;

class PDF extends ProviderV2 {
	public function getMimeType(): string {
		return '/application\/pdf/';
	}

	public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
		$tmpPath = $this->getLocalFile($file);
		$outputPath = \OC::$server->getTempManager()->getTemporaryFile();

		$gsBin = \OC_Helper::findBinaryPath('gs');
		$cmd = $gsBin . " -o " . escapeshellarg($outputPath) . " -sDEVICE=jpeg -sPAPERSIZE=a4 -dLastPage=1 -dPDFFitPage -dJPEGQ=90 -r144 " . escapeshellarg($tmpPath);
		shell_exec($cmd);

		$this->cleanTmpFiles();

		$image = new \OC_Image();
		$image->loadFromFile($outputPath);
		$image->scaleDownToFit($maxX, $maxY);

		unlink($outputPath);

		return $image->valid() ? $image : null;
	}
}
```
