watermark.go 4.5 KB

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