Merge pull request #9470 from colinux/improve-watermark
ETQ instructeur: nouveau filigrane des titres d'identité qui améliore la lisibilité des images
This commit is contained in:
commit
c9bde31ef2
9 changed files with 176 additions and 66 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -75,6 +75,9 @@ jobs:
|
||||||
- name: Setup the app runtime and dependencies
|
- name: Setup the app runtime and dependencies
|
||||||
uses: ./.github/actions/ci-setup-rails
|
uses: ./.github/actions/ci-setup-rails
|
||||||
|
|
||||||
|
- name: Install fonts pickable by ImageMagick
|
||||||
|
run: sudo apt-get install -y gsfonts
|
||||||
|
|
||||||
- name: Pre-compile assets
|
- name: Pre-compile assets
|
||||||
uses: ./.github/actions/ci-setup-assets
|
uses: ./.github/actions/ci-setup-assets
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ Vous souhaitez y apporter des changements ou des améliorations ? Lisez notre [
|
||||||
#### Tous environnements
|
#### Tous environnements
|
||||||
|
|
||||||
- postgresql
|
- postgresql
|
||||||
|
- imagemagick et gsfonts pour générer les filigranes sur les titres d'identité.
|
||||||
|
|
||||||
#### Développement
|
#### Développement
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 9.7 KiB |
|
@ -10,77 +10,18 @@ class TitreIdentiteWatermarkJob < ApplicationJob
|
||||||
# (to avoid modifying the file while it is being scanned).
|
# (to avoid modifying the file while it is being scanned).
|
||||||
retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
|
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)
|
def perform(blob)
|
||||||
return if blob.watermark_done?
|
return if blob.watermark_done?
|
||||||
raise FileNotScannedYetError if blob.virus_scanner.pending?
|
raise FileNotScannedYetError if blob.virus_scanner.pending?
|
||||||
|
|
||||||
blob.open do |file|
|
blob.open do |file|
|
||||||
watermark = resize_watermark(file)
|
Tempfile.create(["watermarked", File.extname(file)]) do |output|
|
||||||
|
processed = WatermarkService.new.process(file, output)
|
||||||
if watermark.present?
|
return if processed.blank?
|
||||||
processed = watermark_image(file, watermark)
|
|
||||||
|
|
||||||
blob.upload(processed)
|
blob.upload(processed)
|
||||||
blob.touch(:watermarked_at)
|
blob.touch(:watermarked_at)
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
118
app/services/watermark_service.rb
Normal file
118
app/services/watermark_service.rb
Normal 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
|
|
@ -71,9 +71,6 @@ DS_ENV="staging"
|
||||||
# Instance customization: PDF export logo ---> to be put in "app/assets/images"
|
# 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"
|
# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.png"
|
||||||
|
|
||||||
# Instance customization: watermark for identity documents
|
|
||||||
# WATERMARK_FILE=""
|
|
||||||
|
|
||||||
# Enabling maintenance mode
|
# Enabling maintenance mode
|
||||||
# MAINTENANCE_MODE="true"
|
# MAINTENANCE_MODE="true"
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
WATERMARK_FILE = ENV.fetch('WATERMARK_FILE', 'watermark.png')
|
|
|
@ -22,4 +22,32 @@ namespace :pjs do
|
||||||
|
|
||||||
blobs.in_batches { |batch| batch.ids.each { |id| PjsMigrationJob.perform_later(id) } }
|
blobs.in_batches { |batch| batch.ids.each { |id| PjsMigrationJob.perform_later(id) } }
|
||||||
end
|
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
|
end
|
||||||
|
|
23
spec/services/watermark_service_spec.rb
Normal file
23
spec/services/watermark_service_spec.rb
Normal 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
|
Loading…
Reference in a new issue