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
4 changes: 4 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type Doc struct {
flatten bool
dropAnnots bool
dropOutlines bool
// outline, when set, is written instead of carrying the sources' own
// bookmarks over: a document assembled rather than merged has an outline
// of its caller's rather than of anybody else's.
outline []Bookmark

// How the file is written: packed into compressed object streams, and
// protected or not.
Expand Down
64 changes: 57 additions & 7 deletions outlines.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,43 @@ type outlineItem struct {
// that loses every bookmark is a poor merge.
func (d *Doc) DropOutlines() { d.dropOutlines = true }

// writeOutlines carries the bookmarks of every source document over, in the
// order the documents first contribute a page, dropping whatever pointed at a
// page that is no longer here.
func (d *Doc) writeOutlines(w *reader.Writer, where destinations) reader.Object {
// A Bookmark is an entry of an outline written from scratch: what it says, the
// page of this document it points at counting from one, and whatever sits
// under it.
//
// It is what a document assembled rather than merged carries — a shared
// edit, a report built out of pieces — where there is no source outline to
// carry over because the outline is the caller's own.
type Bookmark struct {
Title string
Page int
Children []Bookmark
}

// SetOutline writes these bookmarks rather than carrying over the ones the
// sources had. An entry pointing at a page this document has not got is left
// out, and so is everything under it: a heading whose section has gone is not
// a heading any more.
//
// Passing nothing puts the sources' own bookmarks back.
func (d *Doc) SetOutline(marks []Bookmark) { d.outline = marks }

// writeOutlines writes the bookmarks the caller set, or carries over those of
// every source document, in the order the documents first contribute a page,
// dropping whatever pointed at a page that is no longer here.
func (d *Doc) writeOutlines(w *reader.Writer, where destinations, refs []reader.Ref) reader.Object {
if d.dropOutlines {
return nil
}
var items []outlineItem
budget := maxOutlineItems
for _, src := range d.sources() {
items = append(items, d.readOutlines(w, src, where, &budget)...)
if len(d.outline) > 0 {
budget := maxOutlineItems
items = d.buildOutline(refs, d.outline, 0, &budget)
} else {
budget := maxOutlineItems
for _, src := range d.sources() {
items = append(items, d.readOutlines(w, src, where, &budget)...)
}
}
if len(items) == 0 {
return nil
Expand Down Expand Up @@ -62,6 +88,30 @@ func (d *Doc) sources() []*reader.Document {
return out
}

// buildOutline turns the caller's bookmarks into the ones a file carries,
// dropping any that point nowhere in this document.
func (d *Doc) buildOutline(refs []reader.Ref, marks []Bookmark, depth int, budget *int) []outlineItem {
if depth > maxOutlineDepth {
return nil
}
var out []outlineItem
for _, m := range marks {
if *budget <= 0 {
return out
}
if m.Page < 1 || m.Page > len(refs) {
continue
}
*budget--
out = append(out, outlineItem{
title: []byte(m.Title),
dest: reader.Array{refs[m.Page-1], reader.Name("Fit")},
children: d.buildOutline(refs, m.Children, depth+1, budget),
})
}
return out
}

// readOutlines reads one document's bookmarks.
func (d *Doc) readOutlines(w *reader.Writer, src *reader.Document, where destinations, budget *int) []outlineItem {
cat, _ := src.Catalog()
Expand Down
83 changes: 83 additions & 0 deletions outlines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,86 @@ func TestBookmarksOfMadePagesAreNone(t *testing.T) {
t.Errorf("bookmarks = %v", got)
}
}

func TestAnOutlineWrittenFromScratch(t *testing.T) {
// A document assembled rather than merged has an outline of its caller's:
// there is no source outline to carry over.
d, err := Open(simple(t, 5))
if err != nil {
t.Fatal(err)
}
d.SetOutline([]Bookmark{
{Title: "One", Page: 1, Children: []Bookmark{
{Title: "One and a half", Page: 2},
}},
{Title: "Three", Page: 3},
{Title: "Nowhere", Page: 99}, // past the end
{Title: "Also nowhere", Page: 0}, // not a page
{Title: "Gone", Page: 42, Children: []Bookmark{{Title: "Under", Page: 1}}},
})
back, _ := writeAndOpen(t, d)
titles := outlineTitles(t, back)
want := []string{"One", "One and a half", "Three"}
if !reflect.DeepEqual(titles, want) {
t.Errorf("the outline says %v, want %v", titles, want)
}
}

func TestAnOutlineSetAndThenTakenBack(t *testing.T) {
// Passing nothing puts the sources' own bookmarks back, and asking for
// none at all still wins.
f := richPDF(t, 3)
d, err := Open(f.bytes)
if err != nil {
t.Fatal(err)
}
d.SetOutline([]Bookmark{{Title: "Mine", Page: 1}})
back, _ := writeAndOpen(t, d)
if titles := outlineTitles(t, back); len(titles) != 1 || titles[0] != "Mine" {
t.Errorf("the outline says %v", titles)
}

d, _ = Open(f.bytes)
d.SetOutline(nil)
back, _ = writeAndOpen(t, d)
if titles := outlineTitles(t, back); len(titles) == 0 {
t.Error("the source's own bookmarks were not put back")
}

d, _ = Open(f.bytes)
d.SetOutline([]Bookmark{{Title: "Mine", Page: 1}})
d.DropOutlines()
back, _ = writeAndOpen(t, d)
if titles := outlineTitles(t, back); len(titles) != 0 {
t.Errorf("bookmarks were written after being dropped: %v", titles)
}
}

func TestAnOutlineDeeperThanAnyoneNeeds(t *testing.T) {
// A tree deeper than the format is read to, and one wider than it is
// carried to, are both cut off rather than followed.
deep := Bookmark{Title: "top", Page: 1}
at := &deep
for i := 0; i < maxOutlineDepth+5; i++ {
at.Children = []Bookmark{{Title: "under", Page: 1}}
at = &at.Children[0]
}
d, err := Open(simple(t, 2))
if err != nil {
t.Fatal(err)
}
d.SetOutline([]Bookmark{deep})
if _, err := d.Bytes(); err != nil {
t.Fatal(err)
}

wide := make([]Bookmark, maxOutlineItems+10)
for i := range wide {
wide[i] = Bookmark{Title: "one", Page: 1}
}
d, _ = Open(simple(t, 2))
d.SetOutline(wide)
if _, err := d.Bytes(); err != nil {
t.Fatal(err)
}
}
2 changes: 1 addition & 1 deletion write.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (d *Doc) Bytes() ([]byte, error) {
})

catalog := reader.Dict{"Type": reader.Name("Catalog"), "Pages": pagesRef}
if outlines := d.writeOutlines(w, where); outlines != nil {
if outlines := d.writeOutlines(w, where, refs); outlines != nil {
catalog["Outlines"] = outlines
}
trailer := reader.Dict{"Root": w.Add(catalog)}
Expand Down
Loading