redmine_apijs 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE +339 -0
- data/README +74 -0
- data/app/controllers/apijs_controller.rb +326 -0
- data/app/views/application/_browser.html.erb +45 -0
- data/app/views/attachments/_links.html.erb +146 -0
- data/app/views/settings/_apijs.html.erb +256 -0
- data/assets/fonts/apijs/config.json +130 -0
- data/assets/fonts/apijs/fontello.woff +0 -0
- data/assets/fonts/apijs/fontello.woff2 +0 -0
- data/assets/images/apijs/player-black-200.png +0 -0
- data/assets/images/apijs/player-black-400.png +0 -0
- data/assets/images/apijs/player-white-200.png +0 -0
- data/assets/images/apijs/player-white-400.png +0 -0
- data/assets/images/apijs/tv.gif +0 -0
- data/assets/javascripts/apijs-redmine.min.js +7 -0
- data/assets/javascripts/apijs.min.js +7 -0
- data/assets/javascripts/app.js +224 -0
- data/assets/stylesheets/apijs-print.min.css +8 -0
- data/assets/stylesheets/apijs-redmine-rtl.min.css +8 -0
- data/assets/stylesheets/apijs-redmine.min.css +8 -0
- data/assets/stylesheets/apijs-screen-rtl.min.css +8 -0
- data/assets/stylesheets/apijs-screen.min.css +8 -0
- data/assets/stylesheets/styles.css +73 -0
- data/config/locales/cs.yml +30 -0
- data/config/locales/de.yml +30 -0
- data/config/locales/en.yml +30 -0
- data/config/locales/es.yml +30 -0
- data/config/locales/fr.yml +30 -0
- data/config/locales/it.yml +30 -0
- data/config/locales/ja.yml +30 -0
- data/config/locales/nl.yml +30 -0
- data/config/locales/pl.yml +30 -0
- data/config/locales/pt-BR.yml +30 -0
- data/config/locales/pt.yml +30 -0
- data/config/locales/ru.yml +30 -0
- data/config/locales/sk.yml +30 -0
- data/config/locales/tr.yml +30 -0
- data/config/locales/zh.yml +30 -0
- data/config/routes.rb +86 -0
- data/init.rb +69 -0
- data/lib/apijs_attachment.rb +252 -0
- data/lib/apijs_const.rb +35 -0
- data/lib/apijs_files.rb +55 -0
- data/lib/image.py +201 -0
- data/lib/redmine_apijs.rb +46 -0
- data/lib/useragentparser.rb +175 -0
- data/lib/video.py +78 -0
- data/redmine_apijs.gemspec +69 -0
- metadata +94 -0
data/init.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Created L/21/05/2012
|
3
|
+
# Updated V/24/07/2020
|
4
|
+
#
|
5
|
+
# Copyright 2008-2020 | Fabrice Creuzot (luigifab) <code~luigifab~fr>
|
6
|
+
# https://www.luigifab.fr/redmine/apijs
|
7
|
+
#
|
8
|
+
# This program is free software, you can redistribute it or modify
|
9
|
+
# it under the terms of the GNU General Public License (GPL) as published
|
10
|
+
# by the free software foundation, either version 2 of the license, or
|
11
|
+
# (at your option) any later version.
|
12
|
+
#
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
14
|
+
# but without any warranty, without even the implied warranty of
|
15
|
+
# merchantability or fitness for a particular purpose. See the
|
16
|
+
# GNU General Public License (GPL) for more details.
|
17
|
+
|
18
|
+
require 'redmine'
|
19
|
+
require 'apijs_const'
|
20
|
+
require 'apijs_files'
|
21
|
+
require 'apijs_attachment'
|
22
|
+
require 'useragentparser'
|
23
|
+
|
24
|
+
Redmine::Plugin.register :redmine_apijs do
|
25
|
+
|
26
|
+
name 'Redmine Apijs plugin'
|
27
|
+
author 'Fabrice Creuzot'
|
28
|
+
description 'Integrate the apijs javascript library into Redmine.'
|
29
|
+
version '6.3.0-gem'
|
30
|
+
url 'https://www.luigifab.fr/redmine/apijs'
|
31
|
+
author_url 'https://www.luigifab.fr/'
|
32
|
+
|
33
|
+
permission :edit_attachments, {apijs: :edit}, {require: :loggedin}
|
34
|
+
permission :delete_attachments, {apijs: :delete}, {require: [:loggedin, :member]}
|
35
|
+
requires_redmine version_or_higher: '1.4.0'
|
36
|
+
|
37
|
+
settings({
|
38
|
+
partial: 'settings/apijs',
|
39
|
+
default: {
|
40
|
+
enabled: '0',
|
41
|
+
sort_attachments: '0',
|
42
|
+
browser: '0',
|
43
|
+
show_album: '0',
|
44
|
+
show_album_infos: '0',
|
45
|
+
show_filename: '0',
|
46
|
+
show_exifdate: '0',
|
47
|
+
album_exclude_name: '',
|
48
|
+
album_exclude_desc: '',
|
49
|
+
create_all: '0',
|
50
|
+
# python-pil
|
51
|
+
album_mimetype_jpg: '1',
|
52
|
+
album_mimetype_jpeg: '1',
|
53
|
+
album_mimetype_gif: '0',
|
54
|
+
album_mimetype_png: '0',
|
55
|
+
album_mimetype_tif: '0',
|
56
|
+
album_mimetype_tiff: '0',
|
57
|
+
album_mimetype_webp: '0',
|
58
|
+
album_mimetype_bmp: '0',
|
59
|
+
album_mimetype_eps: '0',
|
60
|
+
album_mimetype_psd: '0',
|
61
|
+
# python-scour
|
62
|
+
album_mimetype_svg: '1',
|
63
|
+
# ffmpegthumbnailer
|
64
|
+
album_mimetype_ogv: '1',
|
65
|
+
album_mimetype_webm: '1',
|
66
|
+
album_mimetype_mp4: '0'
|
67
|
+
}
|
68
|
+
})
|
69
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Created V/27/12/2013
|
3
|
+
# Updated D/26/07/2020
|
4
|
+
#
|
5
|
+
# Copyright 2008-2020 | Fabrice Creuzot (luigifab) <code~luigifab~fr>
|
6
|
+
# https://www.luigifab.fr/redmine/apijs
|
7
|
+
#
|
8
|
+
# This program is free software, you can redistribute it or modify
|
9
|
+
# it under the terms of the GNU General Public License (GPL) as published
|
10
|
+
# by the free software foundation, either version 2 of the license, or
|
11
|
+
# (at your option) any later version.
|
12
|
+
#
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
14
|
+
# but without any warranty, without even the implied warranty of
|
15
|
+
# merchantability or fitness for a particular purpose. See the
|
16
|
+
# GNU General Public License (GPL) for more details.
|
17
|
+
|
18
|
+
module ApijsAttachment
|
19
|
+
|
20
|
+
def self.included(base)
|
21
|
+
base.send(:include, InstanceMethods)
|
22
|
+
base.class_eval do
|
23
|
+
unloadable
|
24
|
+
if Rails::VERSION::MAJOR >= 3
|
25
|
+
include Rails.application.routes.url_helpers
|
26
|
+
else
|
27
|
+
include ActionController::UrlWriter
|
28
|
+
end
|
29
|
+
if Rails::VERSION::MAJOR >= 4
|
30
|
+
before_create :update_date
|
31
|
+
else
|
32
|
+
before_save :update_date
|
33
|
+
end
|
34
|
+
after_destroy :delete_cache
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module InstanceMethods
|
39
|
+
|
40
|
+
# https://www.redmine.org/issues/19024
|
41
|
+
def getUrl(action, all=false)
|
42
|
+
if action == 'redmineshow'
|
43
|
+
return self.getSuburi(url_for({only_path: true, controller: 'attachments', action: 'show', id: self.id, filename: self.filename}))
|
44
|
+
elsif all
|
45
|
+
return self.getSuburi(url_for({only_path: true, controller: 'apijs', action: action, id: self.id, filename: self.filename}))
|
46
|
+
else
|
47
|
+
return self.getSuburi(url_for({only_path: true, controller: 'apijs', action: action}))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def getSuburi(url)
|
52
|
+
if Redmine::VERSION::MAJOR >= 2
|
53
|
+
baseurl = Redmine::Utils.relative_url_root
|
54
|
+
if not baseurl.blank? and not url.match(/^#{baseurl}/)
|
55
|
+
url = baseurl + url
|
56
|
+
end
|
57
|
+
end
|
58
|
+
return url
|
59
|
+
end
|
60
|
+
|
61
|
+
# liens
|
62
|
+
def getShowUrl
|
63
|
+
return self.getUrl('show', true)
|
64
|
+
end
|
65
|
+
|
66
|
+
def getThumbUrl
|
67
|
+
return self.filename =~ /\.svg$/i ? self.getShowUrl : self.getUrl('thumb', true)
|
68
|
+
end
|
69
|
+
|
70
|
+
def getSrcsetUrl
|
71
|
+
return self.filename =~ /\.svg$/i ? self.getShowUrl : self.getUrl('srcset', true)
|
72
|
+
end
|
73
|
+
|
74
|
+
def getDownloadUrl
|
75
|
+
return self.getUrl('download', true)
|
76
|
+
end
|
77
|
+
|
78
|
+
def getDownloadButton
|
79
|
+
return "self.location.href = '" + self.getUrl('download', true) + "';"
|
80
|
+
end
|
81
|
+
|
82
|
+
def getEditButton(token)
|
83
|
+
return "apijsRedmine.editAttachment(" + self.id.to_s + ", '" + self.getUrl('editdesc') + "', '" + token + "');"
|
84
|
+
end
|
85
|
+
|
86
|
+
def getDeleteButton(token)
|
87
|
+
return "apijsRedmine.removeAttachment(" + self.id.to_s + ", '" + self.getUrl('delete') + "', '" + token + "');"
|
88
|
+
end
|
89
|
+
|
90
|
+
def getShowButton(setting_show_filename, setting_show_exifdate, description)
|
91
|
+
if self.isImage?
|
92
|
+
return "apijs.dialog.dialogPhoto('" + self.getShowUrl + "', '" + ((setting_show_filename) ? self.filename : 'false') + "', '" + ((setting_show_exifdate) ? format_time(self.created_on) : 'false') + "', '" + description + "');"
|
93
|
+
elsif self.isVideo?
|
94
|
+
return "apijs.dialog.dialogVideo('" + self.getDownloadUrl + "', '" + ((setting_show_filename) ? self.filename : 'false') + "', '" + ((setting_show_exifdate) ? format_time(self.created_on) : 'false') + "', '" + description + "');"
|
95
|
+
elsif self.is_text?
|
96
|
+
return "self.location.href = '" + self.getUrl('redmineshow', false) + "';"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# chemin
|
101
|
+
def getImgThumb
|
102
|
+
return File.join(APIJS_ROOT, 'thumb', self.created_on.strftime('%Y-%m').to_s, self.id.to_s + self.getExt)
|
103
|
+
end
|
104
|
+
|
105
|
+
def getImgSrcset
|
106
|
+
return File.join(APIJS_ROOT, 'srcset', self.created_on.strftime('%Y-%m').to_s, self.id.to_s + self.getExt)
|
107
|
+
end
|
108
|
+
|
109
|
+
def getImgShow
|
110
|
+
return File.join(APIJS_ROOT, 'show', self.created_on.strftime('%Y-%m').to_s, self.id.to_s + self.getExt)
|
111
|
+
end
|
112
|
+
|
113
|
+
# image, photo, vidéo
|
114
|
+
def isImage?
|
115
|
+
return self.filename =~ /\.(jpg|jpeg|gif|png|webp|svg)$/i
|
116
|
+
end
|
117
|
+
|
118
|
+
def isPhoto?
|
119
|
+
types = []
|
120
|
+
types.push('jpg') if Setting.plugin_redmine_apijs['album_mimetype_jpg'] == '1'
|
121
|
+
types.push('jpeg') if Setting.plugin_redmine_apijs['album_mimetype_jpeg'] == '1'
|
122
|
+
types.push('gif') if Setting.plugin_redmine_apijs['album_mimetype_gif'] == '1'
|
123
|
+
types.push('png') if Setting.plugin_redmine_apijs['album_mimetype_png'] == '1'
|
124
|
+
types.push('tif') if Setting.plugin_redmine_apijs['album_mimetype_tif'] == '1'
|
125
|
+
types.push('tiff') if Setting.plugin_redmine_apijs['album_mimetype_tiff'] == '1'
|
126
|
+
types.push('webp') if Setting.plugin_redmine_apijs['album_mimetype_webp'] == '1'
|
127
|
+
types.push('bmp') if Setting.plugin_redmine_apijs['album_mimetype_bmp'] == '1'
|
128
|
+
types.push('eps') if Setting.plugin_redmine_apijs['album_mimetype_eps'] == '1'
|
129
|
+
types.push('psd') if Setting.plugin_redmine_apijs['album_mimetype_psd'] == '1'
|
130
|
+
types.push('svg') if Setting.plugin_redmine_apijs['album_mimetype_svg'] == '1'
|
131
|
+
return self.filename =~ /\.(#{types.join('|')})$/i
|
132
|
+
end
|
133
|
+
|
134
|
+
def isVideo?
|
135
|
+
types = []
|
136
|
+
types.push('ogv') if Setting.plugin_redmine_apijs['album_mimetype_ogv'] == '1'
|
137
|
+
types.push('webm') if Setting.plugin_redmine_apijs['album_mimetype_webm'] == '1'
|
138
|
+
types.push('mp4') if Setting.plugin_redmine_apijs['album_mimetype_mp4'] == '1'
|
139
|
+
return self.filename =~ /\.(#{types.join('|')})$/i
|
140
|
+
end
|
141
|
+
|
142
|
+
# extension des images générées
|
143
|
+
def getExt
|
144
|
+
ext = File.extname(self.filename).downcase
|
145
|
+
return ext if ext == '.gif'
|
146
|
+
return ext if ext == '.png'
|
147
|
+
return ext if ext == '.webp'
|
148
|
+
return ext if ext == '.svg'
|
149
|
+
return '.jpg'
|
150
|
+
end
|
151
|
+
|
152
|
+
# commande python
|
153
|
+
def getCmd(source, target, width, height, fixed=false)
|
154
|
+
|
155
|
+
if Redmine::Platform.mswin?
|
156
|
+
cmd = 'python.exe'
|
157
|
+
else
|
158
|
+
cmd = `command -v python3 || command -v python || command -v python2`.to_s.strip!
|
159
|
+
end
|
160
|
+
|
161
|
+
script = File.join(File.dirname(__FILE__), (self.isPhoto? ? 'image.py' : 'video.py'))
|
162
|
+
return cmd + ' ' + script.to_s + ' ' + source.to_s + ' ' + target.to_s + ' ' +
|
163
|
+
width.to_s + ' ' + height.to_s + (fixed ? ' 90 fixed' : ' 90') + ' 2>&1'
|
164
|
+
end
|
165
|
+
|
166
|
+
# exclusion
|
167
|
+
def isExcluded?
|
168
|
+
|
169
|
+
names = Setting.plugin_redmine_apijs['album_exclude_name'].split(',')
|
170
|
+
descs = Setting.plugin_redmine_apijs['album_exclude_desc'].split(',')
|
171
|
+
|
172
|
+
unless names.empty?
|
173
|
+
names.each { |token|
|
174
|
+
return true if (!self.filename.blank? && self.filename.index(token) == 0)
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
unless descs.empty?
|
179
|
+
descs.each { |token|
|
180
|
+
return true if (!self.description.blank? && self.description.index(token) == 0)
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
return false
|
185
|
+
end
|
186
|
+
|
187
|
+
# supprime les images en cache
|
188
|
+
def delete_cache
|
189
|
+
|
190
|
+
img_thumb = self.getImgThumb
|
191
|
+
File.delete(img_thumb) if File.file?(img_thumb)
|
192
|
+
|
193
|
+
img_srcset = self.getImgSrcset
|
194
|
+
File.delete(img_srcset) if File.file?(img_srcset)
|
195
|
+
|
196
|
+
img_show = self.getImgShow
|
197
|
+
File.delete(img_show) if File.file?(img_show)
|
198
|
+
end
|
199
|
+
|
200
|
+
# lecture de la date exif et maj de la date de création avec exiftool (libimage-exiftool)
|
201
|
+
def update_date
|
202
|
+
|
203
|
+
if new_record? && (self.isPhoto? || self.isVideo?) && self.readable?
|
204
|
+
|
205
|
+
cmd = 'exiftool -FastScan -IgnoreMinorErrors -DateTimeOriginal -S3 ' + self.diskfile + ' 2>&1'
|
206
|
+
result = `#{cmd}`.gsub(/^\s+|\s+$/, '')
|
207
|
+
|
208
|
+
logger.info 'APIJS::ApijsAttachment#update_date: ' + cmd + ' (' + result + ')'
|
209
|
+
|
210
|
+
# 2014:06:14 16:43:53 (utilise le fuseau horaire de l'utilisateur)
|
211
|
+
if result =~ /^[0-9]{4}.[0-9]{2}.[0-9]{2} [0-9]{2}.[0-9]{2}.[0-9]{2}/
|
212
|
+
|
213
|
+
date = result[0..9].gsub(':', '-') + ' ' + result[11..18]
|
214
|
+
zone = User.current.time_zone
|
215
|
+
date = zone ? zone.parse(date) : date
|
216
|
+
|
217
|
+
self.created_on = date
|
218
|
+
self.update_filedir!
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# déplace le nouveau fichier s'il n'est pas dans le bon dossier
|
224
|
+
def update_filedir!
|
225
|
+
|
226
|
+
return unless defined? self.disk_directory
|
227
|
+
|
228
|
+
src = self.diskfile
|
229
|
+
time = self.created_on || DateTime.now
|
230
|
+
self.disk_directory = time.strftime("%Y/%m")
|
231
|
+
dest = self.diskfile
|
232
|
+
|
233
|
+
return if src == dest
|
234
|
+
|
235
|
+
unless FileUtils.mkdir_p(File.dirname(dest))
|
236
|
+
logger.error 'Could not create directory ' + File.dirname(dest)
|
237
|
+
return
|
238
|
+
end
|
239
|
+
|
240
|
+
unless FileUtils.mv(src, dest)
|
241
|
+
logger.error 'Could not move attachment from ' + src + ' to ' + dest
|
242
|
+
return
|
243
|
+
end
|
244
|
+
|
245
|
+
update_column :disk_directory, self.disk_directory unless new_record?
|
246
|
+
logger.info 'APIJS::ApijsAttachment#update_filedir: moving file from ' + src + ' to ' + dest
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
Attachment.send(:include, ApijsAttachment)
|
data/lib/apijs_const.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Created L/01/09/2014
|
3
|
+
# Updated J/23/07/2020
|
4
|
+
#
|
5
|
+
# Copyright 2008-2020 | Fabrice Creuzot (luigifab) <code~luigifab~fr>
|
6
|
+
# https://www.luigifab.fr/redmine/apijs
|
7
|
+
#
|
8
|
+
# This program is free software, you can redistribute it or modify
|
9
|
+
# it under the terms of the GNU General Public License (GPL) as published
|
10
|
+
# by the free software foundation, either version 2 of the license, or
|
11
|
+
# (at your option) any later version.
|
12
|
+
#
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
14
|
+
# but without any warranty, without even the implied warranty of
|
15
|
+
# merchantability or fitness for a particular purpose. See the
|
16
|
+
# GNU General Public License (GPL) for more details.
|
17
|
+
|
18
|
+
if Redmine::VERSION::MAJOR >= 3
|
19
|
+
if defined? Redmine.root
|
20
|
+
ALL_FILES = Redmine::Configuration['attachments_storage_path'] || File.join(Redmine.root, 'files')
|
21
|
+
APIJS_ROOT = File.join(Redmine.root, 'tmp', 'apijs')
|
22
|
+
else
|
23
|
+
ALL_FILES = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, 'files')
|
24
|
+
APIJS_ROOT = File.join(Rails.root, 'tmp', 'apijs')
|
25
|
+
end
|
26
|
+
elsif ENV['RAILS_TMP']
|
27
|
+
ALL_FILES = Redmine::Configuration['attachments_storage_path'] || File.join(ENV['RAILS_VAR'], 'files')
|
28
|
+
APIJS_ROOT = File.join(ENV['RAILS_TMP'], 'tmp', 'apijs')
|
29
|
+
elsif ENV['RAILS_CACHE']
|
30
|
+
ALL_FILES = Redmine::Configuration['attachments_storage_path'] || File.join(ENV['RAILS_VAR'], 'files')
|
31
|
+
APIJS_ROOT = File.join(ENV['RAILS_CACHE'], 'tmp', 'apijs')
|
32
|
+
else
|
33
|
+
ALL_FILES = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, 'files')
|
34
|
+
APIJS_ROOT = File.join(Rails.root, 'tmp', 'apijs')
|
35
|
+
end
|
data/lib/apijs_files.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Created L/21/05/2012
|
3
|
+
# Updated D/03/05/2020
|
4
|
+
#
|
5
|
+
# Copyright 2008-2020 | Fabrice Creuzot (luigifab) <code~luigifab~fr>
|
6
|
+
# https://www.luigifab.fr/redmine/apijs
|
7
|
+
#
|
8
|
+
# This program is free software, you can redistribute it or modify
|
9
|
+
# it under the terms of the GNU General Public License (GPL) as published
|
10
|
+
# by the free software foundation, either version 2 of the license, or
|
11
|
+
# (at your option) any later version.
|
12
|
+
#
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
14
|
+
# but without any warranty, without even the implied warranty of
|
15
|
+
# merchantability or fitness for a particular purpose. See the
|
16
|
+
# GNU General Public License (GPL) for more details.
|
17
|
+
|
18
|
+
class ApijsFiles < Redmine::Hook::ViewListener
|
19
|
+
|
20
|
+
def view_layouts_base_html_head(context)
|
21
|
+
rtl = l(:direction) == 'rtl'
|
22
|
+
if Setting.plugin_redmine_apijs['enabled'] == '1'
|
23
|
+
#stylesheet_link_tag(langrtl && rtl ? 'apijs-screen-rtl.min.css' : 'apijs-screen.min.css', :plugin => 'redmine_apijs', :media => 'screen') +
|
24
|
+
stylesheet_link_tag('apijs-screen.min.css', plugin: 'redmine_apijs', media: 'screen') +
|
25
|
+
stylesheet_link_tag(rtl ? 'apijs-redmine-rtl.min.css' : 'apijs-redmine.min.css', plugin: 'redmine_apijs', media: 'screen') +
|
26
|
+
stylesheet_link_tag('apijs-print.min.css', plugin: 'redmine_apijs', media: 'print') +
|
27
|
+
javascript_include_tag('apijs.min.js', plugin: 'redmine_apijs') +
|
28
|
+
javascript_include_tag('apijs-redmine.min.js', plugin: 'redmine_apijs')
|
29
|
+
else
|
30
|
+
stylesheet_link_tag(rtl ? 'apijs-redmine-rtl.min.css' : 'apijs-redmine.min.css', plugin: 'redmine_apijs', media: 'screen')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if Redmine::VERSION::MAJOR >= 4 || Redmine::VERSION::MAJOR == 3 && Redmine::VERSION::MINOR >= 3
|
35
|
+
def view_layouts_base_body_top(context)
|
36
|
+
if Setting.plugin_redmine_apijs['browser'] == '1'
|
37
|
+
controller = context[:controller]
|
38
|
+
Thread.current[:request] = controller.request
|
39
|
+
controller.render partial: 'browser', locals: {pos: 'top'}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
else
|
43
|
+
def view_layouts_base_body_bottom(context)
|
44
|
+
if Setting.plugin_redmine_apijs['browser'] == '1'
|
45
|
+
controller = context[:controller]
|
46
|
+
Thread.current[:request] = controller.request
|
47
|
+
if Rails::VERSION::MAJOR >= 3
|
48
|
+
controller.render partial: 'browser', locals: {pos: 'bottom'}
|
49
|
+
else
|
50
|
+
return controller.send(:render_to_string, partial: 'application/browser', locals: {pos: 'bottom'}) # Redmine 1.4
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/image.py
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf8 -*-
|
3
|
+
# Created J/26/12/2013
|
4
|
+
# Updated D/26/07/2020
|
5
|
+
#
|
6
|
+
# Copyright 2008-2020 | Fabrice Creuzot (luigifab) <code~luigifab~fr>
|
7
|
+
# https://www.luigifab.fr/openmage/apijs
|
8
|
+
#
|
9
|
+
# This program is free software, you can redistribute it or modify
|
10
|
+
# it under the terms of the GNU General Public License (GPL) as published
|
11
|
+
# by the free software foundation, either version 2 of the license, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# This program is distributed in the hope that it will be useful,
|
15
|
+
# but without any warranty, without even the implied warranty of
|
16
|
+
# merchantability or fitness for a particular purpose. See the
|
17
|
+
# GNU General Public License (GPL) for more details.
|
18
|
+
|
19
|
+
import os
|
20
|
+
import re
|
21
|
+
import sys
|
22
|
+
from PIL import Image, ImageSequence
|
23
|
+
|
24
|
+
try:
|
25
|
+
filein = str(sys.argv[1])
|
26
|
+
fileout = str(sys.argv[2])
|
27
|
+
size = (int(sys.argv[3]), int(sys.argv[4]))
|
28
|
+
quality = int(sys.argv[5])
|
29
|
+
fixed = len(sys.argv) == 7 and sys.argv[6] == 'fixed'
|
30
|
+
except:
|
31
|
+
print("Usage: image.py source destination width height [quality=0=auto] [fixed]")
|
32
|
+
print("source: all supported format by python-pil (including animated gif/png/webp) or svg")
|
33
|
+
print("destination: jpg,png,gif,webp or svg")
|
34
|
+
exit(-1)
|
35
|
+
|
36
|
+
if not os.path.exists(os.path.dirname(fileout)):
|
37
|
+
os.makedirs(os.path.dirname(fileout))
|
38
|
+
|
39
|
+
# python-scour
|
40
|
+
if ".svg" in fileout:
|
41
|
+
from scour.scour import (sanitizeOptions, start)
|
42
|
+
options = sanitizeOptions()
|
43
|
+
options.strip_xml_prolog = True # --strip-xml-prolog
|
44
|
+
options.remove_metadata = True # --remove-metadata
|
45
|
+
options.strip_comments = True # --enable-comment-stripping
|
46
|
+
options.strip_ids = True # --enable-id-stripping
|
47
|
+
options.indent_type = None # --indent=none
|
48
|
+
start(options, open(filein, 'rb'), open(fileout, 'wb'))
|
49
|
+
exit(0)
|
50
|
+
|
51
|
+
# python-pil
|
52
|
+
source = Image.open(filein)
|
53
|
+
if size[1] == 0 and size[0] == 0:
|
54
|
+
size = (source.size[0], source.size[1])
|
55
|
+
elif size[1] == 0:
|
56
|
+
size = (size[0], int(size[0] / (source.size[0] / source.size[1])))
|
57
|
+
elif size[0] == 0:
|
58
|
+
size = (int(size[1] * (source.size[0] / source.size[1])), size[1])
|
59
|
+
|
60
|
+
# (Animated) PNG resizing (since PIL 7.1.0)
|
61
|
+
# based on https://stackoverflow.com/a/41827681/2980105
|
62
|
+
if source.format == 'PNG' and ".png" in fileout:
|
63
|
+
|
64
|
+
frames = []
|
65
|
+
try:
|
66
|
+
while True:
|
67
|
+
new_frame = Image.new('RGBA', source.size, (255,255,255,1))
|
68
|
+
new_frame.paste(source, (0,0), source.convert('RGBA'))
|
69
|
+
new_frame.thumbnail(size, Image.ANTIALIAS)
|
70
|
+
|
71
|
+
if fixed:
|
72
|
+
offset_x = int(max((size[0] - new_frame.size[0]) / 2, 0))
|
73
|
+
offset_y = int(max((size[1] - new_frame.size[1]) / 2, 0))
|
74
|
+
final_frame = Image.new('RGBA', size, (255,255,255,1))
|
75
|
+
final_frame.paste(new_frame, (offset_x, offset_y))
|
76
|
+
frames.append(final_frame)
|
77
|
+
else:
|
78
|
+
frames.append(new_frame)
|
79
|
+
|
80
|
+
b = source.tell()
|
81
|
+
source.seek(b + 1)
|
82
|
+
if b == source.tell():
|
83
|
+
break
|
84
|
+
except EOFError:
|
85
|
+
pass
|
86
|
+
# Animated GIF resizing
|
87
|
+
# based on https://stackoverflow.com/a/41827681/2980105
|
88
|
+
elif source.format == 'GIF' and source.is_animated and ".gif" in fileout:
|
89
|
+
|
90
|
+
p = source.getpalette()
|
91
|
+
frames = []
|
92
|
+
try:
|
93
|
+
while True:
|
94
|
+
# If the GIF uses local colour tables, each frame will have its own palette.
|
95
|
+
# If not, we need to apply the global palette to the new frame.
|
96
|
+
if not source.getpalette():
|
97
|
+
source.putpalette(p)
|
98
|
+
|
99
|
+
new_frame = Image.new('RGBA', source.size, (255,255,255,1))
|
100
|
+
new_frame.paste(source, (0,0), source.convert('RGBA'))
|
101
|
+
new_frame.thumbnail(size, Image.ANTIALIAS)
|
102
|
+
|
103
|
+
if fixed:
|
104
|
+
offset_x = int(max((size[0] - new_frame.size[0]) / 2, 0))
|
105
|
+
offset_y = int(max((size[1] - new_frame.size[1]) / 2, 0))
|
106
|
+
final_frame = Image.new('RGBA', size, (255,255,255,1))
|
107
|
+
final_frame.paste(new_frame, (offset_x, offset_y))
|
108
|
+
frames.append(final_frame)
|
109
|
+
else:
|
110
|
+
frames.append(new_frame)
|
111
|
+
|
112
|
+
b = source.tell()
|
113
|
+
source.seek(b + 1)
|
114
|
+
if b == source.tell():
|
115
|
+
break
|
116
|
+
except EOFError:
|
117
|
+
pass
|
118
|
+
# Animated WEBP resizing (since PIL 5.0.0)
|
119
|
+
# based on https://stackoverflow.com/a/41827681/2980105
|
120
|
+
elif source.format == 'WEBP' and ".webp" in fileout:
|
121
|
+
|
122
|
+
frames = []
|
123
|
+
try:
|
124
|
+
while True:
|
125
|
+
new_frame = Image.new('RGBA', source.size, (255,255,255,1))
|
126
|
+
new_frame.paste(source, (0,0), source.convert('RGBA'))
|
127
|
+
new_frame.thumbnail(size, Image.ANTIALIAS)
|
128
|
+
|
129
|
+
if fixed:
|
130
|
+
offset_x = int(max((size[0] - new_frame.size[0]) / 2, 0))
|
131
|
+
offset_y = int(max((size[1] - new_frame.size[1]) / 2, 0))
|
132
|
+
final_frame = Image.new('RGBA', size, (255,255,255,1))
|
133
|
+
final_frame.paste(new_frame, (offset_x, offset_y))
|
134
|
+
frames.append(final_frame)
|
135
|
+
else:
|
136
|
+
frames.append(new_frame)
|
137
|
+
|
138
|
+
b = source.tell()
|
139
|
+
source.seek(b + 1)
|
140
|
+
if b == source.tell():
|
141
|
+
break
|
142
|
+
except EOFError:
|
143
|
+
pass
|
144
|
+
# Standard resizing
|
145
|
+
elif fixed:
|
146
|
+
source.thumbnail(size, Image.ANTIALIAS)
|
147
|
+
offset_x = int(max((size[0] - source.size[0]) / 2, 0))
|
148
|
+
offset_y = int(max((size[1] - source.size[1]) / 2, 0))
|
149
|
+
dest = Image.new('RGBA', size, (255,255,255,1))
|
150
|
+
dest.paste(source, (offset_x, offset_y))
|
151
|
+
else:
|
152
|
+
source.thumbnail(size, Image.ANTIALIAS)
|
153
|
+
dest = Image.new('RGBA', (source.size[0], source.size[1]), (255,255,255,1))
|
154
|
+
dest.paste(source)
|
155
|
+
|
156
|
+
# https://pillow.readthedocs.io/en/latest/handbook/image-file-formats.html
|
157
|
+
# source.info.get('loop')=None
|
158
|
+
if ".png" in fileout:
|
159
|
+
# The image quality, on a scale from 0 (best-speed) to 9 (best-compression), the default is 6
|
160
|
+
# but when optimize option is True compress_level has no effect
|
161
|
+
try:
|
162
|
+
if len(frames) == 1:
|
163
|
+
frames[0].save(fileout, 'PNG', optimize=True, compress_level=9)
|
164
|
+
else:
|
165
|
+
frames[0].save(fileout, 'PNG', optimize=True, compress_level=9, save_all=True, append_images=frames[1:],
|
166
|
+
loop=0, duration=source.info.get('duration'))
|
167
|
+
except:
|
168
|
+
dest.save(fileout, 'PNG', optimize=True, compress_level=9)
|
169
|
+
elif ".gif" in fileout:
|
170
|
+
try:
|
171
|
+
if len(frames) == 1:
|
172
|
+
frames[0].save(fileout, 'GIF', optimize=True)
|
173
|
+
else:
|
174
|
+
frames[0].save(fileout, 'GIF', optimize=True, save_all=True, append_images=frames[1:],
|
175
|
+
loop=0, duration=source.info.get('duration'), default_image=source.info.get('default_image'))
|
176
|
+
except:
|
177
|
+
dest.save(fileout, 'GIF', optimize=True)
|
178
|
+
elif ".webp" in fileout:
|
179
|
+
# The image quality, on a scale from 0 (worst) to 100 (best), the default is 80 (lossy, 0 gives the smallest size and 100 the largest)
|
180
|
+
# The method, on a scale from 0 (fast) to 6 (slower-better), the default is 0
|
181
|
+
if quality < 1:
|
182
|
+
quality = 80
|
183
|
+
elif quality > 100:
|
184
|
+
quality = 100
|
185
|
+
try:
|
186
|
+
if len(frames) == 1:
|
187
|
+
frames[0].save(fileout, 'WEBP', method=5, quality=quality)
|
188
|
+
else:
|
189
|
+
frames[0].save(fileout, 'WEBP', method=5, quality=quality, save_all=True, append_images=frames[1:],
|
190
|
+
loop=0, duration=source.info.get('duration'))
|
191
|
+
except:
|
192
|
+
dest.save(fileout, 'WEBP', method=5, quality=quality)
|
193
|
+
else:
|
194
|
+
# The image quality, on a scale from 0 (worst) to 95 (best), the default is 75
|
195
|
+
if quality < 1:
|
196
|
+
quality = 75
|
197
|
+
elif quality > 95:
|
198
|
+
quality = 95
|
199
|
+
dest.convert('RGB').save(fileout, 'JPEG', optimize=True, quality=quality)
|
200
|
+
|
201
|
+
exit(0)
|