Implemented preview feature as default feature.

This commit is contained in:
Martin Edenhofer 2019-02-14 06:42:41 +01:00
parent 11b0d4d286
commit 5dedd6ecfc
16 changed files with 342 additions and 14 deletions

View file

@ -10,7 +10,7 @@ DB_CONFIG="test:\n adapter: postgresql\n database: zammad_test\n host: 127.0.
# install build dependencies # install build dependencies
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends autoconf automake autotools-dev bison build-essential curl git-core libffi-dev libgdbm-dev libgmp-dev libmariadbclient-dev-compat libncurses5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libxml2-dev libxslt1-dev libyaml-0-2 libyaml-dev patch pkg-config postfix sqlite3 zlib1g-dev sudo apt-get install -y --no-install-recommends autoconf automake autotools-dev bison build-essential curl git-core libffi-dev libgdbm-dev libgmp-dev libmariadbclient-dev-compat libncurses5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libxml2-dev libxslt1-dev libyaml-0-2 libyaml-dev patch pkg-config postfix sqlite3 zlib1g-dev libimlib2 libimlib2-dev
if [ "${CIRCLE_JOB}" == "install-mysql" ]; then if [ "${CIRCLE_JOB}" == "install-mysql" ]; then
DB_ADAPTER="mysql2" DB_ADAPTER="mysql2"

View file

@ -10,36 +10,69 @@ targets:
- nginx - nginx
- postgresql-server - postgresql-server
- which - which
debian-8: - epel-release
dependencies: - imlib2
- curl - imlib2-devel
- elasticsearch build_dependencies:
- nginx|apache2 - http://download.fedoraproject.org/pub/epel/7/x86_64/Packages/i/imlib2-1.4.5-9.el7.x86_64.rpm
- postgresql|mysql-server|mariadb-server|sqlite - http://download.fedoraproject.org/pub/epel/7/x86_64/Packages/i/imlib2-devel-1.4.5-9.el7.x86_64.rpm
debian-9: debian-9:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mariadb-server|sqlite - postgresql|mariadb-server|sqlite
- libimlib2
- libimlib2-dev
build_dependencies:
- libimlib2
- libimlib2-dev
ubuntu-16.04: ubuntu-16.04:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
- libimlib2
- libimlib2-dev
build_dependencies:
- libimlib2
- libimlib2-dev
ubuntu-18.04: ubuntu-18.04:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
- libimlib2
build_dependencies:
- libimlib2-dev
sles-12: sles-12:
dependencies: dependencies:
- curl - curl
- elasticsearch - elasticsearch
- nginx - nginx
- postgresql-server - postgresql-server
- imlib2
- imlib2-devel
build_dependencies:
- imlib2
- imlib2-devel
sles-12:
dependencies:
- curl
- elasticsearch
- nginx
- postgresql-server
- imlib2
- libImlib2-1
- imlib2
- imlib2-devel
build_dependencies:
- https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/imlib2-1.4.5-12.1.1.x86_64.rpm
- https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/imlib2-devel-1.4.5-12.1.1.x86_64.rpm
- https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/imlib2-filters-1.4.5-12.1.1.x86_64.rpm
- https://ftp.gwdg.de/pub/opensuse/discontinued/distribution/12.3/repo/oss/suse/x86_64/libImlib2-1-1.4.5-12.1.1.x86_64.rpm
before: before:
- contrib/packager.io/before.sh - contrib/packager.io/before.sh
after: after:

View file

@ -116,6 +116,9 @@ gem 'autodiscover', git: 'https://github.com/zammad-deps/autodiscover'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm' gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm'
gem 'viewpoint' gem 'viewpoint'
# image processing
gem 'rszr'
# Gems used only for develop/test and not required # Gems used only for develop/test and not required
# in production environments by default. # in production environments by default.
group :development, :test do group :development, :test do

View file

@ -408,6 +408,7 @@ GEM
rspec-mocks (~> 3.8.0) rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.8.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rszr (0.3.2)
rubocop (0.64.0) rubocop (0.64.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
@ -584,6 +585,7 @@ DEPENDENCIES
rb-fsevent rb-fsevent
rchardet (>= 1.8.0) rchardet (>= 1.8.0)
rspec-rails rspec-rails
rszr
rubocop rubocop
rubyntlm! rubyntlm!
sassc-rails sassc-rails

View file

@ -11,9 +11,33 @@ class App.TicketZoomArticleImageView extends App.ControllerModal
'click .js-cancel': 'cancel' 'click .js-cancel': 'cancel'
'click .js-close': 'cancel' 'click .js-close': 'cancel'
constructor: ->
super
@unbindAll()
$(document).bind('keydown.image_preview', 'right', (e) =>
nextElement = @parentElement.closest('.attachment').next('.attachment.attachment--preview')
return if nextElement.length is 0
@close()
nextElement.find('img').click()
)
$(document).bind('keydown.image_preview', 'left', (e) =>
prevElement = @parentElement.closest('.attachment').prev('.attachment.attachment--preview')
return if prevElement.length is 0
@close()
prevElement.find('img').click()
)
content: -> content: ->
@image = @image.replace(/view=preview/, 'view=inline')
"<div class=\"centered imagePreview\">#{@image}</div>" "<div class=\"centered imagePreview\">#{@image}</div>"
onSubmit: => onSubmit: =>
@image = @image.replace(/(\?|)view=(preview|inline)/, '')
url = "#{$(@image).attr('src')}?disposition=attachment" url = "#{$(@image).attr('src')}?disposition=attachment"
window.open(url, '_blank') window.open(url, '_blank')
onClose: =>
@unbindAll()
unbindAll: ->
$(document).unbind('keydown.image_preview')

View file

@ -421,4 +421,4 @@ class ArticleViewItem extends App.ObserverController
imageView: (e) -> imageView: (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML) new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget))

View file

@ -66,7 +66,7 @@
<div class="attachment-icon"> <div class="attachment-icon">
<% if attachment.preferences && attachment.preferences['Content-Type'] && @ContentTypeIcon(attachment.preferences['Content-Type']): %> <% if attachment.preferences && attachment.preferences['Content-Type'] && @ContentTypeIcon(attachment.preferences['Content-Type']): %>
<% if @canPreview(attachment.preferences['Content-Type']): %> <% if @canPreview(attachment.preferences['Content-Type']): %>
<img src="<%= App.Config.get('api_path') %>/ticket_attachment/<%= @article.ticket_id %>/<%= @article.id %>/<%= attachment.id %>"> <img src="<%= App.Config.get('api_path') %>/ticket_attachment/<%= @article.ticket_id %>/<%= @article.id %>/<%= attachment.id %>?view=preview">
<% else: %> <% else: %>
<%- @Icon( @ContentTypeIcon(attachment.preferences['Content-Type']) ) %> <%- @Icon( @ContentTypeIcon(attachment.preferences['Content-Type']) ) %>
<% end %> <% end %>

View file

@ -255,8 +255,21 @@ class TicketArticlesController < ApplicationController
disposition = sanitized_disposition disposition = sanitized_disposition
content = nil
if params[:view].present? && file.preferences[:resizable] == true
if file.preferences[:content_inline] == true && params[:view] == 'inline'
content = file.content_inline
elsif file.preferences[:content_preview] == true && params[:view] == 'preview'
content = file.content_preview
end
end
if content.blank?
content = file.content
end
send_data( send_data(
file.content, content,
filename: file.filename, filename: file.filename,
type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream', type: file.preferences['Content-Type'] || file.preferences['Mime-Type'] || 'application/octet-stream',
disposition: disposition disposition: disposition

View file

@ -34,7 +34,7 @@ returns
=end =end
def self.add(data) def self.add(data)
data = data.stringify_keys data.deep_stringify_keys!
# lookup store_object.id # lookup store_object.id
store_object = Store::Object.create_if_not_exists(name: data['object']) store_object = Store::Object.create_if_not_exists(name: data['object'])
@ -50,9 +50,34 @@ returns
data.delete('data') data.delete('data')
data.delete('object') data.delete('object')
data['preferences'] ||= {}
['Mime-Type', 'Content-Type', 'mime_type', 'content_type'].each do |key|
next if data['preferences'][key].blank?
next if !data['preferences'][key].match(%r{image/(jpeg|jpg|png)}i)
data['preferences']['resizable'] = true
break
end
# store meta data # store meta data
store = Store.create!(data) store = Store.create!(data)
begin
if store.preferences[:resizable] == true
if store.content_preview(silence: true)
store.preferences[:content_preview] = true
end
if store.content_inline(silence: true)
store.preferences[:content_inline] = true
end
store.save!
end
rescue => e
logger.error e
store.preferences[:resizable] = false
store.save!
end
store store
end end
@ -165,6 +190,52 @@ returns
=begin =begin
get content of file in preview size
store = Store.find(store_id)
content_as_string = store.content_preview
returns
content_as_string
=end
def content_preview(options = {})
file = Store::File.find_by(id: store_file_id)
if !file
raise "No such file #{store_file_id}!"
end
raise 'Unable to generate preview' if options[:silence] != true && preferences[:content_preview] != true
image_resize(file.content, 200)
end
=begin
get content of file in inline size
store = Store.find(store_id)
content_as_string = store.content_inline
returns
content_as_string
=end
def content_inline(options = {})
file = Store::File.find_by(id: store_file_id)
if !file
raise "No such file #{store_file_id}!"
end
raise 'Unable to generate inline' if options[:silence] != true && preferences[:content_inline] != true
image_resize(file.content, 1800)
end
=begin
get content of file get content of file
store = Store.find(store_id) store = Store.find(store_id)
@ -204,4 +275,32 @@ returns
file.provider file.provider
end end
private
def image_resize(content, width)
local_sha = Digest::SHA256.hexdigest(content)
cache_key = "image-resize-#{local_sha}_#{width}"
all = nil
image = Cache.get(cache_key)
return image if image
temp_file = ::Tempfile.new
temp_file.binmode
temp_file.write(content)
temp_file.close
image = Rszr::Image.load(temp_file.path)
return if image.width < width
image.resize!(width, :auto)
temp_file_resize = ::Tempfile.new.path
image.save(temp_file_resize)
image_resized = ::File.binread(temp_file_resize)
Cache.write(cache_key, image_resized, { expires_in: 6.months })
image_resized
end
end end

View file

@ -82,7 +82,7 @@ returns
article['attachments'].each do |file| article['attachments'].each do |file|
next if !file[:preferences] || !file[:preferences]['Content-ID'] || (file[:preferences]['Content-ID'] != cid && file[:preferences]['Content-ID'] != "<#{cid}>" ) next if !file[:preferences] || !file[:preferences]['Content-ID'] || (file[:preferences]['Content-ID'] != cid && file[:preferences]['Content-ID'] != "<#{cid}>" )
replace = "#{tag_start}/api/v1/ticket_attachment/#{article['ticket_id']}/#{article['id']}/#{file[:id]}\"#{tag_end}>" replace = "#{tag_start}/api/v1/ticket_attachment/#{article['ticket_id']}/#{article['id']}/#{file[:id]}?view=inline\"#{tag_end}>"
inline_attachments[file[:id]] = true inline_attachments[file[:id]] = true
break break
end end

View file

@ -0,0 +1,34 @@
class SettingChangeTicketZoomAttachmentPreview < ActiveRecord::Migration[5.1]
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
Setting.create_or_update(
title: 'Sidebar Attachments',
name: 'ui_ticket_zoom_attachments_preview',
area: 'UI::TicketZoom::Preview',
description: 'Enables preview of attachments.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_attachments_preview',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: {
prio: 400,
permission: ['admin.ui'],
},
frontend: true
)
end
end

View file

@ -793,7 +793,7 @@ Setting.create_if_not_exists(
}, },
], ],
}, },
state: false, state: true,
preferences: { preferences: {
prio: 400, prio: 400,
permission: ['admin.ui'], permission: ['admin.ui'],

View file

@ -501,7 +501,7 @@ do(window) ->
stopPropagation: (event) -> stopPropagation: (event) ->
event.stopPropagation() event.stopPropagation()
onPaste: (e) => onDrop: (e) =>
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
test/data/image/1x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

View file

@ -151,4 +151,124 @@ class StoreTest < ActiveSupport::TestCase
assert_not(attachments[0]) assert_not(attachments[0])
end end
end end
test 'test resizable' do
# not possible
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'upload', 'upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'text/plain',
content_id: 234,
},
created_by_id: 1,
)
assert_not(store.preferences.key?(:resizable))
assert_not(store.preferences.key?(:content_inline))
assert_not(store.preferences.key?(:content_preview))
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# not possible
store = Store.add(
object: 'SomeObject2',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'upload', 'upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], false)
assert_not(store.preferences.key?(:content_inline))
assert_not(store.preferences.key?(:content_preview))
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# possible (preview and inline)
store = Store.add(
object: 'SomeObject3',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'upload', 'upload2.jpg')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_equal(store.preferences[:content_inline], true)
assert_equal(store.preferences[:content_preview], true)
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_inline)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 1800)
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_preview)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 200)
# possible (preview only)
store = Store.add(
object: 'SomeObject4',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'image', '1000x1000.png')),
filename: 'test1.png',
preferences: {
content_type: 'image/png',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_nil(store.preferences[:content_inline])
assert_equal(store.preferences[:content_preview], true)
assert_raises(RuntimeError) do
store.content_inline
end
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_preview)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 200)
# possible (now preview or inline needed)
store = Store.add(
object: 'SomeObject5',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test', 'data', 'image', '1x1.png')),
filename: 'test1.png',
preferences: {
content_type: 'image/png',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_nil(store.preferences[:content_inline])
assert_nil(store.preferences[:content_preview])
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
end
end end