github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/creator/image.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  	"bytes"
    10  	"fmt"
    11  	goimage "image"
    12  
    13  	"io/ioutil"
    14  
    15  	"github.com/unidoc/unidoc/common"
    16  	"github.com/unidoc/unidoc/pdf/contentstream"
    17  	"github.com/unidoc/unidoc/pdf/core"
    18  	"github.com/unidoc/unidoc/pdf/model"
    19  )
    20  
    21  // The Image type is used to draw an image onto PDF.
    22  type Image struct {
    23  	xobj *model.XObjectImage
    24  	img  *model.Image
    25  
    26  	// Rotation angle.
    27  	angle float64
    28  
    29  	// The dimensions of the image. As to be placed on the PDF.
    30  	width, height float64
    31  
    32  	// The original dimensions of the image (pixel based).
    33  	origWidth, origHeight float64
    34  
    35  	// Positioning: relative / absolute.
    36  	positioning positioning
    37  
    38  	// Absolute coordinates (when in absolute mode).
    39  	xPos float64
    40  	yPos float64
    41  
    42  	// Opacity (alpha value).
    43  	opacity float64
    44  
    45  	// Margins to be applied around the block when drawing on Page.
    46  	margins margins
    47  
    48  	// Rotional origin.  Default (0,0 - upper left corner of block).
    49  	rotOriginX, rotOriginY float64
    50  
    51  	// Encoder
    52  	encoder core.StreamEncoder
    53  }
    54  
    55  // NewImage create a new image from a unidoc image (model.Image).
    56  func NewImage(img *model.Image) (*Image, error) {
    57  	image := &Image{}
    58  	image.img = img
    59  
    60  	// Image original size in points = pixel size.
    61  	image.origWidth = float64(img.Width)
    62  	image.origHeight = float64(img.Height)
    63  	image.width = image.origWidth
    64  	image.height = image.origHeight
    65  	image.angle = 0
    66  	image.opacity = 1.0
    67  
    68  	image.positioning = positionRelative
    69  
    70  	return image, nil
    71  }
    72  
    73  // NewImageFromData creates an Image from image data.
    74  func NewImageFromData(data []byte) (*Image, error) {
    75  	imgReader := bytes.NewReader(data)
    76  
    77  	// Load the image with default handler.
    78  	img, err := model.ImageHandling.Read(imgReader)
    79  	if err != nil {
    80  		common.Log.Error("Error loading image: %s", err)
    81  		return nil, err
    82  	}
    83  
    84  	return NewImage(img)
    85  }
    86  
    87  // NewImageFromFile creates an Image from a file.
    88  func NewImageFromFile(path string) (*Image, error) {
    89  	imgData, err := ioutil.ReadFile(path)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	img, err := NewImageFromData(imgData)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return img, nil
   100  }
   101  
   102  // NewImageFromGoImage creates an Image from a go image.Image datastructure.
   103  func NewImageFromGoImage(goimg goimage.Image) (*Image, error) {
   104  	img, err := model.ImageHandling.NewImageFromGoImage(goimg)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	return NewImage(img)
   110  }
   111  
   112  // SetEncoder sets the encoding/compression mechanism for the image.
   113  func (img *Image) SetEncoder(encoder core.StreamEncoder) {
   114  	img.encoder = encoder
   115  }
   116  
   117  // Height returns Image's document height.
   118  func (img *Image) Height() float64 {
   119  	return img.height
   120  }
   121  
   122  // Width returns Image's document width.
   123  func (img *Image) Width() float64 {
   124  	return img.width
   125  }
   126  
   127  // SetOpacity sets opacity for Image.
   128  func (img *Image) SetOpacity(opacity float64) {
   129  	img.opacity = opacity
   130  }
   131  
   132  // SetMargins sets the margins for the Image (in relative mode): left, right, top, bottom.
   133  func (img *Image) SetMargins(left, right, top, bottom float64) {
   134  	img.margins.left = left
   135  	img.margins.right = right
   136  	img.margins.top = top
   137  	img.margins.bottom = bottom
   138  }
   139  
   140  // GetMargins returns the Image's margins: left, right, top, bottom.
   141  func (img *Image) GetMargins() (float64, float64, float64, float64) {
   142  	return img.margins.left, img.margins.right, img.margins.top, img.margins.bottom
   143  }
   144  
   145  // makeXObject makes the encoded XObject Image that will be used in the PDF.
   146  func (img *Image) makeXObject() error {
   147  	encoder := img.encoder
   148  	if encoder == nil {
   149  		// Default: Use flate encoder.
   150  		encoder = core.NewFlateEncoder()
   151  	}
   152  
   153  	// Create the XObject image.
   154  	ximg, err := model.NewXObjectImageFromImage(img.img, nil, encoder)
   155  	if err != nil {
   156  		common.Log.Error("Failed to create xobject image: %s", err)
   157  		return err
   158  	}
   159  
   160  	img.xobj = ximg
   161  	return nil
   162  }
   163  
   164  // GeneratePageBlocks generate the Page blocks. Draws the Image on a block, implementing the Drawable interface.
   165  func (img *Image) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
   166  	if img.xobj == nil {
   167  		// Build the XObject Image if not already prepared.
   168  		img.makeXObject()
   169  	}
   170  
   171  	blocks := []*Block{}
   172  	origCtx := ctx
   173  
   174  	blk := NewBlock(ctx.PageWidth, ctx.PageHeight)
   175  	if img.positioning.isRelative() {
   176  		if img.height > ctx.Height {
   177  			// Goes out of the bounds.  Write on a new template instead and create a new context at upper
   178  			// left corner.
   179  
   180  			blocks = append(blocks, blk)
   181  			blk = NewBlock(ctx.PageWidth, ctx.PageHeight)
   182  
   183  			// New Page.
   184  			ctx.Page++
   185  			newContext := ctx
   186  			newContext.Y = ctx.Margins.top // + p.Margins.top
   187  			newContext.X = ctx.Margins.left + img.margins.left
   188  			newContext.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom - img.margins.bottom
   189  			newContext.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right - img.margins.left - img.margins.right
   190  			ctx = newContext
   191  		} else {
   192  			ctx.Y += img.margins.top
   193  			ctx.Height -= img.margins.top + img.margins.bottom
   194  			ctx.X += img.margins.left
   195  			ctx.Width -= img.margins.left + img.margins.right
   196  		}
   197  	} else {
   198  		// Absolute.
   199  		ctx.X = img.xPos
   200  		ctx.Y = img.yPos
   201  	}
   202  
   203  	// Place the Image on the template at position (x,y) based on the ctx.
   204  	ctx, err := drawImageOnBlock(blk, img, ctx)
   205  	if err != nil {
   206  		return nil, ctx, err
   207  	}
   208  
   209  	blocks = append(blocks, blk)
   210  
   211  	if img.positioning.isAbsolute() {
   212  		// Absolute drawing should not affect context.
   213  		ctx = origCtx
   214  	} else {
   215  		// XXX/TODO: Use projected height.
   216  		ctx.Y += img.margins.bottom
   217  		ctx.Height -= img.margins.bottom
   218  	}
   219  
   220  	return blocks, ctx, nil
   221  }
   222  
   223  // SetPos sets the absolute position. Changes object positioning to absolute.
   224  func (img *Image) SetPos(x, y float64) {
   225  	img.positioning = positionAbsolute
   226  	img.xPos = x
   227  	img.yPos = y
   228  }
   229  
   230  // Scale scales Image by a constant factor, both width and height.
   231  func (img *Image) Scale(xFactor, yFactor float64) {
   232  	img.width = xFactor * img.width
   233  	img.height = yFactor * img.height
   234  }
   235  
   236  // ScaleToWidth scale Image to a specified width w, maintaining the aspect ratio.
   237  func (img *Image) ScaleToWidth(w float64) {
   238  	ratio := img.height / img.width
   239  	img.width = w
   240  	img.height = w * ratio
   241  }
   242  
   243  // ScaleToHeight scale Image to a specified height h, maintaining the aspect ratio.
   244  func (img *Image) ScaleToHeight(h float64) {
   245  	ratio := img.width / img.height
   246  	img.height = h
   247  	img.width = h * ratio
   248  }
   249  
   250  // SetWidth set the Image's document width to specified w. This does not change the raw image data, i.e.
   251  // no actual scaling of data is performed. That is handled by the PDF viewer.
   252  func (img *Image) SetWidth(w float64) {
   253  	img.width = w
   254  }
   255  
   256  // SetHeight sets the Image's document height to specified h.
   257  func (img *Image) SetHeight(h float64) {
   258  	img.height = h
   259  }
   260  
   261  // SetAngle sets Image rotation angle in degrees.
   262  func (img *Image) SetAngle(angle float64) {
   263  	img.angle = angle
   264  }
   265  
   266  // Draw the image onto the specified blk.
   267  func drawImageOnBlock(blk *Block, img *Image, ctx DrawContext) (DrawContext, error) {
   268  	origCtx := ctx
   269  
   270  	// Find a free name for the image.
   271  	num := 1
   272  	imgName := core.PdfObjectName(fmt.Sprintf("Img%d", num))
   273  	for blk.resources.HasXObjectByName(imgName) {
   274  		num++
   275  		imgName = core.PdfObjectName(fmt.Sprintf("Img%d", num))
   276  	}
   277  
   278  	// Add to the Page resources.
   279  	err := blk.resources.SetXObjectImageByName(imgName, img.xobj)
   280  	if err != nil {
   281  		return ctx, err
   282  	}
   283  
   284  	// Find an available GS name.
   285  	i := 0
   286  	gsName := core.PdfObjectName(fmt.Sprintf("GS%d", i))
   287  	for blk.resources.HasExtGState(gsName) {
   288  		i++
   289  		gsName = core.PdfObjectName(fmt.Sprintf("GS%d", i))
   290  	}
   291  
   292  	// Graphics state with normal blend mode.
   293  	gs0 := core.MakeDict()
   294  	gs0.Set("BM", core.MakeName("Normal"))
   295  	if img.opacity < 1.0 {
   296  		gs0.Set("CA", core.MakeFloat(img.opacity))
   297  		gs0.Set("ca", core.MakeFloat(img.opacity))
   298  	}
   299  
   300  	err = blk.resources.AddExtGState(gsName, core.MakeIndirectObject(gs0))
   301  	if err != nil {
   302  		return ctx, err
   303  	}
   304  
   305  	xPos := ctx.X
   306  	yPos := ctx.PageHeight - ctx.Y - img.Height()
   307  	angle := img.angle
   308  
   309  	// Create content stream to add to the Page contents.
   310  	contentCreator := contentstream.NewContentCreator()
   311  
   312  	contentCreator.Add_gs(gsName) // Set graphics state.
   313  
   314  	contentCreator.Translate(xPos, yPos)
   315  	if angle != 0 {
   316  		// Make the rotation about the upper left corner.
   317  		contentCreator.Translate(0, img.Height())
   318  		contentCreator.RotateDeg(angle)
   319  		contentCreator.Translate(0, -img.Height())
   320  	}
   321  
   322  	contentCreator.
   323  		Scale(img.Width(), img.Height()).
   324  		Add_Do(imgName) // Draw the image.
   325  
   326  	ops := contentCreator.Operations()
   327  	ops.WrapIfNeeded()
   328  
   329  	blk.addContents(ops)
   330  
   331  	if img.positioning.isRelative() {
   332  		ctx.Y += img.Height()
   333  		ctx.Height -= img.Height()
   334  		return ctx, nil
   335  	}
   336  	// Absolute positioning - return original context.
   337  	return origCtx, nil
   338  }