refactor(watermark): small text based watermarked to increase document visibility

This commit is contained in:
Colin Darie 2023-09-07 18:10:26 +02:00
parent 2f5310a470
commit 201b31bf36
No known key found for this signature in database
GPG key ID: 8C76CADD40253590
7 changed files with 172 additions and 66 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -1 +0,0 @@
WATERMARK_FILE = ENV.fetch('WATERMARK_FILE', 'watermark.png')

View file

@ -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

View file

@ -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