pdf

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 25 Imported by: 0

README

fyne-pdf

CI Coverage Go Reference

A PDF viewer widget for Fyne, written in pure Go.

Pages are rendered with cera — no MuPDF, no cgo PDF library, no external binaries.

Features

  • Document widget: page view with thumbnail strip, zoom slider, zoom in/out, 100 % (physical size), fit page / fit width / fit height
  • Two view modes: one page at a time, or all pages one below the other, scrolled through without a break (SetViewMode(pdf.ViewContinuous) or the toolbar button). Only the pages in view get objects and renders, so documents with hundreds of pages scroll smoothly
  • Viewer gestures: Ctrl+wheel zoom around the pointer, page turns when scrolling past a page's edge (single page mode), drag to pan, double tap for fit/100 %; keyboard: Page Up/Down, Space, Home/End, arrow keys, Ctrl +/-/0
  • Rendering in the background, the UI never waits for a page; pages in view first, then their neighbours; the resolution of the main view follows the zoom level and the screen scale, the page images kept share a memory budget
  • Text search: Search finds a query in the background (match case, whole words, ignore diacritics), highlights the results on the pages and scrolls to the current one; NextSearchResult / PreviousSearchResult (also F3 / Shift+F3), ShowSearchResult(i) and SearchResults() for a result list
  • Optional save button via SaveCallback, render errors via OnError
  • Source: goroutine-safe access to an opened PDF — page count, page bounds, rendering at arbitrary DPI, document metadata, page text and search
  • NewImageFromMemory: render a single page to an image.Image without any UI
  • Render timeout per page (15 s); slow pages fall back to the partially drawn image instead of blocking the UI

Installation

go get github.com/timzifer/fyne-pdf

Requires Go 1.27.1+ and Fyne 2.6+ (uses fyne.Do). Fyne itself needs a C compiler and the platform graphics headers — see the Fyne prerequisites.

Usage

Viewer widget
package main

import (
	"os"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	pdf "github.com/timzifer/fyne-pdf"
)

func main() {
	a := app.New()
	w := a.NewWindow("PDF")

	contents, err := os.ReadFile("example.pdf")
	if err != nil {
		panic(err)
	}

	doc := pdf.NewDocument()
	defer doc.Close()
	if err := doc.LoadFromMemory(contents); err != nil {
		panic(err)
	}

	// doc.SetViewMode(pdf.ViewContinuous) // all pages one below the other
	// doc.ShowPage(2)                     // jump to the third page

	w.SetContent(doc)
	w.Resize(fyne.NewSize(1024, 768))
	w.ShowAndRun()
}

A complete viewer with an open dialog, save button, printing, search and error reporting is in examples/viewer:

cd examples/viewer
go run . [-print-dialog native|fyne] [-find text] [file.pdf]

The example is a module of its own that builds against the library in this repository (replace in its go.mod), so the printing packages stay out of the library's dependencies.

File > Print (Ctrl+P) prints through goprint: -print-dialog native (the default) shows the platform's print dialog, -print-dialog fyne the dialog fyneprint draws with Fyne, with a preview and a "Save as PDF" button.

Edit > Find (Ctrl+F) opens a search bar with options and a result count; the results are listed beside the pages (search.go).

doc.OnSearchChanged = func() { // on the main goroutine
	results := doc.SearchResults() // Page, Prefix, Text, Suffix, Rects
	fmt.Println(len(results), "found, current:", doc.CurrentSearchResult(), "searching:", doc.Searching())
}
doc.Search("gamma", pdf.SearchOptions{WholeWord: true})
doc.NextSearchResult()  // or F3 in the page view
doc.ShowSearchResult(3) // scroll to a result, e.g. picked from a list
doc.ClearSearch()

The results are highlighted on the pages (HighlightColor, CurrentHighlightColor). Without UI, Source.Search, Source.SearchPage and Source.PageText return the results and the text; Rects are in points of the displayed page, origin top left. Page.SetHighlights marks any areas of a page.

Render a page to an image
img, err := pdf.NewImageFromMemory(contents, 0) // page index is 0-based
Low-level access
src, err := pdf.OpenSource(contents)
if err != nil {
	return err
}
defer src.Close()

fmt.Println(src.PageCount(), src.Metadata().Title)
img, err := src.RenderPage(0, 300) // 300 dpi

Status

Usable, but young. The API may still change before v1.0.0. Not supported yet: text selection, search, links, annotations.

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md. For rendering bugs, please attach the PDF (or a minimal one reproducing the issue) if you can share it.

License

MIT

Documentation

Overview

Package pdf provides a PDF viewer widget for Fyne, written in pure Go.

Document is a ready-to-use viewer with a thumbnail strip and zoom controls. Source gives goroutine-safe low-level access to an opened PDF (page count, bounds, rendering, metadata), and NewImageFromMemory renders a single page without any UI.

Page indices are 0-based throughout the package.

Index

Examples

Constants

View Source
const ThumbnailMinimumDimension = float32(100)

ThumbnailMinimumDimension is the default width and height of a thumbnail.

Variables

This section is empty.

Functions

func NewImageFromMemory

func NewImageFromMemory(contents []byte, pageNumber int, documentCallbacks ...DocumentCallbackFn) (image.Image, error)

NewImageFromMemory renders page pageNumber (0-based) of the PDF in contents at 144 dpi. documentCallbacks are called with the opened Source before rendering.

Example
package main

import (
	"fmt"
	"os"

	pdf "github.com/timzifer/fyne-pdf"
)

func main() {
	contents, err := os.ReadFile("example.pdf")
	if err != nil {
		panic(err)
	}

	img, err := pdf.NewImageFromMemory(contents, 0)
	if err != nil {
		panic(err)
	}
	fmt.Println(img.Bounds().Size())
}

Types

type Document

type Document struct {
	widget.BaseWidget

	// BackgroundColor fills the area around the pages. If nil, the
	// theme's separator color is used.
	BackgroundColor color.Color
	// SaveCallback, if set, shows a save button in the toolbar that calls
	// it. Call Refresh after changing it.
	SaveCallback func()
	// OnError is called on the main goroutine when a page cannot be
	// rendered. If nil, the error is logged.
	OnError func(page int, err error)
	// OnSearchChanged is called on the main goroutine when the search
	// changes: results found, the current result, the search done or
	// cleared. See [Document.Search].
	OnSearchChanged func()
	// HighlightColor fills the search results on the pages,
	// CurrentHighlightColor the current one. If nil, translucent
	// yellow and orange are used.
	HighlightColor, CurrentHighlightColor color.Color
	// contains filtered or unexported fields
}

Document is a PDF viewer widget: a page view with a toolbar for zooming and a thumbnail strip for navigation. Create it with NewDocument.

The page view shows one page at a time or, see Document.SetViewMode, all pages one below the other. It supports the usual viewer gestures: Ctrl+wheel (Cmd on macOS) zooms around the pointer, dragging pans, a double tap switches between the fitted page and 100 %; showing one page, scrolling on past its top or bottom turns it. Once clicked, it takes the keyboard focus: Page Up/Down and Space scroll by a screen, Home/End go to the first and last page, the arrow keys scroll (Left/Right go to the previous and next page while the pages fit horizontally), Ctrl +/-/0 zoom.

Document.Search finds text in the background and highlights the results on the pages; F3 and Shift+F3 go to the next and previous one.

Pages are rendered in the background, those in view first; the resolution of the main view follows the zoom level. Methods must be called from the Fyne main goroutine (or before the app runs).

func NewDocument

func NewDocument() *Document

NewDocument creates an empty viewer. Load a PDF with Document.LoadFromMemory.

func (*Document) ClearSearch added in v0.6.0

func (d *Document) ClearSearch()

ClearSearch ends the search and removes its highlights.

func (*Document) Close

func (d *Document) Close() error

Close releases the loaded PDF and stops pending renders. Pages already shown stay visible.

func (*Document) CreateRenderer

func (d *Document) CreateRenderer() fyne.WidgetRenderer

func (*Document) CurrentPage

func (d *Document) CurrentPage() int

CurrentPage returns the index (0-based) of the page shown in the main view. Showing all pages, it is the page at the centre of the view, or the one last gone to with Document.ShowPage or a key until the view is scrolled.

func (*Document) CurrentSearchResult added in v0.6.0

func (d *Document) CurrentSearchResult() int

CurrentSearchResult returns the index of the current result into SearchResults, or -1 if there is none.

func (*Document) LoadFromMemory

func (d *Document) LoadFromMemory(contents []byte) error

LoadFromMemory opens the PDF in contents and shows its first page. A previously loaded PDF is closed. Pages are rendered in the background.

func (*Document) NextSearchResult added in v0.6.0

func (d *Document) NextSearchResult()

NextSearchResult makes the result after the current one current, wrapping around to the first, and scrolls to it. Without a current result, it goes to the first result at or after the current page.

func (*Document) PageCount

func (d *Document) PageCount() int

PageCount returns the number of pages of the loaded PDF.

func (*Document) PreviousSearchResult added in v0.6.0

func (d *Document) PreviousSearchResult()

PreviousSearchResult makes the result before the current one current, wrapping around to the last, and scrolls to it. Without a current result, it goes to the last result at or before the current page.

func (*Document) Refresh

func (d *Document) Refresh()

func (*Document) Resize

func (d *Document) Resize(size fyne.Size)

Resize lays out the widget. While the page is fitted to the view (after ShowPage or one of the ZoomToFit methods), the fit follows the new size.

func (*Document) Search added in v0.6.0

func (d *Document) Search(query string, opt SearchOptions)

Search finds query in the document, in the background: pages from the current one to the end first, then those before it. The results are highlighted on the pages as they are found; the first one at or after the current page becomes the current result, and the view scrolls to it. OnSearchChanged reports the progress. An empty query, like Document.ClearSearch, ends the search and removes the highlights. Loading another document ends it, too.

func (*Document) SearchProgress added in v0.6.0

func (d *Document) SearchProgress() (searched, total int)

SearchProgress returns the number of pages searched so far and the number of pages to search.

func (*Document) SearchQuery added in v0.6.0

func (d *Document) SearchQuery() string

SearchQuery returns the query of the last search, "" after ClearSearch.

func (*Document) SearchResults added in v0.6.0

func (d *Document) SearchResults() []SearchResult

SearchResults returns the results found so far, sorted by page and in reading order on a page. While the search runs, results of pages before the page it started at are inserted in front of those found earlier.

func (*Document) Searching added in v0.6.0

func (d *Document) Searching() bool

Searching reports whether the search is still going through the pages.

func (*Document) SetViewMode added in v0.5.0

func (d *Document) SetViewMode(mode ViewMode)

SetViewMode switches between showing one page at a time (ViewSinglePage, the default) and all pages one below the other (ViewContinuous). The current page stays in view, as do the zoom and the fit. The toolbar has a button for it, too.

func (*Document) ShowPage

func (d *Document) ShowPage(page int)

ShowPage makes page (0-based) current. Showing one page at a time, it shows page fitted into the view. Showing all pages, it scrolls to the top of page and keeps the zoom, or the fit applied to page.

func (*Document) ShowSearchResult added in v0.6.0

func (d *Document) ShowSearchResult(i int)

ShowSearchResult makes result i (an index into SearchResults) the current result, highlighted in CurrentHighlightColor, and scrolls the view so it is visible: to its page and, unless it is in view already, with the result at the centre of the view.

func (*Document) ViewMode added in v0.5.0

func (d *Document) ViewMode() ViewMode

ViewMode returns how the main view shows the pages.

func (*Document) Zoom

func (d *Document) Zoom(scale float64)

Zoom sets the zoom factor; 1 shows the page at roughly its physical size. The factor is clamped to 0.2 to 4.

func (*Document) ZoomToFit

func (d *Document) ZoomToFit()

ZoomToFit scales the current page so it fits completely into the view; showing all pages, it also scrolls to the top of that page. The fit is kept when the view is resized, until the zoom is changed otherwise; the same holds for ZoomToFitVertical and ZoomToFitHorizontal.

func (*Document) ZoomToFitHorizontal

func (d *Document) ZoomToFitHorizontal()

ZoomToFitHorizontal scales the current page to the width of the view.

func (*Document) ZoomToFitVertical

func (d *Document) ZoomToFitVertical()

ZoomToFitVertical scales the current page to the height of the view.

type DocumentCallbackFn

type DocumentCallbackFn func(src *Source)

DocumentCallbackFn is called with the opened Source before rendering, see NewImageFromMemory.

type Highlight added in v0.6.0

type Highlight struct {
	Position fyne.Position
	Size     fyne.Size
	Color    color.Color
}

Highlight marks an area of a Page, such as a search result: a rectangle in the units of Page.PageSize (at Scale 1, origin at the top left of the page), filled with Color over the page image. Use a translucent color to keep the text readable.

type Metadata added in v0.5.0

type Metadata struct {
	Title, Author, Subject, Keywords, Creator, Producer string

	// Created and Modified are zero when absent or not a date.
	Created, Modified time.Time

	// Trapped is "True", "False", "Unknown" or "".
	Trapped string

	// Custom holds the other string entries of /Info, by key as written
	// in the document ("Department", ...).
	Custom map[string]string

	// XMP is the catalog's /Metadata stream, decoded but not parsed; nil if
	// there is none.
	XMP []byte
}

Metadata is what a document says about itself: its document information dictionary (/Info), decoded, and its XMP metadata stream.

type Page

type Page struct {
	widget.BaseWidget

	Scale           float64
	BackgroundColor color.Color
	// contains filtered or unexported fields
}

Page displays a single rendered page image at Page.PageSize multiplied by Scale, which is also its minimum size. A larger widget centres the page on BackgroundColor. The image is scaled to the page size, so it can be rendered at any resolution; the aspect ratio is kept. Highlights, see Page.SetHighlights, are drawn over the image.

func NewPage

func NewPage() *Page

NewPage creates an empty Page.

func NewPageWithImage

func NewPageWithImage(img image.Image) *Page

NewPageWithImage creates a Page showing img.

func (*Page) CreateRenderer

func (p *Page) CreateRenderer() fyne.WidgetRenderer

func (*Page) Highlights added in v0.6.0

func (p *Page) Highlights() []Highlight

Highlights returns the highlights drawn over the page.

func (*Page) PageSize

func (p *Page) PageSize() fyne.Size

PageSize returns the size of the page at Scale 1. Unless set with Page.SetPageSize, this is the size of the page image in pixels.

func (*Page) SetHighlights added in v0.6.0

func (p *Page) SetHighlights(highlights []Highlight)

SetHighlights replaces the highlights drawn over the page; nil removes them.

func (*Page) SetImage

func (p *Page) SetImage(pageImage image.Image)

SetImage replaces the displayed page image.

func (*Page) SetPageSize

func (p *Page) SetPageSize(size fyne.Size)

SetPageSize sets the size of the page at Scale 1, independent of the resolution of the image.

type Rect added in v0.6.0

type Rect struct {
	X0, Y0, X1, Y1 float64
}

Rect is a rectangle on a page in points (1/72 inch) of the page as it is displayed: origin at the top left corner, y down, /Rotate and /UserUnit applied, like Source.Bound.

type SearchOptions added in v0.6.0

type SearchOptions struct {
	// MatchCase tells upper and lower case apart.
	MatchCase bool
	// WholeWord matches the query only where it neither continues a word
	// before nor after it.
	WholeWord bool
	// IgnoreDiacritics finds "resume" in "résumé" and reads compatibility
	// characters as what they stand for, such as "2" for "²".
	IgnoreDiacritics bool
}

SearchOptions control how a query is matched. The zero value matches case-insensitively anywhere in a word.

type SearchResult added in v0.6.0

type SearchResult struct {
	// Page is the index (0-based) of the page.
	Page int
	// Text is the text found, Prefix and Suffix up to 32 characters of the
	// text before and after it, for a result list. White space in them is
	// one space.
	Prefix, Text, Suffix string
	// Rects cover the text found, one per line it spans.
	Rects []Rect
}

SearchResult is a place on a page where the query was found.

func (SearchResult) Bounds added in v0.6.0

func (r SearchResult) Bounds() Rect

Bounds returns the smallest rectangle around the rects of r.

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source is an opened PDF. All methods are safe for concurrent use: cera only allows interpreting pages of a document from one goroutine at a time, hence the mutex. A single page is still rasterized by cera on all cores.

func OpenSource

func OpenSource(contents []byte) (*Source, error)

OpenSource parses the PDF in contents.

Example
package main

import (
	"fmt"
	"os"

	pdf "github.com/timzifer/fyne-pdf"
)

func main() {
	contents, err := os.ReadFile("example.pdf")
	if err != nil {
		panic(err)
	}

	src, err := pdf.OpenSource(contents)
	if err != nil {
		panic(err)
	}
	defer func() { _ = src.Close() }()

	fmt.Println("pages:", src.PageCount())
	fmt.Println("title:", src.Metadata().Title)

	img, err := src.RenderPage(0, 300)
	if err != nil {
		panic(err)
	}
	fmt.Println("size:", img.Bounds().Size())
}

func (*Source) Bound

func (s *Source) Bound(page int) (image.Rectangle, error)

Bound returns the visible area of page (0-based) in points (72 dpi), with /Rotate and /UserUnit already applied.

func (*Source) Close

func (s *Source) Close() error

Close releases the document. Afterwards all methods return an error or zero pages. Closing more than once is allowed.

func (*Source) Metadata

func (s *Source) Metadata() Metadata

Metadata returns the document's metadata, or the zero Metadata after Close. A missing or damaged /Info gives empty fields.

func (*Source) PageCount

func (s *Source) PageCount() int

PageCount returns the number of pages, or 0 after Close.

func (*Source) PageText added in v0.6.0

func (s *Source) PageText(ctx context.Context, page int) (string, error)

PageText returns the text of page (0-based) in reading order: lines separated by a line break, blocks of text by an empty line.

func (*Source) RenderPage

func (s *Source) RenderPage(page int, dpi float64) (image.Image, error)

RenderPage renders page (0-based) at the given resolution onto a white background. If rendering takes longer than 15 seconds, the partially drawn image is returned without an error.

func (*Source) RenderPageContext

func (s *Source) RenderPageContext(ctx context.Context, page int, dpi float64) (image.Image, error)

RenderPageContext is like Source.RenderPage, but stops early and returns ctx.Err() when ctx is cancelled.

func (*Source) Search added in v0.6.0

func (s *Source) Search(ctx context.Context, query string, opt SearchOptions) ([]SearchResult, error)

Search returns the places of query in the whole document, page by page, as Source.SearchPage finds them. Pages whose text cannot be read are skipped; their errors are returned joined, with the results of the other pages. When ctx ends, Search returns the results so far and ctx.Err().

func (*Source) SearchPage added in v0.6.0

func (s *Source) SearchPage(ctx context.Context, page int, query string, opt SearchOptions) ([]SearchResult, error)

SearchPage returns the places of query on page (0-based), in reading order and not overlapping. Runs of white space in the text and the query match one space, and words broken across lines by a hyphen are found joined. An empty query finds nothing.

type Thumbnail

type Thumbnail struct {
	widget.BaseWidget

	OnTapped func()
	// contains filtered or unexported fields
}

Thumbnail is a tappable page preview with a title below it.

func NewThumbnail

func NewThumbnail() *Thumbnail

NewThumbnail creates an empty Thumbnail.

func (*Thumbnail) CreateRenderer

func (t *Thumbnail) CreateRenderer() fyne.WidgetRenderer

func (*Thumbnail) Selected

func (t *Thumbnail) Selected() bool

Selected reports whether the thumbnail is highlighted.

func (*Thumbnail) SetImage

func (t *Thumbnail) SetImage(theImage image.Image)

SetImage sets the preview image.

func (*Thumbnail) SetMinimumSize

func (t *Thumbnail) SetMinimumSize(size fyne.Size)

SetMinimumSize sets the size of the preview image area.

func (*Thumbnail) SetSelected

func (t *Thumbnail) SetSelected(selected bool)

SetSelected highlights the thumbnail, e.g. for the page currently shown.

func (*Thumbnail) SetTitle

func (t *Thumbnail) SetTitle(title string)

SetTitle sets the label shown below the preview.

func (*Thumbnail) Tapped

func (t *Thumbnail) Tapped(_ *fyne.PointEvent)

type ViewMode added in v0.5.0

type ViewMode int

ViewMode selects how the main view of a Document shows the pages.

const (
	// ViewSinglePage shows one page at a time; scrolling past its top or
	// bottom turns to the previous or next one.
	ViewSinglePage ViewMode = iota
	// ViewContinuous shows all pages one below the other, scrolled through
	// without a break.
	ViewContinuous
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL