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  }