Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.77% covered (success)
96.77%
30 / 31
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Extractor
96.77% covered (success)
96.77%
30 / 31
66.67% covered (warning)
66.67%
2 / 3
11
0.00% covered (danger)
0.00%
0 / 1
 __construct
50.00% covered (warning)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 countPages
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 extract
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Build\Image;
16
17use Pop\Pdf\Build\Exception;
18
19/**
20 * Pdf page-to-image extractor class
21 *
22 * Rasterizes pages of an existing PDF into standalone image files via
23 * Imagick. The opposite direction of Parser (which turns a raster image
24 * into a PDF page).
25 *
26 * @category   Pop
27 * @package    Pop\Pdf
28 * @author     Nick Sagona, III <nick@popphp.org>
29 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
30 * @license    https://www.popphp.org/license     New BSD License
31 * @version    6.2.0
32 */
33class Extractor
34{
35
36    /**
37     * Supported output image formats
38     * @var array
39     */
40    protected const array SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'tif'];
41
42    /**
43     * Constructor
44     *
45     * Instantiate the extractor object
46     *
47     * @throws Exception
48     */
49    public function __construct()
50    {
51        if (!class_exists('Imagick', false)) {
52            throw new Exception('Error: The Imagick extension is required to extract PDF pages as images.');
53        }
54    }
55
56    /**
57     * Get the total number of pages in the PDF file
58     *
59     * @param  string $file
60     * @throws Exception
61     * @return int
62     */
63    public function countPages(string $file): int
64    {
65        if (!file_exists($file)) {
66            throw new Exception("Error: The PDF file '{$file}' does not exist.");
67        }
68
69        $imagick = new \Imagick();
70        $imagick->pingImage($file);
71        $totalPages = $imagick->getNumberImages();
72        $imagick->clear();
73
74        return $totalPages;
75    }
76
77    /**
78     * Extract the given page numbers of the PDF file as individual image files
79     *
80     * @param  string $file
81     * @param  string $location
82     * @param  string $format
83     * @param  int    $resolution
84     * @param  string $filenameFormat sprintf() format string, given the file's basename and the
85     *                                1-indexed page number (e.g. '%1$s-%2$02d' => 'document-01',
86     *                                'page-%2$02d' => 'page-01')
87     * @param  array  $pageNumbers 1-indexed page numbers to extract
88     * @throws Exception
89     * @return array
90     */
91    public function extract(
92        string $file, string $location, string $format, int $resolution, string $filenameFormat, array $pageNumbers
93    ): array
94    {
95        if (!file_exists($file)) {
96            throw new Exception("Error: The PDF file '{$file}' does not exist.");
97        }
98        if (!is_dir($location) || !is_writable($location)) {
99            throw new Exception("Error: The location '{$location}' is not a writable directory.");
100        }
101
102        $format = strtolower($format);
103        if (!in_array($format, self::SUPPORTED_FORMATS)) {
104            throw new Exception("Error: The format '{$format}' is not supported.");
105        }
106
107        $basename = pathinfo($file, PATHINFO_FILENAME);
108        $location = rtrim($location, '/\\');
109        $images   = [];
110
111        foreach ($pageNumbers as $pageNum) {
112            // setResolution() must be called before readImage() - Imagick
113            // only honors DPI at rasterization time, not after the fact.
114            $page = new \Imagick();
115            $page->setResolution($resolution, $resolution);
116            $page->readImage($file . '[' . ($pageNum - 1) . ']');
117            $page->setImageFormat($format);
118
119            // The primary use case for extracted pages is OCR, so JPEG/WebP -
120            // both lossy by default - are forced to a high quality to avoid
121            // compression artifacts that degrade text edges. PNG and TIFF
122            // are already lossless and left alone.
123            if (in_array($format, ['jpg', 'jpeg', 'webp'])) {
124                $page->setImageCompressionQuality(90);
125            }
126
127            $path = $location . DIRECTORY_SEPARATOR . sprintf($filenameFormat, $basename, $pageNum) . '.' . $format;
128            $page->writeImage($path);
129            $page->clear();
130
131            $images[$pageNum] = $path;
132        }
133
134        return $images;
135    }
136
137}