Merge branch 'v3' into v3-peterwilliams97-extract.text.take2

This commit is contained in:
Gunnsteinn Hall 2018-12-07 12:17:07 +00:00
commit dc263c9820
7 changed files with 308 additions and 44 deletions

11
Jenkinsfile vendored
View File

@ -7,6 +7,9 @@ node {
env.PATH="${root}/bin:${env.GOPATH}/bin:${env.PATH}"
env.GOCACHE="off"
env.TMPDIR="${WORKSPACE}/temp"
sh "mkdir ${env.TMPDIR}"
dir("${GOPATH}/src/github.com/unidoc/unidoc") {
sh 'go version'
@ -21,8 +24,8 @@ node {
sh 'go get github.com/tebeka/go2xunit'
sh 'go get github.com/t-yuki/gocover-cobertura'
// Get all dependencies.
sh 'go get ./...'
// Get all dependencies (for tests also).
sh 'go get -t ./...'
}
stage('Linting') {
@ -36,13 +39,13 @@ node {
stage('Testing') {
// Go test - No tolerance.
sh 'rm -f /tmp/*.pdf'
sh "rm -f ${env.TMPDIR}/*.pdf"
sh '2>&1 go test -v ./... | tee gotest.txt'
}
stage('Check generated PDFs') {
// Check the created output pdf files.
sh 'find /tmp -maxdepth 1 -name "*.pdf" -print0 | xargs -t -n 1 -0 gs -dNOPAUSE -dBATCH -sDEVICE=nullpage -sPDFPassword=password -dPDFSTOPONERROR -dPDFSTOPONWARNING'
sh "find ${env.TMPDIR} -maxdepth 1 -name \"*.pdf\" -print0 | xargs -t -n 1 -0 gs -dNOPAUSE -dBATCH -sDEVICE=nullpage -sPDFPassword=password -dPDFSTOPONERROR -dPDFSTOPONWARNING"
}
stage('Test coverage') {

View File

@ -413,6 +413,11 @@ func (array *PdfObjectArray) Append(objects ...PdfObject) {
}
}
// Clear resets the array to an empty state.
func (array *PdfObjectArray) Clear() {
array.vec = []PdfObject{}
}
// ToFloat64Array returns a slice of all elements in the array as a float64 slice. An error is
// returned if the array contains non-numeric objects (each element can be either PdfObjectInteger
// or PdfObjectFloat).

View File

@ -41,6 +41,9 @@ type Block struct {
// Margins to be applied around the block when drawing on Page.
margins margins
// Block annotations.
annotations []*model.PdfAnnotation
}
// NewBlock creates a new Block with specified width and height.
@ -98,6 +101,12 @@ func (blk *Block) SetAngle(angleDeg float64) {
blk.angle = angleDeg
}
// AddAnnotation adds an annotation to the current block.
// The annotation will be added to the page the block will be rendered on.
func (blk *Block) AddAnnotation(annotation *model.PdfAnnotation) {
blk.annotations = append(blk.annotations, annotation)
}
// duplicate duplicates the block with a new copy of the operations list.
func (blk *Block) duplicate() *Block {
dup := &Block{}
@ -280,6 +289,11 @@ func (blk *Block) drawToPage(page *model.PdfPage) error {
return err
}
// Add block annotations to the page.
for _, annotation := range blk.annotations {
page.Annotations = append(page.Annotations, annotation)
}
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/unidoc/unidoc/common"
"github.com/unidoc/unidoc/pdf/contentstream"
"github.com/unidoc/unidoc/pdf/core"
"github.com/unidoc/unidoc/pdf/model"
)
// StyledParagraph represents text drawn with a specified font and can wrap across lines and pages.
@ -25,6 +26,9 @@ type StyledParagraph struct {
// Style used for the paragraph for spacing and offsets.
defaultStyle TextStyle
// Style used for the paragraph link annotations.
defaultLinkStyle TextStyle
// Text alignment: Align left/right/center/justify.
alignment TextAlignment
@ -67,33 +71,32 @@ type StyledParagraph struct {
// newStyledParagraph creates a new styled paragraph.
func newStyledParagraph(style TextStyle) *StyledParagraph {
// TODO: Can we wrap intellectually, only if given width is known?
p := &StyledParagraph{
chunks: []*TextChunk{},
defaultStyle: style,
lineHeight: 1.0,
alignment: TextAlignmentLeft,
enableWrap: true,
defaultWrap: true,
angle: 0,
scaleX: 1,
scaleY: 1,
positioning: positionRelative,
return &StyledParagraph{
chunks: []*TextChunk{},
defaultStyle: style,
defaultLinkStyle: newLinkStyle(style.Font),
lineHeight: 1.0,
alignment: TextAlignmentLeft,
enableWrap: true,
defaultWrap: true,
angle: 0,
scaleX: 1,
scaleY: 1,
positioning: positionRelative,
}
}
return p
// appendChunk adds the provided text chunk to the paragraph.
func (p *StyledParagraph) appendChunk(chunk *TextChunk) *TextChunk {
p.chunks = append(p.chunks, chunk)
p.wrapText()
return chunk
}
// Append adds a new text chunk to the paragraph.
func (p *StyledParagraph) Append(text string) *TextChunk {
chunk := &TextChunk{
Text: text,
Style: p.defaultStyle,
}
p.chunks = append(p.chunks, chunk)
p.wrapText()
return chunk
chunk := newTextChunk(text, p.defaultStyle)
return p.appendChunk(chunk)
}
// Insert adds a new text chunk at the specified position in the paragraph.
@ -103,17 +106,34 @@ func (p *StyledParagraph) Insert(index uint, text string) *TextChunk {
index = l
}
chunk := &TextChunk{
Text: text,
Style: p.defaultStyle,
}
chunk := newTextChunk(text, p.defaultStyle)
p.chunks = append(p.chunks[:index], append([]*TextChunk{chunk}, p.chunks[index:]...)...)
p.wrapText()
return chunk
}
// AddExternalLink adds a new external link to the paragraph.
// The text parameter represents the text that is displayed and the url
// parameter sets the destionation of the link.
func (p *StyledParagraph) AddExternalLink(text, url string) *TextChunk {
chunk := newTextChunk(text, p.defaultLinkStyle)
chunk.annotation = newExternalLinkAnnotation(url)
return p.appendChunk(chunk)
}
// AddInternalLink adds a new internal link tot the paragraph.
// The text parameter represents the text that is displayed.
// The user is taken to the specified page, at the specified x and y
// coordinates. Position 0, 0 is at the top left of the page.
// The zoom of the destination page is controlled with the zoom
// parameter. Pass in 0 to keep the current zoom value.
func (p *StyledParagraph) AddInternalLink(text string, page int64, x, y, zoom float64) *TextChunk {
chunk := newTextChunk(text, p.defaultLinkStyle)
chunk.annotation = newInternalLinkAnnotation(page-1, x, y, zoom)
return p.appendChunk(chunk)
}
// Reset removes all the text chunks the paragraph contains.
func (p *StyledParagraph) Reset() {
p.chunks = []*TextChunk{}
@ -318,8 +338,18 @@ func (p *StyledParagraph) wrapText() error {
var line []*TextChunk
var lineWidth float64
copyAnnotation := func(src *model.PdfAnnotation) *model.PdfAnnotation {
if src == nil {
return nil
}
annotation := *src
return &annotation
}
for _, chunk := range p.chunks {
style := chunk.Style
annotation := chunk.annotation
var part []rune
var glyphs []string
@ -338,8 +368,9 @@ func (p *StyledParagraph) wrapText() error {
if glyph == "controlLF" {
// moves to next line.
line = append(line, &TextChunk{
Text: strings.TrimRightFunc(string(part), unicode.IsSpace),
Style: style,
Text: strings.TrimRightFunc(string(part), unicode.IsSpace),
Style: style,
annotation: copyAnnotation(annotation),
})
p.lines = append(p.lines, line)
line = []*TextChunk{}
@ -396,8 +427,9 @@ func (p *StyledParagraph) wrapText() error {
}
line = append(line, &TextChunk{
Text: strings.TrimRightFunc(string(text), unicode.IsSpace),
Style: style,
Text: strings.TrimRightFunc(string(text), unicode.IsSpace),
Style: style,
annotation: copyAnnotation(annotation),
})
p.lines = append(p.lines, line)
line = []*TextChunk{}
@ -411,8 +443,9 @@ func (p *StyledParagraph) wrapText() error {
if len(part) > 0 {
line = append(line, &TextChunk{
Text: string(part),
Style: style,
Text: string(part),
Style: style,
annotation: copyAnnotation(annotation),
})
}
}
@ -547,7 +580,10 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
cc.Add_BT()
currY := yPos
for idx, line := range p.lines {
currX := ctx.X
if idx != 0 {
// Move to next line if not first.
cc.Add_Tstar()
@ -557,18 +593,25 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
// Get width of the line (excluding spaces).
var width float64
var height float64
var spaceWidth float64
var spaces uint
var chunkWidths []float64
for _, chunk := range line {
style := &chunk.Style
if style.FontSize > height {
height = style.FontSize
}
spaceMetrics, found := style.Font.GetGlyphCharMetrics("space")
if !found {
return ctx, errors.New("The font does not have a space glyph")
}
var chunkSpaces uint
var chunkWidth float64
for _, r := range chunk.Text {
glyph, found := style.Font.Encoder().RuneToGlyph(r)
if !found {
@ -590,12 +633,16 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
return ctx, errors.New("Unsupported text glyph")
}
width += style.FontSize * metrics.Wx
chunkWidth += style.FontSize * metrics.Wx
}
chunkWidths = append(chunkWidths, chunkWidth)
width += chunkWidth
spaceWidth += float64(chunkSpaces) * spaceMetrics.Wx * style.FontSize
spaces += chunkSpaces
}
height *= p.lineHeight
// Add line shifts.
objs := []core.PdfObject{}
@ -608,12 +655,18 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
}
} else if p.alignment == TextAlignmentCenter {
// Start with an offset of half of the remaining line space.
shift := (wrapWidth - width - spaceWidth) / 2 / defaultFontSize
offset := (wrapWidth - width - spaceWidth) / 2
shift := offset / defaultFontSize
objs = append(objs, core.MakeFloat(-shift))
currX += offset / 1000.0
} else if p.alignment == TextAlignmentRight {
// Push the text at the end of the line.
shift := (wrapWidth - width - spaceWidth) / defaultFontSize
offset := (wrapWidth - width - spaceWidth)
shift := offset / defaultFontSize
objs = append(objs, core.MakeFloat(-shift))
currX += offset / 1000.0
}
if len(objs) > 0 {
@ -650,11 +703,6 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
}
if glyph == "space" {
if !found {
common.Log.Debug("Unsupported glyph %s in font\n", glyph)
return ctx, errors.New("Unsupported text glyph")
}
if len(encStr) > 0 {
cc.Add_rg(r, g, b).
Add_Tf(fonts[idx][k], style.FontSize).
@ -667,6 +715,8 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
cc.Add_Tf(fontName, fontSize).
Add_TL(fontSize * p.lineHeight).
Add_TJ([]core.PdfObject{core.MakeFloat(-spaceWidth)}...)
chunkWidths[k] += spaceWidth * fontSize
} else {
encStr = append(encStr, style.Font.Encoder().Encode(string(rn))...)
}
@ -678,7 +728,58 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
Add_TL(style.FontSize * p.lineHeight).
Add_TJ([]core.PdfObject{core.MakeStringFromBytes(encStr)}...)
}
chunkWidth := chunkWidths[k] / 1000.0
// Add annotations.
if chunk.annotation != nil {
var annotRect *core.PdfObjectArray
// Process annotation.
if !chunk.annotationProcessed {
annotCtx := chunk.annotation.GetContext()
switch t := annotCtx.(type) {
case *model.PdfAnnotationLink:
// Initialize annotation rectangle.
annotRect = core.MakeArray()
t.Rect = annotRect
// Reverse the Y axis of the destination coordinates.
// The user passes in the annotation coordinates as if
// position 0, 0 is at the top left of the page.
// However, position 0, 0 in the PDF is at the bottom
// left of the page.
annotDest, ok := t.Dest.(*core.PdfObjectArray)
if ok && annotDest.Len() == 5 {
t, ok := annotDest.Get(1).(*core.PdfObjectName)
if ok && t.String() == "XYZ" {
y, err := core.GetNumberAsFloat(annotDest.Get(3))
if err == nil {
annotDest.Set(3, core.MakeFloat(ctx.PageHeight-y))
}
}
}
}
chunk.annotationProcessed = true
}
// Set the coordinates of the annotation.
if annotRect != nil {
annotRect.Clear()
annotRect.Append(core.MakeFloat(currX))
annotRect.Append(core.MakeFloat(currY))
annotRect.Append(core.MakeFloat(currX + chunkWidth))
annotRect.Append(core.MakeFloat(currY + height))
}
blk.AddAnnotation(chunk.annotation)
}
currX += chunkWidth
}
currY -= height
}
cc.Add_ET()
cc.Add_Q()

View File

@ -368,3 +368,71 @@ func TestStyledParagraph(t *testing.T) {
t.Fatalf("Fail: %v\n", err)
}
}
func TestStyledParagraphLinks(t *testing.T) {
c := New()
// First page.
c.NewPage()
p := c.NewStyledParagraph()
p.Append("Paragraph links are useful for going to remote places like ")
p.AddExternalLink("Google", "https://google.com")
p.Append(", or maybe ")
p.AddExternalLink("Github", "https://github.com")
p.Append("\nHowever, you can also use them to move go to the ")
p.AddInternalLink("start", 2, 0, 0, 0).Style.Color = ColorRGBFrom8bit(255, 0, 0)
p.Append(" of the second page, the ")
p.AddInternalLink("middle", 2, 0, 250, 0).Style.Color = ColorRGBFrom8bit(0, 255, 0)
p.Append(" or the ")
p.AddInternalLink("end", 2, 0, 500, 0).Style.Color = ColorRGBFrom8bit(0, 0, 255)
p.Append(" of the second page.\nOr maybe go to the third ")
p.AddInternalLink("page", 3, 0, 0, 0)
err := c.Draw(p)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
// Second page.
c.NewPage()
p = c.NewStyledParagraph()
p.Append(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. In maximus id purus vitae faucibus. Proin at egestas ex. Mauris id luctus nulla, et scelerisque odio. Praesent scelerisque a erat non ullamcorper. Donec at est nec nunc tempor bibendum at eget quam. Aliquam bibendum est vel ultrices condimentum. Sed augue sapien, commodo et ligula a, consequat consectetur diam. Donec in justo dui. Proin quis aliquam magna. Fusce vel enim ut leo sagittis vehicula vel sed magna. Curabitur lacinia condimentum laoreet. Maecenas venenatis, sapien a hendrerit viverra, arcu odio blandit nulla, a varius sem nisl in magna. Fusce aliquam nec urna nec congue. Phasellus metus quam, hendrerit ac laoreet non, bibendum quis augue. Donec quam ex, aliquam sed rutrum a, lobortis at turpis. Pellentesque pellentesque vitae augue at faucibus.
Nullam porttitor scelerisque mauris. Aenean nunc nunc, facilisis ut arcu eget, dignissim euismod justo. Curabitur lobortis ut augue sit amet pellentesque. Donec interdum lobortis quam, eget lacinia nunc sagittis sed. Nunc tristique consectetur convallis. Fusce tincidunt consequat tincidunt. Phasellus a faucibus metus. Vestibulum eu facilisis sem. Quisque vulputate eros in quam vulputate, id faucibus nibh aliquet. Etiam dictum laoreet urna, sed ultricies nulla volutpat vel. In volutpat nisl nisl, eu suscipit risus feugiat eu. Duis egestas, ante quis pellentesque pulvinar, purus urna imperdiet metus, nec commodo libero sem in dolor.
Phasellus semper, ipsum sollicitudin iaculis dapibus, justo leo interdum ipsum, id feugiat enim lacus id nisl. Sed cursus lacinia laoreet. Cras cursus risus ex, id dapibus mauris lacinia et. Nam eget metus nec ex iaculis laoreet a eu mauris. Vivamus porta ut lacus nec suscipit. Vivamus eu elit in ante consectetur condimentum. Vivamus iaculis tristique lacus, id iaculis arcu maximus pellentesque. Duis commodo nisi turpis, et gravida libero mattis id. Nullam pretium arcu metus, at auctor neque tincidunt vel. Morbi sagittis massa sed arcu dictum, eget ornare nisi ullamcorper. Sed ac lacus ex. Aliquam ornare vehicula interdum. Nulla vehicula est vel turpis ullamcorper iaculis.
Suspendisse potenti. Aenean pellentesque eros nulla, sed tempor tellus hendrerit tristique. Etiam nec enim et ligula sollicitudin faucibus ut eget libero. Suspendisse eget blandit lacus. Suspendisse consequat orci risus. Curabitur id libero quam. Ut pellentesque tristique porta. Phasellus leo augue, porttitor id suscipit eleifend, elementum ut diam. Ut non ipsum in orci consectetur posuere. Nulla facilisi. In laoreet, nunc fringilla feugiat dapibus, augue diam cursus felis, eu efficitur dui ipsum vestibulum orci. Maecenas leo leo, sagittis pharetra venenatis at, porttitor ut risus.
Nunc euismod facilisis venenatis. Donec diam enim, sollicitudin ac vestibulum ultrices, malesuada eget ipsum. Morbi et sem vel metus convallis scelerisque. Vivamus justo felis, ullamcorper nec arcu eget, pharetra fringilla diam. Praesent ut mauris leo. Quisque sollicitudin sodales justo vel ornare. Proin sollicitudin suscipit risus, vel aliquam nisl ultrices a. Nulla facilisi. Sed eget facilisis dui. Duis maximus tortor eget massa varius sollicitudin. Cras interdum ornare nulla, pulvinar sagittis elit gravida non. Nulla consequat arcu gravida ante commodo, non tempus turpis porta. Quisque tincidunt quam et nisl maximus, nec hendrerit libero feugiat. Sed vel vestibulum leo. Mauris quis efficitur ligula, quis facilisis nibh. Suspendisse commodo elit id vehicula viverra.
Donec auctor tempor ante vel eleifend. Cras laoreet in lacus sit amet tristique. Donec porta, mi non dignissim consectetur, magna urna gravida lectus, in mattis nisi felis id odio. Sed sem ligula, feugiat et lectus tincidunt, condimentum sollicitudin dolor. Donec pulvinar, nibh ultricies tristique aliquam, lorem massa laoreet purus, eu ultrices lorem turpis eget erat. Maecenas auctor tempus dignissim. Pellentesque ut consequat magna. Vestibulum ante velit, feugiat id lectus pellentesque, congue consectetur sapien. Quisque mattis, nisi et facilisis pulvinar, nulla tellus dignissim tortor, eget convallis sem dolor vel lorem. Pellentesque pharetra tortor odio, et egestas elit scelerisque et. Maecenas suscipit lorem ut purus porta dictum.
Donec placerat finibus leo, quis aliquet ipsum dignissim sed. Duis semper vulputate rutrum. Suspendisse egestas, magna in lacinia vulputate, massa sapien lobortis quam, consequat interdum tellus enim ac nulla. Praesent non risus ut nulla tincidunt posuere quis vitae dui. Cras sem lectus, efficitur eget nunc et, gravida accumsan massa. Nam egestas laoreet nunc, eu posuere dolor iaculis sed. In fringilla cursus lectus sit amet ullamcorper.
Curabitur iaculis elit id neque sollicitudin, non dapibus nisi bibendum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Quisque nec dui tempor, convallis felis in, fermentum urna. In ut egestas lacus, quis mollis turpis. Vestibulum finibus metus vel turpis maximus pharetra. Duis tempus aliquam leo eu feugiat. In tincidunt lectus dolor. Mauris id tristique enim, vitae pellentesque elit. Nam mattis vestibulum molestie. Quisque aliquam lacus vel porttitor euismod. Donec rhoncus erat orci. Curabitur venenatis augue vitae metus facilisis, bibendum bibendum ligula elementum.
Sed imperdiet sodales lacus sed sollicitudin. In porta tortor quis augue tempor, eget laoreet tortor tempor. Phasellus in elit et risus interdum tincidunt a ut ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis volutpat nisl id molestie finibus. Suspendisse et nunc aliquet, elementum metus et, bibendum dui. Cras aliquam nunc est, et sagittis nibh tristique sed. Phasellus porta lectus vel sapien elementum, in finibus orci sodales. Mauris orci felis, porta et dapibus eu, dignissim sed tortor. Nullam faucibus sit amet magna ut pellentesque. Etiam non purus non lacus auctor faucibus.`)
p.Append("\n\nYou can also go back to ").Style.FontSize = 14
p.AddInternalLink("page 1", 1, 0, 0, 0).Style.FontSize = 14
err = c.Draw(p)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
// Third page.
c.NewPage()
p = c.NewStyledParagraph()
p.Append("This is the third page.\nGo to ")
p.AddInternalLink("page 1", 1, 0, 0, 0)
p.Append("\nGo to ")
p.AddInternalLink("page 2", 2, 0, 0, 0)
err = c.Draw(p)
if err != nil {
t.Fatalf("Error drawing: %v", err)
}
// Write output file.
err = c.WriteToFile(tempFile("styled_paragraph_links.pdf"))
if err != nil {
t.Fatalf("Fail: %v\n", err)
}
}

View File

@ -5,6 +5,11 @@
package creator
import (
"github.com/unidoc/unidoc/pdf/core"
"github.com/unidoc/unidoc/pdf/model"
)
// TextChunk represents a chunk of text along with a particular style.
type TextChunk struct {
// The text that is being rendered in the PDF.
@ -12,4 +17,62 @@ type TextChunk struct {
// The style of the text being rendered.
Style TextStyle
// Text chunk annotation.
annotation *model.PdfAnnotation
// Internally used in order to skip processing the annotation
// if it has already been processed by the parent component.
annotationProcessed bool
}
// newTextChunk returns a new text chunk instance.
func newTextChunk(text string, style TextStyle) *TextChunk {
return &TextChunk{
Text: text,
Style: style,
}
}
// newExternalLinkAnnotation returns a new external link annotation.
func newExternalLinkAnnotation(url string) *model.PdfAnnotation {
annotation := model.NewPdfAnnotationLink()
// Set border style.
bs := model.NewBorderStyle()
bs.SetBorderWidth(0)
annotation.BS = bs.ToPdfObject()
// Set link destination.
action := core.MakeDict()
action.Set(core.PdfObjectName("S"), core.MakeName("URI"))
action.Set(core.PdfObjectName("URI"), core.MakeString(url))
annotation.A = action
return annotation.PdfAnnotation
}
// newExternalLinkAnnotation returns a new internal link annotation.
func newInternalLinkAnnotation(page int64, x, y, zoom float64) *model.PdfAnnotation {
annotation := model.NewPdfAnnotationLink()
// Set border style.
bs := model.NewBorderStyle()
bs.SetBorderWidth(0)
annotation.BS = bs.ToPdfObject()
// Set link destination.
if page < 0 {
page = 0
}
annotation.Dest = core.MakeArray(
core.MakeInteger(page),
core.MakeName("XYZ"),
core.MakeFloat(x),
core.MakeFloat(y),
core.MakeFloat(zoom),
)
return annotation.PdfAnnotation
}

View File

@ -29,3 +29,13 @@ func newTextStyle(font *model.PdfFont) TextStyle {
FontSize: 10,
}
}
// newLinkStyle creates a new text style object which can be
// used for link annotations.
func newLinkStyle(font *model.PdfFont) TextStyle {
return TextStyle{
Color: ColorRGBFrom8bit(0, 0, 238),
Font: font,
FontSize: 10,
}
}