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 }