1
0

watermark.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package processing
  2. import (
  3. "context"
  4. "math"
  5. "github.com/imgproxy/imgproxy/v3/config"
  6. "github.com/imgproxy/imgproxy/v3/imagedata"
  7. "github.com/imgproxy/imgproxy/v3/imath"
  8. "github.com/imgproxy/imgproxy/v3/options"
  9. "github.com/imgproxy/imgproxy/v3/vips"
  10. )
  11. // watermarkPipeline constructs the watermark processing pipeline.
  12. // This pipeline is applied to the watermark image.
  13. func (p *Processor) watermarkPipeline() Pipeline {
  14. return Pipeline{
  15. p.vectorGuardScale,
  16. p.scaleOnLoad,
  17. p.colorspaceToProcessing,
  18. p.scale,
  19. p.rotateAndFlip,
  20. p.padding,
  21. }
  22. }
  23. func (p *Processor) prepareWatermark(
  24. ctx context.Context,
  25. wm *vips.Image,
  26. wmData imagedata.ImageData,
  27. po *options.ProcessingOptions,
  28. imgWidth, imgHeight int,
  29. offsetScale float64,
  30. framesCount int,
  31. ) error {
  32. if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
  33. return err
  34. }
  35. opts := po.Watermark
  36. wmPo := po.Default()
  37. wmPo.ResizingType = options.ResizeFit
  38. wmPo.Dpr = 1
  39. wmPo.Enlarge = true
  40. wmPo.Format = wmData.Format()
  41. if opts.Scale > 0 {
  42. wmPo.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
  43. wmPo.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
  44. }
  45. if opts.ShouldReplicate() {
  46. var offX, offY int
  47. if math.Abs(opts.Position.X) >= 1.0 {
  48. offX = imath.RoundToEven(opts.Position.X * offsetScale)
  49. } else {
  50. offX = imath.ScaleToEven(imgWidth, opts.Position.X)
  51. }
  52. if math.Abs(opts.Position.Y) >= 1.0 {
  53. offY = imath.RoundToEven(opts.Position.Y * offsetScale)
  54. } else {
  55. offY = imath.ScaleToEven(imgHeight, opts.Position.Y)
  56. }
  57. wmPo.Padding.Enabled = true
  58. wmPo.Padding.Left = offX / 2
  59. wmPo.Padding.Right = offX - wmPo.Padding.Left
  60. wmPo.Padding.Top = offY / 2
  61. wmPo.Padding.Bottom = offY - wmPo.Padding.Top
  62. }
  63. if err := p.watermarkPipeline().Run(ctx, wm, wmPo, wmData); err != nil {
  64. return err
  65. }
  66. // We need to copy the image to ensure that it is in memory since we will
  67. // close it after watermark processing is done.
  68. if err := wm.CopyMemory(); err != nil {
  69. return err
  70. }
  71. if opts.ShouldReplicate() {
  72. if err := wm.Replicate(imgWidth, imgHeight, true); err != nil {
  73. return err
  74. }
  75. }
  76. // We don't want any headers to be copied from the watermark to the image
  77. return wm.StripAll()
  78. }
  79. func (p *Processor) applyWatermark(
  80. ctx context.Context,
  81. img *vips.Image,
  82. po *options.ProcessingOptions,
  83. offsetScale float64,
  84. framesCount int,
  85. ) error {
  86. if p.watermarkProvider == nil {
  87. return nil
  88. }
  89. wmData, _, err := p.watermarkProvider.Get(ctx, po)
  90. if err != nil {
  91. return err
  92. }
  93. if wmData == nil {
  94. return nil
  95. }
  96. defer wmData.Close()
  97. opts := po.Watermark
  98. wm := new(vips.Image)
  99. defer wm.Clear()
  100. width := img.Width()
  101. height := img.Height()
  102. frameHeight := height / framesCount
  103. if err := p.prepareWatermark(ctx, wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
  104. return err
  105. }
  106. if !img.ColourProfileImported() {
  107. if err := img.ImportColourProfile(); err != nil {
  108. return err
  109. }
  110. }
  111. if err := img.RgbColourspace(); err != nil {
  112. return err
  113. }
  114. // TODO: Use runner config
  115. opacity := opts.Opacity * config.WatermarkOpacity
  116. // If we replicated the watermark and need to apply it to an animated image,
  117. // it is faster to replicate the watermark to all the image and apply it single-pass
  118. if opts.ShouldReplicate() && framesCount > 1 {
  119. if err := wm.Replicate(width, height, false); err != nil {
  120. return err
  121. }
  122. return img.ApplyWatermark(wm, 0, 0, opacity)
  123. }
  124. left, top := 0, 0
  125. wmWidth := wm.Width()
  126. wmHeight := wm.Height()
  127. if !opts.ShouldReplicate() {
  128. left, top = calcPosition(width, frameHeight, wmWidth, wmHeight, &opts.Position, offsetScale, true)
  129. }
  130. if left >= width || top >= height || -left >= wmWidth || -top >= wmHeight {
  131. // Watermark is completely outside the image
  132. return nil
  133. }
  134. // if watermark is partially outside the image, it may partially be visible
  135. // on the next frame. We need to crop it vertically.
  136. // We don't care about horizontal overlap, as frames are stacked vertically
  137. if framesCount > 1 {
  138. cropTop := 0
  139. cropHeight := wmHeight
  140. if top < 0 {
  141. cropTop = -top
  142. cropHeight -= cropTop
  143. top = 0
  144. }
  145. if top+cropHeight > frameHeight {
  146. cropHeight = frameHeight - top
  147. }
  148. if cropTop > 0 || cropHeight < wmHeight {
  149. if err := wm.Crop(0, cropTop, wmWidth, cropHeight); err != nil {
  150. return err
  151. }
  152. }
  153. }
  154. for i := 0; i < framesCount; i++ {
  155. if err := img.ApplyWatermark(wm, left, top, opacity); err != nil {
  156. return err
  157. }
  158. top += frameHeight
  159. }
  160. return nil
  161. }
  162. func (p *Processor) watermark(c *Context) error {
  163. if !c.PO.Watermark.Enabled || c.WatermarkProvider == nil {
  164. return nil
  165. }
  166. return p.applyWatermark(c.Ctx, c.Img, c.PO, c.DprScale, 1)
  167. }