class WatermarkService POINTSIZE = 20 KERNING = 1.2 ANGLE = 45 FILL_COLOR = "rgba(0,0,0,0.4)" attr_reader :text attr_reader :text_length def initialize(text = APPLICATION_NAME) @text = " #{text} " # give more space around each occurence @text_length = @text.length end def process(file, output) metadata = image_metadata(file) return if metadata.blank? watermark_image(file, output, metadata) output end private def watermark_image(file, output, metadata) MiniMagick::Tool::Convert.new do |convert| setup_conversion_commands(convert, file) apply_watermark(convert, metadata) convert << output.to_path end end def setup_conversion_commands(convert, file) convert << file.to_path convert << "-pointsize" convert << POINTSIZE convert << "-kerning" convert << KERNING convert << "-fill" convert << FILL_COLOR convert << "-gravity" convert << "northwest" end # Parcourt l'image ligne par ligne et colonne par colonne en y apposant un filigrane # en alternant un décalage horizontal sur chaque ligne def apply_watermark(convert, metadata) stride_x, stride_y, initial_offsets_x, initial_offset_y = calculate_watermark_params 0.step(by: stride_y, to: metadata[:height] + stride_y * 2).with_index do |offset_y, index| initial_offset_x = initial_offsets_x[index % 2] 0.step(by: stride_x, to: metadata[:width] + stride_x * 2) do |offset_x| x = initial_offset_x + offset_x y = initial_offset_y + offset_y draw_text(convert, x, y) end end end def calculate_watermark_params # Approximation de la longueur du texte, qui marche bien pour les constantes par défaut char_width_approx = POINTSIZE / 2 char_height_approx = POINTSIZE * 3 / 4 # Dimensions du rectangle de texte text_width_approx = char_width_approx * text_length * Math.cos(ANGLE * (Math::PI / 180)).abs text_height_approx = char_width_approx * text_length * Math.sin(ANGLE * (Math::PI / 180)).abs + char_height_approx diagonal_length = Math.sqrt(text_width_approx**2 + text_height_approx**2) # Calcul des décalages entre chaque colonne et ligne # afin que chaque occurence "suive" la précédente stride_x = ((diagonal_length + char_width_approx) / Math.cos(ANGLE * (Math::PI / 180))) stride_y = text_height_approx initial_offsets_x = [0, (0 - stride_x / 2).round] # Motif de damier en alternant le décalage horizontal initial_offset_y = 0 - stride_y # Offset négatif pour mieux couvrir le nord ouest [stride_x.round, stride_y.round, initial_offsets_x, initial_offset_y.round] end def draw_text(convert, x, y) # A chaque insertion de texte, positionne le curseur, définit la rotation, puis réinitialise ces paramètres pour la prochaine occurence # Note: x and y can be negative value convert << "-draw" convert << "translate #{x},#{y} rotate #{-ANGLE} text 0,0 '#{text}' rotate #{ANGLE} translate #{-x},#{-y}" end def image_metadata(file) read_image(file) do |image| width = image.width height = image.height if rotated_image?(image) width, height = height, width end { width: width, height: height } end end def read_image(file) image = MiniMagick::Image.new(file.to_path) if image.valid? yield image else Rails.logger.info "Skipping image analysis because ImageMagick doesn't support the file #{file}" nil end end def rotated_image?(image) ['RightTop', 'LeftBottom'].include?(image["%[orientation]"]) end end