Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ type Engine struct {
hyperColorlinks bool // colorlinks option: colour link text (else leave it black)
hyperURLColor uint32 // \url/\href colour when colorlinks is on (hyperref urlcolor)
hyperLinkColor uint32 // \hyperlink colour when colorlinks is on (hyperref linkcolor)
pdfTitle string // \hypersetup{pdftitle=…} → PDF /Info /Title
pdfAuthor string // \hypersetup{pdfauthor=…} → PDF /Info /Author
mathR mathRendererT // lazily-built go-tex/math renderer (see math.go)

// paragraph-builder state (horizontal mode at top level)
Expand Down
6 changes: 6 additions & 0 deletions hyperstyle.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import "strings"
// - linkcolor: colour of internal links (\hyperlink).
// - urlcolor: colour of \url and \href.
// - allcolors: sets both link and url colours at once.
// - pdftitle / pdfauthor: the PDF /Info document title and author (see
// pdfdriver.go); a non-ASCII value reaches the PDF as UTF-16BE.
func (e *Engine) applyHypersetup(opts string) {
for _, seg := range splitKVSegments(opts, ',') {
key, val, has := cutKeyVal(seg)
Expand All @@ -41,6 +43,10 @@ func (e *Engine) applyHypersetup(opts string) {
case "allcolors", "allcolours":
c := e.resolveColor(val)
e.hyperLinkColor, e.hyperURLColor = c, c
case "pdftitle":
e.pdfTitle = val
case "pdfauthor":
e.pdfAuthor = val
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion pdfdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ func (e *Engine) RenderPDF(w io.Writer, margin float64) error {
return fmt.Errorf("texengine: PDF font load: %w", err)
}
size := float64(ef.sizePt())
doc := pdfkit.New(pdfkit.Options{})
// \hypersetup{pdftitle=…,pdfauthor=…} populates the PDF information dictionary,
// so a viewer's title bar and document properties — and reference managers —
// read the paper's real title and author rather than nothing (see hyperstyle.go).
// go-pdfkit encodes a non-ASCII value as a UTF-16BE text string, so an accented
// title is correct.
doc := pdfkit.New(pdfkit.Options{Title: e.pdfTitle, Author: e.pdfAuthor})
vmargin := e.renderVMargin(margin)
paperW, paperH, _ := e.paperSizePt()
for _, page := range e.Pages() {
Expand Down
89 changes: 89 additions & 0 deletions pdfinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) the go-tex/engine authors.
// SPDX-License-Identifier: BSD-3-Clause

package engine

import (
"bytes"
"strings"
"testing"
)

// renderPDFDoc runs a small document to PDF and returns the raw bytes as a string.
// The information dictionary (/Title, /Author) is not stream-compressed, so a test
// can grep the raw PDF for it.
func renderPDFDoc(t *testing.T, body string) string {
t.Helper()
fp := pdfFontOrSkip(t)
e := New()
e.LoadLaTeX()
e.Run(`\font\rm={` + fp + `} at 12pt \rm`)
if _, err := e.Run(`\documentclass{article}\begin{document}` + body + `\end{document}`); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
if err := e.RenderPDF(&buf, 24); err != nil {
t.Fatalf("RenderPDF: %v", err)
}
return buf.String()
}

// \hypersetup{pdftitle=…,pdfauthor=…} reaches the PDF information dictionary.
func TestPDFInfoTitleAndAuthor(t *testing.T) {
out := renderPDFDoc(t, `\hypersetup{pdftitle={A Study of Widgets},pdfauthor={Ada Lovelace}}Body.`)
if !strings.Contains(out, `/Title (A Study of Widgets)`) {
t.Errorf("pdftitle did not reach /Info /Title")
}
if !strings.Contains(out, `/Author (Ada Lovelace)`) {
t.Errorf("pdfauthor did not reach /Info /Author")
}
}

// An accented pdftitle is written UTF-16BE, not as raw UTF-8 (the payoff of the
// go-pdfkit v0.10.0 text-string work).
func TestPDFInfoAccentedTitleIsUTF16(t *testing.T) {
out := renderPDFDoc(t, `\hypersetup{pdftitle={Résumé des travaux}}Body.`)
if !strings.Contains(out, "<FEFF") {
t.Errorf("accented pdftitle was not written UTF-16BE")
}
if strings.Contains(out, "Résumé des travaux") {
t.Errorf("accented pdftitle leaked into the PDF as raw UTF-8")
}
}

// Without a pdftitle the document has no /Title in its /Info (the pre-existing
// behaviour is preserved — no empty title is emitted).
func TestPDFInfoNoTitleByDefault(t *testing.T) {
out := renderPDFDoc(t, `Body with no metadata.`)
if strings.Contains(out, "/Title") {
t.Errorf("a /Title was emitted though no pdftitle was set")
}
}

// The package-option form sets the metadata just as \hypersetup does.
func TestPDFInfoFromPackageOptions(t *testing.T) {
fp := pdfFontOrSkip(t)
e := New()
e.LoadLaTeX()
e.Run(`\font\rm={` + fp + `} at 12pt \rm`)
if _, err := e.Run(`\documentclass{article}\usepackage[pdftitle={Wide Paper}]{hyperref}` +
`\begin{document}Body.\end{document}`); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
if err := e.RenderPDF(&buf, 24); err != nil {
t.Fatalf("RenderPDF: %v", err)
}
if !strings.Contains(buf.String(), `/Title (Wide Paper)`) {
t.Errorf("\\usepackage[pdftitle=…]{hyperref} did not set /Title")
}
}

// The parsed values are stored on the engine (a unit check independent of a font).
func TestPDFInfoStoredOnEngine(t *testing.T) {
e := New()
e.applyHypersetup(`pdftitle={My Title}, pdfauthor={Me}`)
if e.pdfTitle != "My Title" || e.pdfAuthor != "Me" {
t.Errorf("stored title=%q author=%q, want (My Title, Me)", e.pdfTitle, e.pdfAuthor)
}
}