refactor(watermark): small text based watermarked to increase document visibility
This commit is contained in:
parent
2f5310a470
commit
201b31bf36
7 changed files with 172 additions and 66 deletions
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).
|
||||
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
|
||||
|
|
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
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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) } }
|
||||
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
|
||||
|
|
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