watermark.go 4.5 KB

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