watermark.go 4.5 KB

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