watermark.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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(img *vips.Image, watermark auximageprovider.Provider, opts *options.WatermarkOptions, offsetScale float64, framesCount int) error {
  69. if watermark == nil {
  70. return nil
  71. }
  72. wmData, _, err := watermark.Get(context.Background(), nil)
  73. if err != nil {
  74. return err
  75. }
  76. if wmData == nil {
  77. return nil
  78. }
  79. defer wmData.Close()
  80. wm := new(vips.Image)
  81. defer wm.Clear()
  82. width := img.Width()
  83. height := img.Height()
  84. frameHeight := height / framesCount
  85. if err := prepareWatermark(wm, wmData, opts, width, frameHeight, offsetScale, framesCount); err != nil {
  86. return err
  87. }
  88. if !img.ColourProfileImported() {
  89. if err := img.ImportColourProfile(); err != nil {
  90. return err
  91. }
  92. }
  93. if err := img.RgbColourspace(); err != nil {
  94. return err
  95. }
  96. opacity := opts.Opacity * config.WatermarkOpacity
  97. // If we replicated the watermark and need to apply it to an animated image,
  98. // it is faster to replicate the watermark to all the image and apply it single-pass
  99. if opts.ShouldReplicate() && framesCount > 1 {
  100. if err := wm.Replicate(width, height, false); err != nil {
  101. return err
  102. }
  103. return img.ApplyWatermark(wm, 0, 0, opacity)
  104. }
  105. left, top := 0, 0
  106. wmWidth := wm.Width()
  107. wmHeight := wm.Height()
  108. if !opts.ShouldReplicate() {
  109. left, top = calcPosition(width, frameHeight, wmWidth, wmHeight, &opts.Position, offsetScale, true)
  110. }
  111. if left >= width || top >= height || -left >= wmWidth || -top >= wmHeight {
  112. // Watermark is completely outside the image
  113. return nil
  114. }
  115. // if watermark is partially outside the image, it may partially be visible
  116. // on the next frame. We need to crop it vertically.
  117. // We don't care about horizontal overlap, as frames are stacked vertically
  118. if framesCount > 1 {
  119. cropTop := 0
  120. cropHeight := wmHeight
  121. if top < 0 {
  122. cropTop = -top
  123. cropHeight -= cropTop
  124. top = 0
  125. }
  126. if top+cropHeight > frameHeight {
  127. cropHeight = frameHeight - top
  128. }
  129. if cropTop > 0 || cropHeight < wmHeight {
  130. if err := wm.Crop(0, cropTop, wmWidth, cropHeight); err != nil {
  131. return err
  132. }
  133. }
  134. }
  135. for i := 0; i < framesCount; i++ {
  136. if err := img.ApplyWatermark(wm, left, top, opacity); err != nil {
  137. return err
  138. }
  139. top += frameHeight
  140. }
  141. return nil
  142. }
  143. func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
  144. if !po.Watermark.Enabled || pctx.watermarkProvider == nil {
  145. return nil
  146. }
  147. return applyWatermark(img, pctx.watermarkProvider, &po.Watermark, pctx.dprScale, 1)
  148. }