unidoc / unipdf

Golang PDF library for creating and processing PDF files (pure go)
https://unidoc.io
Other
2.46k stars 250 forks source link

[FEATURE] Improve developer experience when creating outlines #523

Closed zb3 closed 7 months ago

zb3 commented 10 months ago

Let's say I'd need to create an outline. Initially, this is what I'd write:

outline := model.NewOutline()
outline.Add(model.NewOutlineItem("title1", model.NewOutlineDest(0, 0, 0)))
outline.Add(model.NewOutlineItem("title2", model.NewOutlineDest(1, 0, 0)))
pdfWriter.AddOutlineTree(outline.ToOutlineTree())

(btw, unipdf-examples do not show how to create the outline from scratch even though the functionality exists - note the developer experience)

However, this doesn't work as expected. It ends up like this in the PDF file:

<< /Dest [ 0 /XYZ 0 0 0 ] ...
<< /Dest [ 1 /XYZ 0 0 0 ] ...

At first glance it'd make sense, however, the "XYZ" part is misleading, because with X=0 and Y=0, this references the BOTTOM left corner of that page (see https://stackoverflow.com/questions/19360946/bookmark-to-specific-page-using-itextsharp-4-1-6), not the top one.

The proper link would use null for at least Y (ghostscript uses it for all these items), but this is impossible to achieve without modifying the OutlineDest.ToPdfObject function. This means there appears to be no easy way to achieve such simple goal (btw note that the Fit mode is not the same as it changes the zoom).

Maybe if the passed coordinate is negative, you could use the null object instead of a float value? Like this:

// ToPdfObject returns a PDF object representation of the outline destination.
func (od OutlineDest) ToPdfObject() core.PdfObject {
    if (od.PageObj == nil && od.Page < 0) || od.Mode == "" {
        return core.MakeNull()
    }

    // Add destination page.
    dest := core.MakeArray()
    if od.PageObj != nil {
        // Internal outline.
        dest.Append(od.PageObj)
    } else {
        // External outline.
        dest.Append(core.MakeInteger(od.Page))
    }

    // Add destination mode.
    dest.Append(core.MakeName(od.Mode))

    // See section 12.3.2.2 "Explicit Destinations" (page 374).
    switch od.Mode {
    // [pageObj|pageNum /Fit]
    // [pageObj|pageNum /FitB]
    case "Fit", "FitB":
    // [pageObj|pageNum /FitH top]
    // [pageObj|pageNum /FitBH top]
    case "FitH", "FitBH":
        dest.Append(core.MakeFloat(od.Y))
    // [pageObj|pageNum /FitV left]
    // [pageObj|pageNum /FitBV left]
    case "FitV", "FitBV":
        dest.Append(core.MakeFloat(od.X))
    // [pageObj|pageNum /XYZ x y zoom]
    case "XYZ":
        if (od.X < 0) {
            dest.Append(core.MakeNull())
        } else {
            dest.Append(core.MakeFloat(od.X))
        }
        if (od.Y < 0) {
            dest.Append(core.MakeNull())
        } else {
            dest.Append(core.MakeFloat(od.Y))
        }
        dest.Append(core.MakeFloat(od.Zoom))
    default:
        dest.Set(1, core.MakeName("Fit"))
    }

    return dest
}
github-actions[bot] commented 10 months ago

Welcome! Thanks for posting your first issue. The way things work here is that while customer issues are prioritized, other issues go into our backlog where they are assessed and fitted into the roadmap when suitable. If you need to get this done, consider buying a license which also enables you to use it in your commercial products. More information can be found on https://unidoc.io/

sampila commented 7 months ago

Hi @zb3,

We created example for outlines usages https://github.com/unidoc/unipdf-examples/tree/master/outlines, and about the X=0 and Y=0 position being on bottom left, it is due the normally PDF begin from bottom to top, if you want position on top left, you need to get the page height first.

Hope this help, and you can re-open this issue if you still have problem with this issue.

Thanks