Skip to content

Save a picture from a PDF with the context menu - #1144

Merged
trypsynth merged 2 commits into
trypsynth:masterfrom
dan1982code:pdf-save-image
Oct 20, 2026
Merged

trypsynth merged 2 commits into
trypsynth:masterfrom
dan1982code:pdf-save-image

Conversation

@dan1982code

Copy link
Copy Markdown
Contributor

The report

The context menu's Save image as… item, added for pictures in EPUB, HTML, Markdown, CHM, FB2 and
comic books, never appears in a PDF. The reader still announces the picture — [Image] or
[Figure: description] — and F/G navigation stops on it, so the file plainly knows where the
picture is; it just had no way to name it.

The other formats name a picture by a path: an entry in the archive, a file beside the HTML, or a
data: URI. A PDF has none of those. Its pictures are objects in a page's content stream, reached
through a form XObject or not, so the marker the parser writes carried an empty reference and
image_file_name_at returned None. The menu item is only added when that returns Some, which is
why it was missing rather than failing when pressed.

The change

The marker now names the picture. parser/pdf/images.rs's append_image takes a reference and
stores it on the marker. Every image line the PDF parser writes gets one:

  • An untagged page names the object by its position in the content stream, as
    pdf:<page>:<path>, where a path is slash-separated indices: 0 is the page's first top-level
    object, 0/2 the third object inside it when it is a form. The parser already had the top edge
    and the path of each announced image (PageContent::image_paths), so the line it wrote can now
    say which one it is.
  • A tagged page that claims no figures places its unclaimed images by height the same way
    (UnclaimedImages now carries tops and paths).
  • A tagged page that does claim figures names its Figure element's picture by marked-content id
    (structure.rs). find_image_path walks the element's subtree for a leaf id that is on an image
    object, which is how pdfium reports the association (FPDFPageObj_GetMarkedContentID and the
    MCID parameter on each mark). The lookup covers images Paperback does not announce, such as a
    page-sized picture, so a figure whose /Alt describes a full-page illustration can still save it.
    PdfPage::image_paths_by_mcid builds it, and the walk only builds that map when the tree contains
    a figure at all.

The picture comes out of pdfium at its stored resolution. pdfium/page.rs gained
PdfPage::image_png(path), which resolves the path through FPDFPage_GetObject and
FPDFFormObj_GetObject, then renders the object with FPDFImageObj_GetBitmap — ignoring the page
matrix, so a photo placed small still saves at full size — and converts whatever layout pdfium
returns (BGRA, BGRx, BGR or Gray, each with its own stride padding) into RGBA before encoding PNG.
The image is re-encoded rather than passed through as the file's original JPEG/Flate bytes, because
FPDFImageObj_GetImageDataRaw returns the stream still filtered rather than a viewable file.

Saving in session/images.rs branches on the pdf: reference before the document extension,
since the reference itself says which reader is needed. It reopens the document to read the
picture; DocumentSession now keeps the password it was opened with so a protected file can be
looked at a second time. The save dialog offers a name like image 25 453.png — the translated stem,
the 1-based page number and the object path — so several pictures from one book do not collide.

doc/readme.md lists PDFs among the formats whose pictures can be saved. No new translatable
strings: the menu items and the image file-name stem are the existing ones.

What it does not do

  • The picture is always written as PNG. A JPEG photo therefore saves losslessly but larger than the
    bytes inside the file.
  • An image mask (/SMask or a stencil mask) is not applied. Pdfium keeps the mask as a separate
    object; the picture on its own is what was asked for, and a masked image with transparency saves
    with the transparent pixels opaque.
  • A Figure whose content names no image on the page still gets its line and its description, but
    no menu item, because there is nothing to write.

Testing

  • demo.pdf page 25 has an image at the top; saving it produced a 401×168 PNG of the
    "Chapter 2 / The place from where we started" heading graphic, which renders correctly.
  • A sweep of all 49 saveable image lines in demo.pdf each wrote a valid PNG. That file is
    untagged, so it exercises the content-stream path.
  • A hand-written tagged PDF whose /Figure element wraps a 2×2 red image with /Alt (A red square) reads as [Figure: A red square] and saves a 2×2 red PNG, which exercises the
    marked-content-id path.
  • cargo test -p paperback-core: 1500 passed, 0 failed. New unit tests cover the reference stored
    on the marker and the PDF file name; the bitmap conversion tests cover BGRA, BGR, Gray and padded
    rows.
  • cargo +nightly-2026-09-27 fmt --check and clippy are clean for the touched crates.

🤖 Generated with Claude Code

dan1982code and others added 2 commits October 19, 2026 12:37
A PDF names its picture by where its object sits rather than by a path, so
the image lines the parser wrote carried no reference at all and the
context menu's "Save image as..." never appeared on one.

The marker now carries `pdf:<page>:<content-stream path>`, written for an
untagged page's image objects, for the images a tagged page's tree never
claims, and for a real Figure element's picture (found through the
marked-content ids its content names). The picture is read back through
pdfium at the resolution the file stores it in, converted from whichever
bitmap layout pdfium hands back, and written as PNG; a protected file is
reopened with the password the session was given.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trypsynth
trypsynth merged commit 8012054 into trypsynth:master Oct 20, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants