Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Automate PDF Page Checks on a Hosted Mac

If PDFs arrive from design agencies, clients, or automated exports, you need a reproducible way to verify each file meets your spec before it hits print, a document system, or a downstream pipeline. A hosted Mac keeps PDFKit and Homebrew's pdfinfo and pdffonts running continuously, without you managing hardware or leaving files on a shared CI runner. Start with Figment's $29/month virtual Intel macOS environment (2 vCPU, 8 GiB RAM, 80 GiB disk) for file-based checks; move to the M-series tier from $95 when you need Apple silicon execution or heavier parallelism, and confirm the exact chip and RAM in your quote.

Choose a Mac plan and define the job

PDF page validation is CPU-light: reading page counts, dimensions, and font tables with pdfinfo and pdffonts typically takes well under a second per file. Figment's virtual Intel macOS environment at $29/month is the right fit for most document validation workloads. macOS ships with mdls for Spotlight metadata, and Homebrew provides pdfinfo and pdffonts from the poppler package with a single install.

Consider a different Figment plan when:

  • You need concurrent batches for separate client accounts and the base VM's 8 GiB RAM becomes the bottleneck — request an i3 Mac at $49 or M-series from $95.
  • Your pipeline also needs to execute arm64-only tooling or sign and archive an iOS build alongside the PDF job. The virtual Intel environment runs Intel toolchains (Xcode 26 or earlier per Apple's Intel transition guidance), which can cross-compile arm64 and universal binaries. Native Apple silicon execution and Xcode 27 require the M-series tier.
  • You want CI setup, backups, monitoring, and 30 minutes of monthly support bundled — the Managed Build Mac at $249 includes exactly those four items on one M-series Mac.

The i3 $49 and physical M-series tiers currently collect requests until supply is confirmed, so treat availability as part of the quote, not a stock promise. Access on the $29 VM is through the browser console, where you can open a desktop terminal and run brew, git, and the scripts below; outbound agents can connect to any CI service you configure.

The PDF diagnostic worksheet

Map quality requirements to concrete checks before writing code. This worksheet defines each check, the tool that runs it, and what failure means. Run it manually on 20 representative files from your source folder before automating anything — unexpected dimension variance and unembedded subset fonts almost always surface on production samples, not synthetic test PDFs.

CheckToolPass criterionOn fail
File is readable, not corruptedpdfinfo exit codeExit 0Reject; alert operator
Page count matches manifestpdfinfo Pages fieldExact match to expectedFlag PAGE_COUNT_MISMATCH
No suspiciously small pagespdfinfo -f 1 -l N Page sizeWidth ≥ 72 pt and height ≥ 72 pt on every pageFlag TINY_PAGE
Page dimensions within specpdfinfo Page sizeWithin ±2 pt of targetFlag to review when a target size is known
All fonts embeddedpdffonts emb columnNo no entriesFlag UNEMBEDDED_FONTS
File is not encryptedpdfinfo Encrypted fieldnoLog and skip
At least one pagepdfinfo Pages fieldPages ≥ 1Reject EMPTY

Manual acceptance procedure — ten minutes per batch, before you trust any automation:

  1. Pick five known-good PDFs and five known-bad PDFs from recent jobs. The bad set should include one truncated file, one encrypted file, one with a 10×10 pt thumbnail page, one with an unembedded Helvetica, and one with the wrong page count.
  2. Run pdfinfo <file> on each and record the Pages, Encrypted, and Page size lines. For PDFs with mixed page sizes, repeat with pdfinfo -f 1 -l <N> <file> to inspect every page — the default Page size line reports only the first page.
  3. Run pdffonts <file> on each and count rows where the emb column is no.
  4. Fill the worksheet by hand. Confirm every bad file flags the category you expected and every good file passes.
  5. Only then wire the same logic into pdf_check.sh below, and re-run the ten-file set through the script to confirm its output matches your hand answers.

Inputs and required permissions

The check script needs:

  • Read access to the PDF drop folder (SFTP upload you host, S3 sync from a client you install, or a mounted share).
  • Write access to a results directory for the TSV reports.
  • Homebrew with poppler installed: brew install poppler.
  • No TCC prompts: file-based PDF processing is not gated by Accessibility or Screen Recording.

mdls -name kMDItemNumberOfPages can return page counts, but only once Spotlight has indexed the file. For automation where files are processed seconds after arrival, read the PDF directly with pdfinfo — it parses the file structure and does not depend on the index state.

Implement the concrete workflow

Install poppler once: brew install poppler. Save the following as pdf_check.sh. This is an educational example, not an executed benchmark — run it against the worksheet samples above before trusting its output, and adjust the thresholds to your spec.

#!/usr/bin/env bash
# pdf_check.sh — page count, dimensions, and font check for a directory of PDFs
# Usage: ./pdf_check.sh <input-dir> <results-dir> [expected-pages]
set -euo pipefail

INPUT_DIR="$1"
RESULTS_DIR="$2"
EXPECTED_PAGES="${3:-}"
mkdir -p "$RESULTS_DIR"
REPORT="${RESULTS_DIR}/report_$(date +%Y%m%d_%H%M%S)_$$.tsv"

printf 'file\tpages\twidth_pt\theight_pt\tunembedded_fonts\tencrypted\tstatus\n' \
  > "$REPORT"

check_pdf() {
    local pdf="$1"
    local name status="OK"
    name=$(basename "$pdf")

    local info
    if ! info=$(pdfinfo "$pdf" 2>&1); then
        printf '%s\t-\t-\t-\t-\t-\tPDFINFO_ERROR\n' "$name" >> "$REPORT"
        return 0
    fi

    local pages encrypted width_pt height_pt
    pages=$(printf '%s\n' "$info"     | awk '/^Pages:/{print $2; exit}')
    encrypted=$(printf '%s\n' "$info" | awk '/^Encrypted:/{print $2; exit}')
    # "Page size: 595.28 x 841.89 pts (A4)" — first-page dimensions only.
    width_pt=$(printf '%s\n' "$info"  | awk '/^Page size:/{print $3; exit}')
    height_pt=$(printf '%s\n' "$info" | awk '/^Page size:/{print $5; exit}')

    [[ "${pages:-0}" -eq 0 ]] && status="EMPTY"
    [[ "${encrypted:-no}" != "no" ]] && status="${status};ENCRYPTED"
    [[ -n "$EXPECTED_PAGES" && "${pages:-0}" != "$EXPECTED_PAGES" ]] \
        && status="${status};PAGE_COUNT_MISMATCH"

    local w_int h_int
    w_int=$(awk -v v="${width_pt:-0}"  'BEGIN{printf "%d", v}')
    h_int=$(awk -v v="${height_pt:-0}" 'BEGIN{printf "%d", v}')
    [[ "$w_int" -lt 72 || "$h_int" -lt 72 ]] && status="${status};TINY_PAGE"

    # pdffonts type can be 1-3 words ("TrueType", "Type 1", "CID Type 0C"),
    # so counting fields from the right is unreliable. Find the 'emb'
    # column's byte offset in the header, then read the same offset on
    # each data row.
    local unembedded
    unembedded=$(pdffonts "$pdf" 2>/dev/null | awk '
        NR==1 { c = index($0, "emb"); next }
        NR==2 { next }
        c && substr($0, c, 3) == "no " { n++ }
        END { print n+0 }
    ') || unembedded=0
    unembedded="${unembedded:-0}"
    [[ "$unembedded" -gt 0 ]] && status="${status};UNEMBEDDED_FONTS"

    status="${status#;}"

    printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
        "$name" "${pages:-0}" "${width_pt:--}" "${height_pt:--}" \
        "$unembedded" "${encrypted:-unknown}" "$status" \
        >> "$REPORT"
}

find "$INPUT_DIR" -type f -name '*.pdf' -print0 \
  | while IFS= read -r -d '' pdf; do
        check_pdf "$pdf" || printf '%s\t-\t-\t-\t-\t-\tSCRIPT_ERROR\n' \
            "$(basename "$pdf")" >> "$REPORT"
    done

echo "Report written to $REPORT"

Make it executable and run against the small sample from the worksheet:

chmod +x ~/scripts/pdf_check.sh
~/scripts/pdf_check.sh ~/pdf-samples ~/pdf-results 2
column -t -s $'\t' ~/pdf-results/report_*.tsv | head -20

The TSV opens directly in Numbers, imports into Airtable, or feeds a Slack alert step. Each row is one file; the status column carries semicolon-delimited failure codes so you can grep for specific problems:

grep 'UNEMBEDDED_FONTS' ~/pdf-results/report_*.tsv

If your PDFs may contain pages with mixed dimensions, replace the single Page size read with a loop that invokes pdfinfo -f N -l N for each page index from 1 through Pages, and apply the tiny-page and spec checks to every page before writing the row.

Schedule with launchd

Use the QueueDirectories key to process files as they arrive in a spool folder. Apple's documentation states that launchd keeps the job running while the listed directories are non-empty and stops it when they drain.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.yourdomain.pdf-check</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/agent/scripts/pdf_check.sh</string>
        <string>/Users/agent/pdf-spool</string>
        <string>/Users/agent/pdf-results</string>
    </array>
    <key>QueueDirectories</key>
    <array>
        <string>/Users/agent/pdf-spool</string>
    </array>
    <key>StandardOutPath</key>
    <string>/Users/agent/Library/Logs/pdf_check/out.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/agent/Library/Logs/pdf_check/err.log</string>
</dict>
</plist>

Save as ~/Library/LaunchAgents/com.yourdomain.pdf-check.plist and load with launchctl load ~/Library/LaunchAgents/com.yourdomain.pdf-check.plist. launchd starts the job whenever the spool directory is non-empty and stops when it drains — no polling loop and no wasted CPU between batches.

Failure and recovery checks

PDFINFO_ERROR — the file is corrupted or truncated. Move it to a quarantine/ folder and alert. Do not retry blindly; a truncated upload may still be in flight, so pair this with a size-stable check (same byte count across two reads 10 seconds apart) before processing.

Mid-batch crash — keep the job idempotent by appending each successful filename to a processed.txt manifest. On restart, skip filenames already in the manifest with a grep -qF check.

Encrypted files — log and skip rather than crash the batch. If encrypted PDFs in the spool indicate a producer-side error, route them to a separate alert rather than the common rejects.

Disk fill — the 80 GiB VM disk also holds macOS, Homebrew, and your tooling, so treat only part of that as usable spool and results space. Sync completed reports off-host and move originals to a completed folder or your own storage immediately after each batch.

Cost and operating limits

Volume per dayAvg file sizeApprox run timeCandidate plan
200 PDFs2–5 MBunder 5 min$29 virtual Intel macOS
1,000 PDFs2–5 MB15–25 min$29 virtual Intel macOS
1,000 PDFs50 MB45–90 mini3 $49 (request availability)
5,000 PDFs20 MB2–4 hoursi3 $49 or M-series from $95 (request)

These are planning estimates, not measured benchmarks. Run the script on 50 representative files from your production set and record the wall time before choosing a plan: pdffonts on a heavily fonted 100-page print file takes noticeably longer than a two-page invoice.

Start or migrate the workload

  1. Request a Figment hosted Mac at /mac-hosting. The $29 virtual Intel macOS environment covers file-based PDF checks; request an M-series only if you need Apple silicon execution or heavier parallelism, and agree the exact chip and RAM in the quote.
  2. Install poppler: brew install poppler.
  3. Upload pdf_check.sh into ~/scripts/ through the browser console session, or clone it from your Git repository in the VM's desktop terminal.
  4. Create spool and results directories: mkdir ~/pdf-spool ~/pdf-results.
  5. Install and load the launchd plist above.
  6. Drop a test PDF into ~/pdf-spool and confirm a report_*.tsv lands in ~/pdf-results.
  7. Wire in your own alerting on non-zero exit (post to a Slack webhook from a wrapper script, or rely on your log aggregator).

Configure your plan and start →

Related guides: Run a Mac API agent runtime · Reboot recovery for Mac agents · Self-hosted GitHub Actions runner on Mac


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms