github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/creator/paragraph.go (about) 1 /* 2 * This file is subject to the terms and conditions defined in 3 * file 'LICENSE.md', which is part of this source code package. 4 */ 5 6 package creator 7 8 import ( 9 "errors" 10 "fmt" 11 12 "github.com/unidoc/unidoc/common" 13 "github.com/unidoc/unidoc/pdf/contentstream" 14 "github.com/unidoc/unidoc/pdf/core" 15 "github.com/unidoc/unidoc/pdf/model" 16 "github.com/unidoc/unidoc/pdf/model/fonts" 17 "github.com/unidoc/unidoc/pdf/model/textencoding" 18 ) 19 20 // Paragraph represents text drawn with a specified font and can wrap across lines and pages. 21 // By default occupies the available width in the drawing context. 22 type Paragraph struct { 23 // The input utf-8 text as a string (series of runes). 24 text string 25 26 // The text encoder which can convert the text (as runes) into a series of glyphs and get character metrics. 27 encoder textencoding.TextEncoder 28 29 // The font to be used to draw the text. 30 textFont fonts.Font 31 32 // The font size (points). 33 fontSize float64 34 35 // The line relative height (default 1). 36 lineHeight float64 37 38 // The text color. 39 color model.PdfColorDeviceRGB 40 41 // Text alignment: Align left/right/center/justify. 42 alignment TextAlignment 43 44 // Wrapping properties. 45 enableWrap bool 46 wrapWidth float64 47 48 // defaultWrap defines whether wrapping has been defined explictly or whether default behavior should 49 // be observed. Default behavior depends on context: normally wrap is expected, except for example in 50 // table cells wrapping is off by default. 51 defaultWrap bool 52 53 // Rotation angle (degrees). 54 angle float64 55 56 // Margins to be applied around the block when drawing on Page. 57 margins margins 58 59 // Positioning: relative / absolute. 60 positioning positioning 61 62 // Absolute coordinates (when in absolute mode). 63 xPos float64 64 yPos float64 65 66 // Scaling factors (1 default). 67 scaleX, scaleY float64 68 69 // Text lines after wrapping to available width. 70 textLines []string 71 } 72 73 // NewParagraph create a new text paragraph. Uses default parameters: Helvetica, WinAnsiEncoding and wrap enabled 74 // with a wrap width of 100 points. 75 func NewParagraph(text string) *Paragraph { 76 p := &Paragraph{} 77 p.text = text 78 p.textFont = fonts.NewFontHelvetica() 79 p.SetEncoder(textencoding.NewWinAnsiTextEncoder()) 80 p.fontSize = 10 81 p.lineHeight = 1.0 82 83 // TODO: Can we wrap intellectually, only if given width is known? 84 p.enableWrap = true 85 p.defaultWrap = true 86 p.SetColor(ColorRGBFrom8bit(0, 0, 0)) 87 p.alignment = TextAlignmentLeft 88 p.angle = 0 89 90 p.scaleX = 1 91 p.scaleY = 1 92 93 p.positioning = positionRelative 94 95 return p 96 } 97 98 // SetFont sets the Paragraph's font. 99 func (p *Paragraph) SetFont(font fonts.Font) { 100 p.textFont = font 101 } 102 103 // SetFontSize sets the font size in document units (points). 104 func (p *Paragraph) SetFontSize(fontSize float64) { 105 p.fontSize = fontSize 106 } 107 108 // SetTextAlignment sets the horizontal alignment of the text within the space provided. 109 func (p *Paragraph) SetTextAlignment(align TextAlignment) { 110 p.alignment = align 111 } 112 113 // SetEncoder sets the text encoding. 114 func (p *Paragraph) SetEncoder(encoder textencoding.TextEncoder) { 115 p.encoder = encoder 116 // Sync with the text font too. 117 // XXX/FIXME: Keep in 1 place only. 118 p.textFont.SetEncoder(encoder) 119 } 120 121 // SetLineHeight sets the line height (1.0 default). 122 func (p *Paragraph) SetLineHeight(lineheight float64) { 123 p.lineHeight = lineheight 124 } 125 126 // SetText sets the text content of the Paragraph. 127 func (p *Paragraph) SetText(text string) { 128 p.text = text 129 } 130 131 // Text sets the text content of the Paragraph. 132 func (p *Paragraph) Text() string { 133 return p.text 134 } 135 136 // SetEnableWrap sets the line wrapping enabled flag. 137 func (p *Paragraph) SetEnableWrap(enableWrap bool) { 138 p.enableWrap = enableWrap 139 p.defaultWrap = false 140 } 141 142 // SetColor set the color of the Paragraph text. 143 // 144 // Example: 145 // 1. p := NewParagraph("Red paragraph") 146 // // Set to red color with a hex code: 147 // p.SetColor(creator.ColorRGBFromHex("#ff0000")) 148 // 149 // 2. Make Paragraph green with 8-bit rgb values (0-255 each component) 150 // p.SetColor(creator.ColorRGBFrom8bit(0, 255, 0) 151 // 152 // 3. Make Paragraph blue with arithmetic (0-1) rgb components. 153 // p.SetColor(creator.ColorRGBFromArithmetic(0, 0, 1.0) 154 // 155 func (p *Paragraph) SetColor(col Color) { 156 pdfColor := model.NewPdfColorDeviceRGB(col.ToRGB()) 157 p.color = *pdfColor 158 } 159 160 // SetPos sets absolute positioning with specified coordinates. 161 func (p *Paragraph) SetPos(x, y float64) { 162 p.positioning = positionAbsolute 163 p.xPos = x 164 p.yPos = y 165 } 166 167 // SetAngle sets the rotation angle of the text. 168 func (p *Paragraph) SetAngle(angle float64) { 169 p.angle = angle 170 } 171 172 // SetMargins sets the Paragraph's margins. 173 func (p *Paragraph) SetMargins(left, right, top, bottom float64) { 174 p.margins.left = left 175 p.margins.right = right 176 p.margins.top = top 177 p.margins.bottom = bottom 178 } 179 180 // GetMargins returns the Paragraph's margins: left, right, top, bottom. 181 func (p *Paragraph) GetMargins() (float64, float64, float64, float64) { 182 return p.margins.left, p.margins.right, p.margins.top, p.margins.bottom 183 } 184 185 // SetWidth sets the the Paragraph width. This is essentially the wrapping width, i.e. the width the text can extend to 186 // prior to wrapping over to next line. 187 func (p *Paragraph) SetWidth(width float64) { 188 p.wrapWidth = width 189 p.wrapText() 190 } 191 192 // Width returns the width of the Paragraph. 193 func (p *Paragraph) Width() float64 { 194 if p.enableWrap { 195 return p.wrapWidth 196 } 197 return p.getTextWidth() / 1000.0 198 } 199 200 // Height returns the height of the Paragraph. The height is calculated based on the input text and how it is wrapped 201 // within the container. Does not include Margins. 202 func (p *Paragraph) Height() float64 { 203 if p.textLines == nil || len(p.textLines) == 0 { 204 p.wrapText() 205 } 206 207 h := float64(len(p.textLines)) * p.lineHeight * p.fontSize 208 return h 209 } 210 211 // getTextWidth calculates the text width as if all in one line (not taking wrapping into account). 212 func (p *Paragraph) getTextWidth() float64 { 213 w := float64(0.0) 214 215 for _, rune := range p.text { 216 glyph, found := p.encoder.RuneToGlyph(rune) 217 if !found { 218 common.Log.Debug("Error! Glyph not found for rune: %s\n", rune) 219 return -1 // XXX/FIXME: return error. 220 } 221 222 // Ignore newline for this.. Handles as if all in one line. 223 if glyph == "controlLF" { 224 continue 225 } 226 227 metrics, found := p.textFont.GetGlyphCharMetrics(glyph) 228 if !found { 229 common.Log.Debug("Glyph char metrics not found! %s\n", glyph) 230 return -1 // XXX/FIXME: return error. 231 } 232 w += p.fontSize * metrics.Wx 233 } 234 235 return w 236 } 237 238 // Simple algorithm to wrap the text into lines (greedy algorithm - fill the lines). 239 // XXX/TODO: Consider the Knuth/Plass algorithm or an alternative. 240 func (p *Paragraph) wrapText() error { 241 if !p.enableWrap { 242 p.textLines = []string{p.text} 243 return nil 244 } 245 246 line := []rune{} 247 lineWidth := float64(0.0) 248 p.textLines = []string{} 249 250 runes := []rune(p.text) 251 glyphs := []string{} 252 widths := []float64{} 253 254 for _, val := range runes { 255 glyph, found := p.encoder.RuneToGlyph(val) 256 if !found { 257 common.Log.Debug("Error! Glyph not found for rune: %v\n", val) 258 return errors.New("Glyph not found for rune") // XXX/FIXME: return error. 259 } 260 261 // Newline wrapping. 262 if glyph == "controlLF" { 263 // Moves to next line. 264 p.textLines = append(p.textLines, string(line)) 265 line = []rune{} 266 lineWidth = 0 267 widths = []float64{} 268 glyphs = []string{} 269 continue 270 } 271 272 metrics, found := p.textFont.GetGlyphCharMetrics(glyph) 273 if !found { 274 common.Log.Debug("Glyph char metrics not found! %s\n", glyph) 275 return errors.New("Glyph char metrics missing") // XXX/FIXME: return error. 276 } 277 278 w := p.fontSize * metrics.Wx 279 if lineWidth+w > p.wrapWidth*1000.0 { 280 // Goes out of bounds: Wrap. 281 // Breaks on the character. 282 // XXX/TODO: when goes outside: back up to next space, otherwise break on the character. 283 idx := -1 284 for i := len(glyphs) - 1; i >= 0; i-- { 285 if glyphs[i] == "space" { 286 idx = i 287 break 288 } 289 } 290 if idx > 0 { 291 p.textLines = append(p.textLines, string(line[0:idx+1])) 292 293 line = line[idx+1:] 294 line = append(line, val) 295 296 glyphs = glyphs[idx+1:] 297 glyphs = append(glyphs, glyph) 298 widths = widths[idx+1:] 299 widths = append(widths, w) 300 301 lineWidth = 0 302 for _, width := range widths { 303 lineWidth += width 304 } 305 306 } else { 307 p.textLines = append(p.textLines, string(line)) 308 line = []rune{val} 309 lineWidth = w 310 widths = []float64{w} 311 glyphs = []string{glyph} 312 } 313 } else { 314 line = append(line, val) 315 lineWidth += w 316 glyphs = append(glyphs, glyph) 317 widths = append(widths, w) 318 } 319 } 320 if len(line) > 0 { 321 p.textLines = append(p.textLines, string(line)) 322 } 323 324 return nil 325 } 326 327 // GeneratePageBlocks generates the page blocks. Multiple blocks are generated if the contents wrap over 328 // multiple pages. Implements the Drawable interface. 329 func (p *Paragraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { 330 origContext := ctx 331 blocks := []*Block{} 332 333 blk := NewBlock(ctx.PageWidth, ctx.PageHeight) 334 if p.positioning.isRelative() { 335 // Account for Paragraph Margins. 336 ctx.X += p.margins.left 337 ctx.Y += p.margins.top 338 ctx.Width -= p.margins.left + p.margins.right 339 ctx.Height -= p.margins.top + p.margins.bottom 340 341 // Use available space. 342 p.SetWidth(ctx.Width) 343 344 if p.Height() > ctx.Height { 345 // Goes out of the bounds. Write on a new template instead and create a new context at upper 346 // left corner. 347 // XXX/TODO: Handle case when Paragraph is larger than the Page... 348 // Should be fine if we just break on the paragraph, i.e. splitting it up over 2+ pages 349 350 blocks = append(blocks, blk) 351 blk = NewBlock(ctx.PageWidth, ctx.PageHeight) 352 353 // New Page. 354 ctx.Page++ 355 newContext := ctx 356 newContext.Y = ctx.Margins.top // + p.Margins.top 357 newContext.X = ctx.Margins.left + p.margins.left 358 newContext.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom - p.margins.bottom 359 newContext.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right - p.margins.left - p.margins.right 360 ctx = newContext 361 } 362 } else { 363 // Absolute. 364 if p.wrapWidth == 0 { 365 // Use necessary space. 366 p.SetWidth(p.getTextWidth()) 367 } 368 ctx.X = p.xPos 369 ctx.Y = p.yPos 370 } 371 372 // Place the Paragraph on the template at position (x,y) based on the ctx. 373 ctx, err := drawParagraphOnBlock(blk, p, ctx) 374 if err != nil { 375 common.Log.Debug("ERROR: %v", err) 376 return nil, ctx, err 377 } 378 379 blocks = append(blocks, blk) 380 if p.positioning.isRelative() { 381 ctx.X -= p.margins.left // Move back. 382 ctx.Width = origContext.Width 383 return blocks, ctx, nil 384 } 385 // Absolute: not changing the context. 386 return blocks, origContext, nil 387 } 388 389 // Draw block on specified location on Page, adding to the content stream. 390 func drawParagraphOnBlock(blk *Block, p *Paragraph, ctx DrawContext) (DrawContext, error) { 391 // Find a free name for the font. 392 num := 1 393 fontName := core.PdfObjectName(fmt.Sprintf("Font%d", num)) 394 for blk.resources.HasFontByName(fontName) { 395 num++ 396 fontName = core.PdfObjectName(fmt.Sprintf("Font%d", num)) 397 } 398 399 // Add to the Page resources. 400 err := blk.resources.SetFontByName(fontName, p.textFont.ToPdfObject()) 401 if err != nil { 402 return ctx, err 403 } 404 405 // Wrap the text into lines. 406 p.wrapText() 407 408 // Create the content stream. 409 cc := contentstream.NewContentCreator() 410 cc.Add_q() 411 412 yPos := ctx.PageHeight - ctx.Y - p.fontSize*p.lineHeight 413 414 cc.Translate(ctx.X, yPos) 415 if p.angle != 0 { 416 cc.RotateDeg(p.angle) 417 } 418 419 cc.Add_BT(). 420 Add_rg(p.color.R(), p.color.G(), p.color.B()). 421 Add_Tf(fontName, p.fontSize). 422 Add_TL(p.fontSize * p.lineHeight) 423 424 for idx, line := range p.textLines { 425 if idx != 0 { 426 // Move to next line if not first. 427 cc.Add_Tstar() 428 } 429 430 runes := []rune(line) 431 432 // Get width of the line (excluding spaces). 433 w := float64(0) 434 spaces := 0 435 for _, runeVal := range runes { 436 glyph, found := p.encoder.RuneToGlyph(runeVal) 437 if !found { 438 common.Log.Debug("Rune 0x%x not supported by text encoder", runeVal) 439 return ctx, errors.New("Unsupported rune in text encoding") 440 } 441 if glyph == "space" { 442 spaces++ 443 continue 444 } 445 if glyph == "controlLF" { 446 continue 447 } 448 metrics, found := p.textFont.GetGlyphCharMetrics(glyph) 449 if !found { 450 common.Log.Debug("Unsupported glyph %s in font\n", glyph) 451 return ctx, errors.New("Unsupported text glyph") 452 } 453 454 w += p.fontSize * metrics.Wx 455 } 456 457 objs := []core.PdfObject{} 458 459 spaceMetrics, found := p.textFont.GetGlyphCharMetrics("space") 460 if !found { 461 return ctx, errors.New("The font does not have a space glyph") 462 } 463 spaceWidth := spaceMetrics.Wx 464 if p.alignment == TextAlignmentJustify { 465 if spaces > 0 && idx < len(p.textLines)-1 { // Not to justify last line. 466 spaceWidth = (p.wrapWidth*1000.0 - w) / float64(spaces) / p.fontSize 467 } 468 } else if p.alignment == TextAlignmentCenter { 469 // Start with a shift. 470 textWidth := w + float64(spaces)*spaceWidth*p.fontSize 471 shift := (p.wrapWidth*1000.0 - textWidth) / 2 / p.fontSize 472 objs = append(objs, core.MakeFloat(-shift)) 473 } else if p.alignment == TextAlignmentRight { 474 textWidth := w + float64(spaces)*spaceWidth*p.fontSize 475 shift := (p.wrapWidth*1000.0 - textWidth) / p.fontSize 476 objs = append(objs, core.MakeFloat(-shift)) 477 } 478 479 encStr := "" 480 for _, runeVal := range runes { 481 //creator.Add_Tj(core.PdfObjectString(tb.Encoder.Encode(line))) 482 glyph, found := p.encoder.RuneToGlyph(runeVal) 483 if !found { 484 common.Log.Debug("Rune 0x%x not supported by text encoder", runeVal) 485 return ctx, errors.New("Unsupported rune in text encoding") 486 } 487 488 if glyph == "space" { 489 if !found { 490 common.Log.Debug("Unsupported glyph %s in font\n", glyph) 491 return ctx, errors.New("Unsupported text glyph") 492 } 493 494 if len(encStr) > 0 { 495 objs = append(objs, core.MakeString(encStr)) 496 encStr = "" 497 } 498 objs = append(objs, core.MakeFloat(-spaceWidth)) 499 } else { 500 encStr += string(p.encoder.Encode(string(runeVal))) 501 } 502 } 503 if len(encStr) > 0 { 504 objs = append(objs, core.MakeString(encStr)) 505 } 506 507 cc.Add_TJ(objs...) 508 } 509 cc.Add_ET() 510 cc.Add_Q() 511 512 ops := cc.Operations() 513 ops.WrapIfNeeded() 514 515 blk.addContents(ops) 516 517 if p.positioning.isRelative() { 518 pHeight := p.Height() + p.margins.bottom 519 ctx.Y += pHeight 520 ctx.Height -= pHeight 521 522 // If the division is inline, calculate context new X coordinate. 523 if ctx.Inline { 524 ctx.X += p.Width() + p.margins.right 525 } 526 } 527 528 return ctx, nil 529 }