Commit aec715d3 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Lin Jen-Shin

Add utilities to auto-crop screenshots

- module utility
- test case
parent c65ced47
......@@ -750,3 +750,64 @@ code review. For docs changes in merge requests, whenever a change to files unde
is made, Danger Bot leaves a comment with further instructions about the documentation
process. This is configured in the `Dangerfile` in the GitLab repository under
[/danger/documentation/](https://gitlab.com/gitlab-org/gitlab/tree/master/danger/documentation).
## Automatic screenshot generator
You can now set up an automatic screenshot generator to take and compress screenshots, with the
help of a configuration file known as **screenshot generator**.
### Use the tool
To run the tool on an existing screenshot generator, take the following steps:
1. Set up the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/gitlab_docs.md).
1. Navigate to the subdirectory with your cloned GitLab repository, typically `gdk/gitlab`.
1. Make sure that your GDK database is fully migrated: `bin/rake db:migrate RAILS_ENV=development`.
1. Install pngquant, see the tool website for more info: [`pngquant`](https://pngquant.org/)
1. Run `scripts/docs_screenshots.rb spec/docs_screenshots/<name_of_screenshot_generator>.rb <milestone-version>`.
1. Identify the location of the screenshots, based on the `gitlab/doc` location defined by the `it` parameter in your script.
1. Commit the newly created screenshots.
### Extending the tool
To add an additional **screenshot generator**, take the following steps:
- Locate the `spec/docs_screenshots` directory.
- Add a new file with a `_docs.rb` extension.
- Be sure to include the following bits in the file:
```ruby
require 'spec_helper'
RSpec.describe '<What I am taking screenshots of>', :js do
include DocsScreenshotHelpers # Helper that enables the screenshots taking mechanism
before do
page.driver.browser.manage.window.resize_to(1366, 1024) # length and width of the page
end
```
- In addition, every `it` block must include the path where the screenshot is saved
```ruby
it 'user/packages/container_registry/img/project_image_repositories_list'
```
#### Full page screenshots
To take a full page screenshot simply `visit the page` and perform any expectation on real content (to have capybara wait till the page is ready and not take a white screenshot).
#### Element screenshot
To have the screenshot focuses few more steps are needed:
- **find the area**: `screenshot_area = find('#js-registry-policies')`
- **scroll the area in focus**: `scroll_to screenshot_area`
- **wait for the content**: `expect(screenshot_area).to have_content 'Expiration interval'`
- **set the crop area**: `set_crop_data(screenshot_area, 20)`
In particular `set_crop_data` accepts as arguments: a `DOM` element and a padding, the padding will be added around the element enlarging the screenshot area.
#### Live example
Please use `spec/docs_screenshots/container_registry_docs.rb` as a guide and as an example to create your own scripts.
......@@ -2,10 +2,10 @@ return if Rails.env.production?
require 'png_quantizator'
require 'parallel'
require_relative '../../tooling/lib/tooling/images'
# The amount of variance (in bytes) allowed in
# file size when testing for compression size
TOLERANCE = 10000
namespace :pngquant do
# Returns an array of all images eligible for compression
......@@ -13,55 +13,13 @@ namespace :pngquant do
Dir.glob('doc/**/*.png', File::FNM_CASEFOLD)
end
# Runs pngquant on an image and optionally
# writes the result to disk
def compress_image(file, overwrite_original)
compressed_file = "#{file}.compressed"
FileUtils.copy(file, compressed_file)
pngquant_file = PngQuantizator::Image.new(compressed_file)
# Run the image repeatedly through pngquant until
# the change in file size is within TOLERANCE
loop do
before = File.size(compressed_file)
pngquant_file.quantize!
after = File.size(compressed_file)
break if before - after <= TOLERANCE
end
savings = File.size(file) - File.size(compressed_file)
is_uncompressed = savings > TOLERANCE
if is_uncompressed && overwrite_original
FileUtils.copy(compressed_file, file)
end
FileUtils.remove(compressed_file)
[is_uncompressed, savings]
end
# Ensures pngquant is available and prints an error if not
def check_executable
unless system('pngquant --version', out: File::NULL)
warn(
'Error: pngquant executable was not detected in the system.'.color(:red),
'Download pngquant at https://pngquant.org/ and place the executable in /usr/local/bin'.color(:green)
)
abort
end
end
desc 'GitLab | Pngquant | Compress all documentation PNG images using pngquant'
task :compress do
check_executable
files = doc_images
puts "Compressing #{files.size} PNG files in doc/**"
Parallel.each(files) do |file|
was_uncompressed, savings = compress_image(file, true)
was_uncompressed, savings = Tooling::Image.compress_image(file)
if was_uncompressed
puts "#{file} was reduced by #{savings} bytes"
......@@ -71,13 +29,11 @@ namespace :pngquant do
desc 'GitLab | Pngquant | Checks that all documentation PNG images have been compressed with pngquant'
task :lint do
check_executable
files = doc_images
puts "Checking #{files.size} PNG files in doc/**"
uncompressed_files = Parallel.map(files) do |file|
is_uncompressed, _ = compress_image(file, false)
is_uncompressed, _ = Tooling::Image.compress_image(file, true)
if is_uncompressed
puts "Uncompressed file detected: ".color(:red) + file
file
......
......@@ -21,7 +21,7 @@ module RuboCop
private
def acceptable_file_path?(path)
File.fnmatch?('*_spec.rb', path) || File.fnmatch?('*/frontend/fixtures/*', path)
File.fnmatch?('*_spec.rb', path) || File.fnmatch?('*/frontend/fixtures/*', path) || File.fnmatch?('*/docs_screenshots/*_docs.rb', path)
end
def shared_example?(node)
......
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'png_quantizator'
require 'open3'
require 'parallel'
require_relative '../tooling/lib/tooling/images.rb'
generator = ARGV[0]
milestone = ARGV[1]
unless generator
warn('Error: missing generator, please supply one')
abort
end
unless milestone
warn('Error: missing milestone, please supply one')
abort
end
def rename_image(file, milestone)
path = File.dirname(file)
basename = File.basename(file)
final_name = File.join(path, "#{basename}_v#{milestone}.png")
FileUtils.mv(file, final_name)
end
system('spring', 'rspec', generator)
files = []
Open3.popen3("git diff --name-only -- '*.png'") do |stdin, stdout, stderr, thread|
files.concat stdout.read.chomp.split("\n")
end
Open3.popen3("git status --porcelain -- '*.png'") do |stdin, stdout, stderr, thread|
files.concat stdout.read.chomp.split("?? ")
end
files.reject!(&:empty?)
if files.empty?
puts "No file generated, did you select the right screenshot generator?"
else
puts "Compressing newly generated screenshots"
Parallel.each(files) do |file|
file_path = File.join(Dir.pwd, file.to_s.strip)
was_uncompressed, savings = Tooling::Image.compress_image(file_path)
rename_image(file_path, milestone)
if was_uncompressed
puts "#{file} was reduced by #{savings} bytes."
else
puts "Skipping already compressed file: #{file}."
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Container Registry', :js do
include DocsScreenshotHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
before do
page.driver.browser.manage.window.resize_to(1366, 1024)
group.add_owner(user)
sign_in(user)
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: [])
end
context 'expiration policy settings' do
it 'user/packages/container_registry/img/expiration_policy_form' do
visit project_settings_ci_cd_path(project)
screenshot_area = find('#js-registry-policies')
scroll_to screenshot_area
expect(screenshot_area).to have_content 'Expiration interval'
set_crop_data(screenshot_area, 20)
end
end
context 'project container_registry' do
it 'user/packages/container_registry/img/project_empty_page' do
visit_project_container_registry
expect(page).to have_content _('There are no container images stored for this project')
end
context 'with a list of repositories' do
before do
stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest], with_manifest: true)
create_list(:container_repository, 12, project: project)
end
it 'user/packages/container_registry/img/project_image_repositories_list' do
visit_project_container_registry
expect(page).to have_content 'Image Repositories'
end
it 'user/packages/container_registry/img/project_image_repositories_list_with_commands_open' do
visit_project_container_registry
click_on 'CLI Commands'
end
end
end
context 'group container_registry' do
it 'user/packages/container_registry/img/group_empty_page' do
visit_group_container_registry
expect(page).to have_content 'There are no container images available in this group'
end
context 'with a list of repositories' do
before do
stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest], with_manifest: true)
create_list(:container_repository, 12, project: project)
end
it 'user/packages/container_registry/img/group_image_repositories_list' do
visit_group_container_registry
expect(page).to have_content 'Image Repositories'
end
end
end
def visit_project_container_registry
visit project_container_registry_index_path(project)
end
def visit_group_container_registry
visit group_container_registries_path(group)
end
end
......@@ -100,6 +100,10 @@ RSpec.configure do |config|
metadata[:enable_admin_mode] = true if location =~ %r{(ee)?/spec/controllers/admin/}
end
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_docs\.rb\z}) do |metadata|
metadata[:type] = :feature
end
config.include LicenseHelpers
config.include ActiveJob::TestHelper
config.include ActiveSupport::Testing::TimeHelpers
......
# frozen_string_literal: true
require 'fileutils'
require 'mini_magick'
module DocsScreenshotHelpers
extend ActiveSupport::Concern
def set_crop_data(element, padding)
@crop_element = element
@crop_padding = padding
end
def crop_image_screenshot(path)
element_rect = @crop_element.evaluate_script("this.getBoundingClientRect()")
width = element_rect['width'] + (@crop_padding * 2)
height = element_rect['height'] + (@crop_padding * 2)
x = element_rect['x'] - @crop_padding
y = element_rect['y'] - @crop_padding
image = MiniMagick::Image.new(path)
image.crop "#{width}x#{height}+#{x}+#{y}"
end
included do |base|
after do |example|
filename = "#{example.description}.png"
path = File.expand_path(filename, 'doc/')
page.save_screenshot(path)
if @crop_element
crop_image_screenshot(path)
set_crop_data(nil, nil)
end
end
end
end
# frozen_string_literal: true
module Tooling
module Image
# Determine the tolerance till when we run pngquant in a loop
TOLERANCE = 10000
def self.check_executables
unless system('pngquant --version', out: File::NULL)
warn(
'Error: pngquant executable was not detected in the system.',
'Download pngquant at https://pngquant.org/ and place the executable in /usr/local/bin'
)
abort
end
unless system('gm version', out: File::NULL)
warn(
'Error: gm executable was not detected in the system.',
'Please install imagemagick: brew install imagemagick or sudo apt install imagemagick'
)
abort
end
end
def self.compress_image(file, keep_original = false)
check_executables
compressed_file = "#{file}.compressed"
FileUtils.copy(file, compressed_file)
pngquant_file = PngQuantizator::Image.new(compressed_file)
# Run the image repeatedly through pngquant until
# the change in file size is within TOLERANCE
# or the loop count is above 1000
1000.times do
before = File.size(compressed_file)
pngquant_file.quantize!
after = File.size(compressed_file)
break if before - after <= TOLERANCE
end
savings = File.size(file) - File.size(compressed_file)
is_uncompressed = savings > TOLERANCE
if is_uncompressed && !keep_original
FileUtils.copy(compressed_file, file)
end
FileUtils.remove(compressed_file)
[is_uncompressed, savings]
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment