diff --git a/app/assets/images/watermark.png b/app/assets/images/watermark.png deleted file mode 100644 index 07114bdcf..000000000 Binary files a/app/assets/images/watermark.png and /dev/null differ diff --git a/app/jobs/titre_identite_watermark_job.rb b/app/jobs/titre_identite_watermark_job.rb index 580dc204d..491530d05 100644 --- a/app/jobs/titre_identite_watermark_job.rb +++ b/app/jobs/titre_identite_watermark_job.rb @@ -10,77 +10,18 @@ class TitreIdentiteWatermarkJob < ApplicationJob # (to avoid modifying the file while it is being scanned). retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 - MAX_IMAGE_SIZE = 1500 - SCALE = 0.9 - WATERMARK = URI.parse(WATERMARK_FILE).is_a?(URI::HTTP) ? WATERMARK_FILE : Rails.root.join("app/assets/images/#{WATERMARK_FILE}") - def perform(blob) return if blob.watermark_done? raise FileNotScannedYetError if blob.virus_scanner.pending? blob.open do |file| - watermark = resize_watermark(file) - - if watermark.present? - processed = watermark_image(file, watermark) + Tempfile.create(["watermarked", File.extname(file)]) do |output| + processed = WatermarkService.new.process(file, output) + return if processed.blank? blob.upload(processed) blob.touch(:watermarked_at) end end end - - private - - def watermark_image(file, watermark) - ImageProcessing::MiniMagick - .source(file) - .convert("png") - .resize_to_limit(MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) - .composite(watermark, mode: "over", gravity: "center") - .call - end - - def resize_watermark(file) - metadata = image_metadata(file) - - if metadata[:width].present? && metadata[:height].present? - width = [metadata[:width], MAX_IMAGE_SIZE].min * SCALE - height = [metadata[:height], MAX_IMAGE_SIZE].min * SCALE - diagonal = Math.sqrt(height**2 + width**2) - angle = Math.asin(height / diagonal) * 180 / Math::PI - - ImageProcessing::MiniMagick - .source(WATERMARK) - .resize_to_limit(diagonal, diagonal / 2) - .rotate(-angle, background: :transparent) - .call - end - end - - def image_metadata(file) - read_image(file) do |image| - if rotated_image?(image) - { width: image.height, height: image.width } - else - { width: image.width, height: image.height } - end - end - end - - def read_image(file) - require "mini_magick" - image = MiniMagick::Image.new(file.path) - - if image.valid? - yield image - else - logger.info "Skipping image analysis because ImageMagick doesn't support the file" - {} - end - end - - def rotated_image?(image) - ['RightTop', 'LeftBottom'].include?(image["%[orientation]"]) - end end diff --git a/app/services/watermark_service.rb b/app/services/watermark_service.rb new file mode 100644 index 000000000..1f192f879 --- /dev/null +++ b/app/services/watermark_service.rb @@ -0,0 +1,118 @@ +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 diff --git a/config/env.example.optional b/config/env.example.optional index 8935825e9..969c105d3 100644 --- a/config/env.example.optional +++ b/config/env.example.optional @@ -68,9 +68,6 @@ DS_ENV="staging" # Instance customization: PDF export logo ---> to be put in "app/assets/images" # DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.png" -# Instance customization: watermark for identity documents -# WATERMARK_FILE="" - # Enabling maintenance mode # MAINTENANCE_MODE="true" diff --git a/config/initializers/watermark.rb b/config/initializers/watermark.rb deleted file mode 100644 index 8c87c9cda..000000000 --- a/config/initializers/watermark.rb +++ /dev/null @@ -1 +0,0 @@ -WATERMARK_FILE = ENV.fetch('WATERMARK_FILE', 'watermark.png') diff --git a/lib/tasks/pjs.rake b/lib/tasks/pjs.rake index 6b52198fc..73d952e3e 100644 --- a/lib/tasks/pjs.rake +++ b/lib/tasks/pjs.rake @@ -22,4 +22,32 @@ namespace :pjs do blobs.in_batches { |batch| batch.ids.each { |id| PjsMigrationJob.perform_later(id) } } end + + desc "Watermark demo. Usage: noglob rake pjs:watermark_demo[tmp/carte-identite-demo-1.jpg]" + task :watermark_demo, [:file_path] => :environment do |_t, args| + file = Pathname.new(args[:file_path]) + output_file = Rails.root.join('tmp', "#{file.basename(file.extname)}_watermarked#{file.extname}") + + processed = WatermarkService.new.process(file, output_file) + + if processed + rake_puts "Watermarked: #{processed}" + else + rake_puts "File #{file} not watermarked. Read application log for more information" + end + end + + desc "Watermark demo all defined demo files. Usage: noglob rake pjs:watermark_demo_all" + task :watermark_demo_all => :environment do + # You must have these filenames in tmp/ to run this demo (download ID cards specimens) + filenames = [ + "carte-identite-demo-1.jpg", "carte-identite-demo-2.jpg", "carte-identite-demo-3.png", "carte-identite-demo-4.jpg", + "carte-identite-demo-5.jpg", "passeport-1.jpg", "passeport-2.jpg" + ] + + filenames.each do |file| + Rake::Task["pjs:watermark_demo"].invoke("tmp/#{file}") + Rake::Task["pjs:watermark_demo"].reenable + end + end end diff --git a/spec/services/watermark_service_spec.rb b/spec/services/watermark_service_spec.rb new file mode 100644 index 000000000..abe0da8b4 --- /dev/null +++ b/spec/services/watermark_service_spec.rb @@ -0,0 +1,23 @@ +RSpec.describe WatermarkService do + let(:image) { file_fixture("logo_test_procedure.png") } + let(:watermark_service) { WatermarkService.new } + + describe '#process' do + it 'returns a tempfile if watermarking succeeds' do + Tempfile.create do |output| + watermark_service.process(image, output) + # output size should always be a little greater than input size + expect(output.size).to be_between(image.size, image.size * 1.5) + end + end + + it 'returns nil if metadata is blank' do + allow(watermark_service).to receive(:image_metadata).and_return(nil) + + Tempfile.create do |output| + expect(watermark_service.process(image.to_path, output)).to be_nil + expect(output.size).to eq(0) + end + end + end +end