github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/creator/block.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 ) 17 18 // Block contains a portion of PDF Page contents. It has a width and a position and can 19 // be placed anywhere on a Page. It can even contain a whole Page, and is used in the creator 20 // where each Drawable object can output one or more blocks, each representing content for separate pages 21 // (typically needed when Page breaks occur). 22 type Block struct { 23 // Block contents and resources. 24 contents *contentstream.ContentStreamOperations 25 resources *model.PdfPageResources 26 27 // Positioning: relative / absolute. 28 positioning positioning 29 30 // Absolute coordinates (when in absolute mode). 31 xPos, yPos float64 32 33 // The bounding box for the block. 34 width float64 35 height float64 36 37 // Rotation angle. 38 angle float64 39 40 // Margins to be applied around the block when drawing on Page. 41 margins margins 42 } 43 44 // NewBlock creates a new Block with specified width and height. 45 func NewBlock(width float64, height float64) *Block { 46 b := &Block{} 47 b.contents = &contentstream.ContentStreamOperations{} 48 b.resources = model.NewPdfPageResources() 49 b.width = width 50 b.height = height 51 return b 52 } 53 54 // NewBlockFromPage creates a Block from a PDF Page. Useful for loading template pages as blocks from a PDF document 55 // and additional content with the creator. 56 func NewBlockFromPage(page *model.PdfPage) (*Block, error) { 57 b := &Block{} 58 59 content, err := page.GetAllContentStreams() 60 if err != nil { 61 return nil, err 62 } 63 64 contentParser := contentstream.NewContentStreamParser(content) 65 operations, err := contentParser.Parse() 66 if err != nil { 67 return nil, err 68 } 69 operations.WrapIfNeeded() 70 71 b.contents = operations 72 73 if page.Resources != nil { 74 b.resources = page.Resources 75 } else { 76 b.resources = model.NewPdfPageResources() 77 } 78 79 mbox, err := page.GetMediaBox() 80 if err != nil { 81 return nil, err 82 } 83 84 if mbox.Llx != 0 || mbox.Lly != 0 { 85 // Account for media box offset if any. 86 b.translate(-mbox.Llx, mbox.Lly) 87 } 88 b.width = mbox.Urx - mbox.Llx 89 b.height = mbox.Ury - mbox.Lly 90 91 return b, nil 92 } 93 94 // SetAngle sets the rotation angle in degrees. 95 func (blk *Block) SetAngle(angleDeg float64) { 96 blk.angle = angleDeg 97 } 98 99 // duplicate duplicates the block with a new copy of the operations list. 100 func (blk *Block) duplicate() *Block { 101 dup := &Block{} 102 103 // Copy over. 104 *dup = *blk 105 106 dupContents := contentstream.ContentStreamOperations{} 107 for _, op := range *blk.contents { 108 dupContents = append(dupContents, op) 109 } 110 dup.contents = &dupContents 111 112 return dup 113 } 114 115 // GeneratePageBlocks draws the block contents on a template Page block. 116 // Implements the Drawable interface. 117 func (blk *Block) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { 118 blocks := []*Block{} 119 120 if blk.positioning.isRelative() { 121 // Draw at current ctx.X, ctx.Y position 122 dup := blk.duplicate() 123 cc := contentstream.NewContentCreator() 124 cc.Translate(ctx.X, ctx.PageHeight-ctx.Y-blk.height) 125 if blk.angle != 0 { 126 // Make the rotation about the upper left corner. 127 // XXX/TODO: Account for rotation origin. (Consider). 128 cc.Translate(0, blk.Height()) 129 cc.RotateDeg(blk.angle) 130 cc.Translate(0, -blk.Height()) 131 } 132 contents := append(*cc.Operations(), *dup.contents...) 133 contents.WrapIfNeeded() 134 dup.contents = &contents 135 136 blocks = append(blocks, dup) 137 138 ctx.Y += blk.height 139 } else { 140 // Absolute. Draw at blk.xPos, blk.yPos position 141 dup := blk.duplicate() 142 cc := contentstream.NewContentCreator() 143 cc.Translate(blk.xPos, ctx.PageHeight-blk.yPos-blk.height) 144 if blk.angle != 0 { 145 // Make the rotation about the upper left corner. 146 // XXX/TODO: Consider supporting specification of rotation origin. 147 cc.Translate(0, blk.Height()) 148 cc.RotateDeg(blk.angle) 149 cc.Translate(0, -blk.Height()) 150 } 151 contents := append(*cc.Operations(), *dup.contents...) 152 contents.WrapIfNeeded() 153 dup.contents = &contents 154 155 blocks = append(blocks, dup) 156 } 157 158 return blocks, ctx, nil 159 } 160 161 // Height returns the Block's height. 162 func (blk *Block) Height() float64 { 163 return blk.height 164 } 165 166 // Width returns the Block's width. 167 func (blk *Block) Width() float64 { 168 return blk.width 169 } 170 171 // addContents adds contents to a block. Wrap both existing and new contents to ensure 172 // independence of content operations. 173 func (blk *Block) addContents(operations *contentstream.ContentStreamOperations) { 174 blk.contents.WrapIfNeeded() 175 operations.WrapIfNeeded() 176 *blk.contents = append(*blk.contents, *operations...) 177 } 178 179 // addContentsByString adds contents to a block by contents string. 180 func (blk *Block) addContentsByString(contents string) error { 181 cc := contentstream.NewContentStreamParser(contents) 182 operations, err := cc.Parse() 183 if err != nil { 184 return err 185 } 186 187 blk.contents.WrapIfNeeded() 188 operations.WrapIfNeeded() 189 *blk.contents = append(*blk.contents, *operations...) 190 191 return nil 192 } 193 194 // SetMargins sets the Block's left, right, top, bottom, margins. 195 func (blk *Block) SetMargins(left, right, top, bottom float64) { 196 blk.margins.left = left 197 blk.margins.right = right 198 blk.margins.top = top 199 blk.margins.bottom = bottom 200 } 201 202 // GetMargins returns the Block's margins: left, right, top, bottom. 203 func (blk *Block) GetMargins() (float64, float64, float64, float64) { 204 return blk.margins.left, blk.margins.right, blk.margins.top, blk.margins.bottom 205 } 206 207 // SetPos sets the Block's positioning to absolute mode with the specified coordinates. 208 func (blk *Block) SetPos(x, y float64) { 209 blk.positioning = positionAbsolute 210 blk.xPos = x 211 blk.yPos = y 212 } 213 214 // Scale block by specified factors in the x and y directions. 215 func (blk *Block) Scale(sx, sy float64) { 216 ops := contentstream.NewContentCreator(). 217 Scale(sx, sy). 218 Operations() 219 220 *blk.contents = append(*ops, *blk.contents...) 221 blk.contents.WrapIfNeeded() 222 223 blk.width *= sx 224 blk.height *= sy 225 } 226 227 // ScaleToWidth scales the Block to a specified width, maintaining the same aspect ratio. 228 func (blk *Block) ScaleToWidth(w float64) { 229 ratio := w / blk.width 230 blk.Scale(ratio, ratio) 231 } 232 233 // ScaleToHeight scales the Block to a specified height, maintaining the same aspect ratio. 234 func (blk *Block) ScaleToHeight(h float64) { 235 ratio := h / blk.height 236 blk.Scale(ratio, ratio) 237 } 238 239 // translate translates the block, moving block contents on the PDF. For internal use. 240 func (blk *Block) translate(tx, ty float64) { 241 ops := contentstream.NewContentCreator(). 242 Translate(tx, -ty). 243 Operations() 244 245 *blk.contents = append(*ops, *blk.contents...) 246 blk.contents.WrapIfNeeded() 247 } 248 249 // drawToPage draws the block on a PdfPage. Generates the content streams and appends to the PdfPage's content 250 // stream and links needed resources. 251 func (blk *Block) drawToPage(page *model.PdfPage) error { 252 // Check if Page contents are wrapped - if not wrap it. 253 content, err := page.GetAllContentStreams() 254 if err != nil { 255 return err 256 } 257 258 contentParser := contentstream.NewContentStreamParser(content) 259 ops, err := contentParser.Parse() 260 if err != nil { 261 return err 262 } 263 ops.WrapIfNeeded() 264 265 // Ensure resource dictionaries are available. 266 if page.Resources == nil { 267 page.Resources = model.NewPdfPageResources() 268 } 269 270 // Merge the contents into ops. 271 err = mergeContents(ops, page.Resources, blk.contents, blk.resources) 272 if err != nil { 273 return err 274 } 275 276 err = page.SetContentStreams([]string{string(ops.Bytes())}, core.NewFlateEncoder()) 277 if err != nil { 278 return err 279 } 280 281 return nil 282 } 283 284 // Draw draws the drawable d on the block. 285 // Note that the drawable must not wrap, i.e. only return one block. Otherwise an error is returned. 286 func (blk *Block) Draw(d Drawable) error { 287 ctx := DrawContext{} 288 ctx.Width = blk.width 289 ctx.Height = blk.height 290 ctx.PageWidth = blk.width 291 ctx.PageHeight = blk.height 292 ctx.X = 0 // Upper left corner of block 293 ctx.Y = 0 294 295 blocks, _, err := d.GeneratePageBlocks(ctx) 296 if err != nil { 297 return err 298 } 299 300 if len(blocks) != 1 { 301 return errors.New("Too many output blocks") 302 } 303 304 for _, newBlock := range blocks { 305 err := mergeContents(blk.contents, blk.resources, newBlock.contents, newBlock.resources) 306 if err != nil { 307 return err 308 } 309 } 310 311 return nil 312 } 313 314 // DrawWithContext draws the Block using the specified drawing context. 315 func (blk *Block) DrawWithContext(d Drawable, ctx DrawContext) error { 316 blocks, _, err := d.GeneratePageBlocks(ctx) 317 if err != nil { 318 return err 319 } 320 321 if len(blocks) != 1 { 322 return errors.New("Too many output blocks") 323 } 324 325 for _, newBlock := range blocks { 326 err := mergeContents(blk.contents, blk.resources, newBlock.contents, newBlock.resources) 327 if err != nil { 328 return err 329 } 330 } 331 332 return nil 333 } 334 335 // mergeBlocks appends another block onto the block. 336 func (blk *Block) mergeBlocks(toAdd *Block) error { 337 err := mergeContents(blk.contents, blk.resources, toAdd.contents, toAdd.resources) 338 return err 339 } 340 341 // mergeContents merges contents and content streams. 342 // Active in the sense that it modified the input contents and resources. 343 func mergeContents(contents *contentstream.ContentStreamOperations, resources *model.PdfPageResources, 344 contentsToAdd *contentstream.ContentStreamOperations, resourcesToAdd *model.PdfPageResources) error { 345 346 // To properly add contents from a block, we need to handle the resources that the block is 347 // using and make sure it is accessible in the modified Page. 348 // 349 // Currently supporting: Font, XObject, Colormap, Pattern, Shading, GState resources 350 // from the block. 351 // 352 353 xobjectMap := map[core.PdfObjectName]core.PdfObjectName{} 354 fontMap := map[core.PdfObjectName]core.PdfObjectName{} 355 csMap := map[core.PdfObjectName]core.PdfObjectName{} 356 patternMap := map[core.PdfObjectName]core.PdfObjectName{} 357 shadingMap := map[core.PdfObjectName]core.PdfObjectName{} 358 gstateMap := map[core.PdfObjectName]core.PdfObjectName{} 359 360 for _, op := range *contentsToAdd { 361 switch op.Operand { 362 case "Do": 363 // XObject. 364 if len(op.Params) == 1 { 365 if name, ok := op.Params[0].(*core.PdfObjectName); ok { 366 if _, processed := xobjectMap[*name]; !processed { 367 var useName core.PdfObjectName 368 // Process if not already processed.. 369 obj, _ := resourcesToAdd.GetXObjectByName(*name) 370 if obj != nil { 371 useName = *name 372 for { 373 obj2, _ := resources.GetXObjectByName(useName) 374 if obj2 == nil || obj2 == obj { 375 break 376 } 377 // If there is a conflict... then append "0" to the name.. 378 useName = useName + "0" 379 } 380 } 381 382 resources.SetXObjectByName(useName, obj) 383 xobjectMap[*name] = useName 384 } 385 useName := xobjectMap[*name] 386 op.Params[0] = &useName 387 } 388 } 389 case "Tf": 390 // Font. 391 if len(op.Params) == 2 { 392 if name, ok := op.Params[0].(*core.PdfObjectName); ok { 393 if _, processed := fontMap[*name]; !processed { 394 var useName core.PdfObjectName 395 // Process if not already processed. 396 obj, found := resourcesToAdd.GetFontByName(*name) 397 if found { 398 useName = *name 399 for { 400 obj2, found := resources.GetFontByName(useName) 401 if !found || obj2 == obj { 402 break 403 } 404 useName = useName + "0" 405 } 406 } 407 408 resources.SetFontByName(useName, obj) 409 fontMap[*name] = useName 410 } 411 412 useName := fontMap[*name] 413 op.Params[0] = &useName 414 } 415 } 416 case "CS", "cs": 417 // Colorspace. 418 if len(op.Params) == 1 { 419 if name, ok := op.Params[0].(*core.PdfObjectName); ok { 420 if _, processed := csMap[*name]; !processed { 421 var useName core.PdfObjectName 422 // Process if not already processed. 423 cs, found := resourcesToAdd.GetColorspaceByName(*name) 424 if found { 425 useName = *name 426 for { 427 cs2, found := resources.GetColorspaceByName(useName) 428 if !found || cs == cs2 { 429 break 430 } 431 useName = useName + "0" 432 } 433 434 resources.SetColorspaceByName(useName, cs) 435 csMap[*name] = useName 436 } else { 437 common.Log.Debug("Colorspace not found") 438 } 439 } 440 441 if useName, has := csMap[*name]; has { 442 op.Params[0] = &useName 443 } else { 444 common.Log.Debug("Error: Colorspace %s not found", *name) 445 } 446 } 447 } 448 case "SCN", "scn": 449 if len(op.Params) == 1 { 450 if name, ok := op.Params[0].(*core.PdfObjectName); ok { 451 if _, processed := patternMap[*name]; !processed { 452 var useName core.PdfObjectName 453 p, found := resourcesToAdd.GetPatternByName(*name) 454 if found { 455 useName = *name 456 for { 457 p2, found := resources.GetPatternByName(useName) 458 if !found || p2 == p { 459 break 460 } 461 useName = useName + "0" 462 } 463 464 err := resources.SetPatternByName(useName, p.ToPdfObject()) 465 if err != nil { 466 return err 467 } 468 469 patternMap[*name] = useName 470 } 471 } 472 473 if useName, has := patternMap[*name]; has { 474 op.Params[0] = &useName 475 } 476 } 477 } 478 case "sh": 479 // Shading. 480 if len(op.Params) == 1 { 481 if name, ok := op.Params[0].(*core.PdfObjectName); ok { 482 if _, processed := shadingMap[*name]; !processed { 483 var useName core.PdfObjectName 484 // Process if not already processed. 485 sh, found := resourcesToAdd.GetShadingByName(*name) 486 if found { 487 useName = *name 488 for { 489 sh2, found := resources.GetShadingByName(useName) 490 if !found || sh == sh2 { 491 break 492 } 493 useName = useName + "0" 494 } 495 496 err := resources.SetShadingByName(useName, sh.ToPdfObject()) 497 if err != nil { 498 common.Log.Debug("ERROR Set shading: %v", err) 499 return err 500 } 501 502 shadingMap[*name] = useName 503 } else { 504 common.Log.Debug("Shading not found") 505 } 506 } 507 508 if useName, has := shadingMap[*name]; has { 509 op.Params[0] = &useName 510 } else { 511 common.Log.Debug("Error: Shading %s not found", *name) 512 } 513 } 514 } 515 case "gs": 516 // ExtGState. 517 if len(op.Params) == 1 { 518 if name, ok := op.Params[0].(*core.PdfObjectName); ok { 519 if _, processed := gstateMap[*name]; !processed { 520 var useName core.PdfObjectName 521 // Process if not already processed. 522 gs, found := resourcesToAdd.GetExtGState(*name) 523 if found { 524 useName = *name 525 i := 1 526 for { 527 gs2, found := resources.GetExtGState(useName) 528 if !found || gs == gs2 { 529 break 530 } 531 useName = core.PdfObjectName(fmt.Sprintf("GS%d", i)) 532 i++ 533 } 534 } 535 536 resources.AddExtGState(useName, gs) 537 gstateMap[*name] = useName 538 } 539 540 useName := gstateMap[*name] 541 op.Params[0] = &useName 542 } 543 } 544 } 545 546 *contents = append(*contents, op) 547 } 548 549 return nil 550 }