printpdf 0.12.0

Rust library for reading and writing PDF files
Documentation
name: Build and test
on: [push, pull_request]

jobs:

  check-no-default-features:
    name: Check and test (no default features)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo check --no-default-features --verbose
      # Not just `--lib`: the no-`text_layout` fallback font parser is only compiled in
      # this configuration, and it was returning a blank shell (no cmap, no widths) so that
      # external fonts rendered nothing at all (#258). `tests/no_text_layout.rs` covers it,
      # and only runs here.
      - run: cargo test --no-default-features --verbose

  check-feature-combinations:
    name: Check feature combinations
    runs-on: ubuntu-latest
    strategy:
      matrix:
        features:
          # text_layout alone (without the html layer on top) is a supported public
          # configuration that no other job builds.
          - "text_layout"
          - "images"
          - "images,png"
          - "images,jpeg"
          - "images,gif"
          - "images,tiff"
          - "images,bmp"
          - "images,webp"
          - "images,png,jpeg,gif,tiff,bmp,webp"
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo check --no-default-features --features "${{ matrix.features }}" --verbose

  check-and-test-unix:
    name: Cargo check and test on Unix systems
    strategy:
      matrix:
        channel: [stable]
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - name: Install ${{ matrix.channel }} toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.channel }}
      - run: cargo check --verbose --examples
      - run: cargo check --verbose
      # Re-enabled. This was commented out as "fails because of SIMD issues, test on
      # Windows only", which meant the whole suite only ever ran on Windows — and so a
      # panic in the HTML font-subsetting path (allsorts panics outright on an empty subset
      # cmap) sat here undetected, because it only reproduces with the fonts a Linux box
      # resolves.
      - run: cargo test --verbose
      # svg is not a default feature, so its tests (Svg::parse, parse_with_fonts)
      # run nowhere else.
      - run: cargo test --verbose --features svg --test svg


  check-and-test-windows:
    name: Cargo check and test on Windows
    strategy:
      matrix:
        target-sys: ["i686-pc-windows", "x86_64-pc-windows"]
        target-abi: ["msvc"]
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install toolchain for ${{ matrix.target-sys }}-${{ matrix.target-abi }}
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target-sys }}-${{ matrix.target-abi }}
      - run: cargo check --verbose --target ${{ matrix.target-sys }}-${{ matrix.target-abi }} --examples
      - run: cargo check --verbose --target ${{ matrix.target-sys }}-${{ matrix.target-abi }}
      - run: cargo test --verbose --target ${{ matrix.target-sys }}-${{ matrix.target-abi }}

  check-wasm32:
    name: Cargo check on wasm32
    strategy:
      matrix:
        channel: [stable]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install toolchain using wasm32-unknown-unknown target
        uses: dtolnay/rust-toolchain@master
        with:
          target: wasm32-unknown-unknown
          toolchain: ${{ matrix.channel }}
      - run: rustup target add wasm32-wasip1
      - run: rustup target add wasm32-wasip2
      - run: rustup target add wasm32-unknown-unknown
      - run: cargo check --verbose --target wasm32-unknown-unknown --examples
      - run: cargo check --verbose --target wasm32-unknown-unknown
      - run: cargo check --verbose --target wasm32-wasip1 --examples
      - run: cargo check --verbose --target wasm32-wasip1
      - run: cargo check --verbose --target wasm32-wasip2 --examples
      - run: cargo check --verbose --target wasm32-wasip2
      - run: cargo check --verbose --target wasm32-wasip2 --no-default-features
      # NOT --all-features: that pulls html_multithreaded + rayon
      # (rust-fontconfig/multithreading, image/rayon) which require rayon and can't
      # compile on wasm. Check the broad wasm-safe feature set instead.
      - run: cargo check --verbose --target wasm32-wasip2 --features "gif,jpeg,png,pnm,tiff,bmp,ico,tga,hdr,dds,webp"
      # The js-sys feature (browser canvas/Date paths, 16 cfg sites) was never built by
      # any CI job — only docs.rs ever compiled it, and on a native target at that.
      - run: cargo check --verbose --target wasm32-unknown-unknown --features js-sys
      # js-sys + wasi: the browser fast paths are cfg'd out for p1/p2 (src/image.rs,
      # src/date.rs) so a wasi binary gets no browser imports — and the combination has
      # to keep compiling, since the feature is additive and unification can turn it on.
      - run: cargo check --verbose --target wasm32-wasip2 --features js-sys
      # svg (svg2pdf/resvg) compiles for browser wasm; keep it that way.
      - run: cargo check --verbose --target wasm32-unknown-unknown --features svg

  # docs.rs builds with --all-features (see [package.metadata.docs.rs]) on a native
  # builder. Nothing else compiles that exact configuration, so keep it green here
  # instead of finding out on the docs.rs build page after a release.
  check-all-features-native:
    name: Check --all-features on the docs.rs target
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo check --all-features --verbose

  # Verifies the produced PDFs with poppler, which shares no code with printpdf.
  #
  # Every other test in this repo checks printpdf against printpdf. That symmetry is how
  # issue #277 shipped: the writer emitted an empty /FontFile2, the reader happily read a
  # font resource back out, and the round-trip test stayed green while Acrobat refused the
  # file. `pdffonts` and `pdftotext` are an independent oracle for the two things users
  # actually care about — does the font embed, and is the text copy-able.
  font-verification-external:
    name: Verify fonts with external tools (poppler)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install poppler-utils
        run: sudo apt-get update && sudo apt-get install -y poppler-utils
      - run: pdffonts -v && pdftotext -v
      - run: cargo test --features html --test external_tools --test font_embedding --test font_embedding_mock --test font -- --nocapture

  # Viewer-semantics check with fontTools — independent of printpdf, poppler AND
  # allsorts. Resolves every Identity-H code the way Acrobat/Preview do (ISO
  # 32000-1, 9.7.4.2): code -> CID -> CFF charset -> glyph, then cross-checks
  # that glyph against the char the font's own cmap assigns, /W widths against
  # hmtx, and ToUnicode against the exact source text.
  #
  # This is the oracle for #280: printpdf wrote glyph ids as Identity-H codes for
  # CID-keyed CFF fonts (NotoSansJP's charset diverges from gid 365 on). PDFium
  # falls back to code == GID, so Chrome looked fine — poppler's pdftotext reads
  # ToUnicode, so text extraction looked fine — while Acrobat and Preview drew
  # wrong or missing glyphs. Only charset-resolution semantics catch it, and no
  # Rust-side test is fully independent of the crate's own CFF parsing.
  font-verification-viewer-semantics:
    name: Verify Identity-H codes with fontTools (Acrobat semantics, #280)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install fontTools
        run: pip install fonttools
      - name: Build the otf-font example PDFs (full embed + subset)
        run: |
          cargo run --example otf-font
          cargo run --example otf-font-subset
      - name: Verify code/charset/W/ToUnicode consistency against the source text
        run: |
          python3 scripts/verify_pdf_font.py font.pdf --expect scripts/otf_font_expected.txt
          python3 scripts/verify_pdf_font.py font_subset.pdf --expect scripts/otf_font_expected.txt
      - name: Mock fonts regenerate byte-identically (defined-metrics oracle stays intact)
        run: |
          python3 scripts/gen_mock_fonts.py
          git diff --exit-code tests/assets/fonts/mock/

  # Builds the working tree the way a CONSUMER resolves it — fresh project, fresh
  # lockfile, path-dependency on the checkout. A "today's freshest" resolution can land
  # on the one dependency version that happens to work while the declared version
  # *ranges* still admit broken graphs; every green job above proves only that single
  # resolution. That is how #279 shipped: fresh resolution on a current toolchain picks
  # time 0.3.53 and lopdf 0.44's `time` feature compiles, while any consumer whose
  # lockfile predates time 0.3.48 (or whose MSRV-aware resolver walks below it) got a
  # compile error inside lopdf. This job builds the consumer shape and then re-pins the
  # known-bad dependency lines, keeping the once-shipped breakage scenarios red forever.
  consumer-resolution:
    name: Fresh consumer resolution (ignore committed lockfile)
    runs-on: ubuntu-latest
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        include:
          - { args: "" }
          - { args: "--no-default-features" }
          - { args: "--no-default-features --features jpeg", regression279: true }
          - { args: "--no-default-features --features svg" }
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Build as a consumer would (fresh lockfile, registry dependencies)
        env:
          ADD_ARGS: ${{ matrix.args }}
        run: |
          set -euo pipefail
          cargo new "$RUNNER_TEMP/consumer"
          cd "$RUNNER_TEMP/consumer"
          # $ADD_ARGS is intentionally word-split.
          cargo add printpdf --path "$GITHUB_WORKSPACE" $ADD_ARGS
          cargo check
      - name: "Regression: known-bad old pins in the consumer lockfile must still build"
        if: matrix.regression279 == true
        run: |
          set -euo pipefail
          cd "$RUNNER_TEMP/consumer"
          # Both pins are printpdf's declared dependency FLOORS. A floor that does not
          # compile is exactly how #279 shipped (lopdf's `time` feature broke on every
          # time <= 0.3.47; pinning our 0.3.36 floor replays that scenario and more):
          #   - time 0.3.36: declared floor; 0.3.25–0.3.35 fail inference on new rustc.
          #   - image 0.25.2: declared floor; `image::ImageReader` first exists there.
          # Pin failures (cargo refuses because the graph's floors moved) are skips,
          # not errors — the scenario has become inexpressible, which is fine.
          PINNED=false
          if cargo update -p time --precise 0.3.36; then PINNED=true; else
            echo "::notice::time 0.3.36 no longer pinnable in this graph — skipping that scenario."
          fi
          if cargo update -p image --precise 0.25.2; then PINNED=true; else
            echo "::notice::image 0.25.2 no longer pinnable in this graph — skipping that scenario."
          fi
          if [ "$PINNED" = true ]; then
            cargo check
          fi

  # THE RELEASE GATE.
  #
  # `cargo publish` STRIPS [patch.crates-io]. A crate published while that section exists
  # is built against dependencies nobody ever compiled or tested against — the local
  # checkout and the published artifact are different programs. That is exactly how 0.10.0
  # shipped broken: locally azul-layout came from a git branch whose
  # `ParsedFont::from_bytes` retains the source font bytes; on crates.io it resolved to
  # azul-layout 0.0.9, whose `from_bytes` does not, so every embedded font became an empty
  # /FontFile2 (#277). The same trap then bit azul itself twice while releasing 0.0.10.
  #
  # printpdf carries no [patch] any more. This job keeps it that way: it fails outright if
  # one reappears, then runs the font suite against the real registry dependency graph —
  # which is now, by construction, the one users get.
  publish-safety:
    name: No [patch], and the published dep graph passes the font suite
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install poppler-utils
        run: sudo apt-get update && sudo apt-get install -y poppler-utils
      - name: Refuse to build with a [patch.crates-io] section
        run: |
          if grep -q '^\[patch\.' Cargo.toml; then
            echo "::error::Cargo.toml has a [patch] section. cargo publish strips it, so the"
            echo "::error::published crate would be built against untested dependencies."
            echo "::error::Publish the patched dependency to crates.io and pin it instead."
            grep -n '^\[patch\.' -A5 Cargo.toml
            exit 1
          fi
          echo "no [patch] section — what we build is what we publish"
      - name: Package size stays under the crates.io cap
        run: |
          set -euo pipefail
          # Everything under examples/assets is include_bytes!'d by tests/examples and
          # must ship, which parks the package at ~87% of crates.io's 10 MiB limit. One
          # carelessly added asset and `cargo publish` refuses mid-release. --no-verify:
          # this step only measures; the jobs around it do the building.
          cargo package --no-verify
          CRATE=$(ls target/package/*.crate)
          SIZE=$(stat -c%s "$CRATE")
          LIMIT=10485760
          echo "package: $CRATE = $SIZE bytes ($(( SIZE * 100 / LIMIT ))% of the crates.io limit)"
          if [ "$SIZE" -gt $(( LIMIT * 95 / 100 )) ]; then
            echo "::error::.crate is $SIZE bytes — over 95% of crates.io's $LIMIT-byte limit. Remove or shrink assets before releasing."
            exit 1
          fi
      - name: Dependency versions a published printpdf resolves to
        run: cargo tree --features html -i azul-layout || true
      - run: cargo test --features html --test external_tools --test font_embedding --test font --test no_text_layout || true
      - run: cargo test --features html --test external_tools --test font_embedding --test font
      - run: cargo test --no-default-features --test no_text_layout

  generate-pdf-artifacts:
    name: Generate PDF artifacts
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable

      # No-feature examples
      - name: Run no-feature examples
        run: |
          for ex in advanced bookmarks customfont graphics layers multipage text otf-font; do
            echo "=== Running $ex ==="
            cargo run --example "$ex" || true
          done

      # Image example
      - name: Run image example
        run: cargo run --example image --features "images,png" || true

      # SVG example
      - name: Run svg example
        run: cargo run --example svg --features svg || true

      # HTML-feature examples
      - name: Run HTML examples
        run: |
          for ex in html css_shapes html_full html_full_debug shape pagination_test \
                    baseline_alignment table_debug debug_pagination debug_displaylist \
                    pagination_boundary_test margin_collapse margin_collapse_inline \
                    margin_collapse_border_test margin_collapse_comprehensive; do
            echo "=== Running $ex ==="
            cargo run --example "$ex" || true
          done

      # Collect and upload PDFs
      - name: Collect PDFs
        run: |
          mkdir -p pdf-artifacts
          find . -maxdepth 1 -name '*.pdf' -exec cp {} pdf-artifacts/ \;
          ls -la pdf-artifacts/
      - name: Upload PDF artifacts
        uses: actions/upload-artifact@v4
        with:
          name: pdf-output-${{ github.sha }}
          path: pdf-artifacts/
          retention-days: 30