Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PHP cURL cannot select PDF pages. It only transfers the response bytes. To export selected pages, download the PDF with cURL, verify that the response is really a PDF, then pass the file to a PDF-aware tool such as qpdf or FPDI. The examples below show both approaches, including range validation, error handling, and preservation caveats.

What the workflow actually does

A reliable export has two separate stages:

  1. Transfer: PHP cURL retrieves the source URL and writes the response to a file.
  2. Page selection: qpdf or a PHP PDF library reads that file and creates a new document containing the requested pages.

A successful cURL call does not prove that the body is a usable PDF. The server might return an HTML error page, a login screen, a redirect target, or a bot challenge with an HTTP 200 status. Check the HTTP status, content type when available, file size, and the PDF signature before extraction.

Download the PDF safely with PHP cURL

This function streams the response to disk, follows redirects, applies a timeout, and rejects unsuccessful HTTP responses. It also checks the first five bytes for the %PDF- signature.

<?php
function downloadPdf(string $url, string $destination): void
{
    $fp = fopen($destination, 'wb');
    if ($fp === false) {
        throw new RuntimeException("Cannot open output file: {$destination}");
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_FILE => $fp,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
        CURLOPT_CONNECTTIMEOUT => 15,
        CURLOPT_TIMEOUT => 90,
        CURLOPT_USERAGENT => 'PdfPageExporter/1.0',
        CURLOPT_FAILONERROR => false,
        CURLOPT_HEADER => false,
    ]);

    $ok = curl_exec($ch);
    $curlError = curl_error($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);
    fclose($fp);

    if ($ok === false) {
        @unlink($destination);
        throw new RuntimeException("Transfer failed: {$curlError}");
    }
    if ($status < 200 || $status >= 300) {
        @unlink($destination);
        throw new RuntimeException("Source returned HTTP {$status}");
    }
    if (filesize($destination) < 5 || file_get_contents($destination, false, null, 0, 5) !== '%PDF-') {
        @unlink($destination);
        throw new RuntimeException("Response is not a PDF (content type: {$contentType})");
    }
}

downloadPdf('https://example.com/source.pdf', __DIR__ . '/source.pdf');

For small files you can use CURLOPT_RETURNTRANSFER => true and write the returned string yourself. Streaming is safer for large documents because it avoids keeping the entire body in PHP memory. Treat redirects, authentication, cookies, and custom headers as part of the source site’s requirements; do not blindly follow a redirect to an untrusted host.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Option 1: select pages with qpdf

qpdf is a command-line PDF utility. Install it through your operating system’s package manager, then invoke it from PHP only when your deployment permits executing a trusted binary. Its page numbers start at 1, and range endpoints are inclusive.

Basic inclusive range

qpdf source.pdf --pages source.pdf 2-4 -- selected.pdf

That command exports pages 2, 3, and 4. You can combine selections with commas:

qpdf source.pdf --pages source.pdf 1,3,7-9 -- selected.pdf

qpdf also supports reversed ranges and ranges relative to the end of the document. Quote arguments when paths or expressions could be interpreted by the shell.

Calling qpdf from PHP

<?php
$input = __DIR__ . '/source.pdf';
$output = __DIR__ . '/selected.pdf';
$pages = '2-4,8';

$command = sprintf(
    'qpdf %s --pages %s %s -- %s 2>&1',
    escapeshellarg($input),
    escapeshellarg($input),
    escapeshellarg($pages),
    escapeshellarg($output)
);

exec($command, $lines, $exitCode);
if ($exitCode !== 0 || !is_file($output) || filesize($output) === 0) {
    throw new RuntimeException("qpdf failed:n" . implode("n", $lines));
}

Do not concatenate page expressions or file paths from untrusted input without validation and escaping. A safer application validates a grammar such as digits, commas, and hyphens, limits the number of pages, and uses a fixed qpdf path configured by the administrator.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Option 2: import pages with FPDI

FPDI fits a PHP PDF-generation workflow. It can be used with FPDF, TCPDF, or tFPDF; it is also a dependency in mPDF. Install the packages with Composer according to the generator you use, then import each requested page into a new document.

FPDI example with FPDF

composer require setasign/fpdf setasign/fpdi
<?php
require __DIR__ . '/vendor/autoload.php';

use setasignFpdiFpdi;

$input = __DIR__ . '/source.pdf';
$output = __DIR__ . '/selected.pdf';
$wantedPages = [2, 3, 4];

$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($input);

foreach ($wantedPages as $pageNumber) {
    if (!is_int($pageNumber) || $pageNumber < 1 || $pageNumber > $pageCount) {
        throw new InvalidArgumentException("Page {$pageNumber} is outside 1-{$pageCount}");
    }

    $template = $pdf->importPage($pageNumber); // CropBox by default
    $size = $pdf->getTemplateSize($template);
    $orientation = $size['width'] > $size['height'] ? 'L' : 'P';
    $pdf->AddPage($orientation, [$size['width'], $size['height']]);
    $pdf->useTemplate($template);
}

$pdf->Output('F', $output);

setSourceFile() returns the source page count, and importPage() accepts a one-based page number. This makes it straightforward to reject invalid requests before producing output. The example preserves the imported page’s visual dimensions rather than forcing every page onto a fixed letter or A4 sheet.

External links and interactive content

FPDI’s external-link option defaults to disabled. If copied URI-action link annotations are required, enable that option as documented by Setasign. Importing the page appearance does not automatically guarantee that every annotation, form field, bookmark, tag, metadata item, or interactive feature survives.

Parse and validate page ranges

Never pass a user-supplied range straight to a shell command or assume the requested page exists. A practical parser expands expressions such as 1,3-5, removes duplicates while retaining order, and compares every result with the page count returned by FPDI (or obtained through a PDF inspection step).

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php
function expandPageRange(string $expression, int $pageCount): array
{
    if (!preg_match('/^[0-9, -]+$/', $expression)) {
        throw new InvalidArgumentException('Invalid page range syntax');
    }
    $result = [];
    foreach (preg_split('/s*,s*/', trim($expression)) as $part) {
        if ($part === '') continue;
        if (str_contains($part, '-')) {
            [$start, $end] = array_map('intval', preg_split('/s*-s*/', $part));
            if ($start < 1 || $end < 1 || $start > $pageCount || $end > $pageCount) {
                throw new OutOfRangeException('Page range exceeds document length');
            }
            $step = $start <= $end ? 1 : -1;
            for ($i = $start; ; $i += $step) {
                $result[$i] = true;
                if ($i === $end) break;
            }
        } else {
            $page = (int) $part;
            if ($page < 1 || $page > $pageCount) throw new OutOfRangeException('Page outside document');
            $result[$page] = true;
        }
    }
    return array_keys($result);
}

This parser deliberately accepts only explicit page numbers and ranges. If you need qpdf’s relative-to-end notation, implement and document it separately rather than silently changing its meaning.

Preservation, security, and operational checks

  • Document-level data: qpdf states that outlines and tags are taken from the primary input and that page-related document data is not fully supported beyond page labels. Verify bookmarks and accessibility tags if they matter.
  • Annotations and forms: test links, form fields, JavaScript, signatures, and embedded files on representative documents. Visual equality is not structural equality.
  • Encrypted files: obtain the password through a secure mechanism and follow the chosen tool’s documented encryption options. Do not log passwords or write temporary files into a shared directory.
  • Temporary files: use restrictive permissions, unique names, cleanup in a finally block, and storage quotas. Never trust the filename supplied by a client.
  • Large or hostile PDFs: enforce download-size and processing-time limits. PDF parsing can consume substantial CPU or memory; isolate worker processes when handling untrusted uploads.
  • Verification: reopen the generated file, check its page count, and inspect the links or metadata your application promises to preserve.

Troubleshooting

“Transfer failed” or a timeout

Check DNS, TLS, firewall rules, the URL, and the connect/read timeout. A server may require authentication, cookies, or a user agent. Capture cURL’s error text, but avoid exposing credentials in logs.

HTTP 200 but “response is not a PDF”

Inspect the saved response’s first bytes and content type. Common causes are an HTML login page, consent screen, CAPTCHA, or an application error returned with status 200. Resolve access at the source rather than feeding the body to qpdf or FPDI.

“Unable to find PDF trailer” or parser exceptions

The download may be truncated, encrypted, malformed, or not a PDF at all. Compare the file size with the source, retry transient transfers, and test another representative file. A parser’s documented support is not a guarantee for every PDF structure.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

qpdf exits nonzero

Run the exact escaped command in a controlled shell, verify that qpdf is installed and executable, and read its captured stderr. Confirm that input paths exist and page expressions use one-based inclusive numbers.

Output looks right but links or bookmarks disappeared

Page import and selection primarily address page content. Check the preservation notes for your chosen tool, enable FPDI link import when appropriate, and validate required document-level structures after generation.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If the source you need is a web page rather than an existing PDF, ScreenshotNeo provides a single screenshot API request and can return PNG, JPEG, WebP, or a PDF. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

For a direct image capture, use the documented endpoint at https://screenshotneo.com/docs/:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can PHP cURL extract pages without another tool?

No. cURL transfers the PDF bytes; qpdf, FPDI, or another PDF-aware component must perform page selection.

Are qpdf page ranges zero-based?

No. qpdf uses one-based page numbers, and range endpoints are inclusive.

Should I choose qpdf or FPDI?

Choose qpdf for concise command-line page selection when a binary is acceptable. Choose FPDI when imported pages must be composed into a PHP-generated PDF.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Will selected pages retain every PDF feature?

Not necessarily. Verify links, outlines, tags, forms, metadata, and other structures required by your application.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.