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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +339 -0
  4. data/README +74 -0
  5. data/app/controllers/apijs_controller.rb +326 -0
  6. data/app/views/application/_browser.html.erb +45 -0
  7. data/app/views/attachments/_links.html.erb +146 -0
  8. data/app/views/settings/_apijs.html.erb +256 -0
  9. data/assets/fonts/apijs/config.json +130 -0
  10. data/assets/fonts/apijs/fontello.woff +0 -0
  11. data/assets/fonts/apijs/fontello.woff2 +0 -0
  12. data/assets/images/apijs/player-black-200.png +0 -0
  13. data/assets/images/apijs/player-black-400.png +0 -0
  14. data/assets/images/apijs/player-white-200.png +0 -0
  15. data/assets/images/apijs/player-white-400.png +0 -0
  16. data/assets/images/apijs/tv.gif +0 -0
  17. data/assets/javascripts/apijs-redmine.min.js +7 -0
  18. data/assets/javascripts/apijs.min.js +7 -0
  19. data/assets/javascripts/app.js +224 -0
  20. data/assets/stylesheets/apijs-print.min.css +8 -0
  21. data/assets/stylesheets/apijs-redmine-rtl.min.css +8 -0
  22. data/assets/stylesheets/apijs-redmine.min.css +8 -0
  23. data/assets/stylesheets/apijs-screen-rtl.min.css +8 -0
  24. data/assets/stylesheets/apijs-screen.min.css +8 -0
  25. data/assets/stylesheets/styles.css +73 -0
  26. data/config/locales/cs.yml +30 -0
  27. data/config/locales/de.yml +30 -0
  28. data/config/locales/en.yml +30 -0
  29. data/config/locales/es.yml +30 -0
  30. data/config/locales/fr.yml +30 -0
  31. data/config/locales/it.yml +30 -0
  32. data/config/locales/ja.yml +30 -0
  33. data/config/locales/nl.yml +30 -0
  34. data/config/locales/pl.yml +30 -0
  35. data/config/locales/pt-BR.yml +30 -0
  36. data/config/locales/pt.yml +30 -0
  37. data/config/locales/ru.yml +30 -0
  38. data/config/locales/sk.yml +30 -0
  39. data/config/locales/tr.yml +30 -0
  40. data/config/locales/zh.yml +30 -0
  41. data/config/routes.rb +86 -0
  42. data/init.rb +69 -0
  43. data/lib/apijs_attachment.rb +252 -0
  44. data/lib/apijs_const.rb +35 -0
  45. data/lib/apijs_files.rb +55 -0
  46. data/lib/image.py +201 -0
  47. data/lib/redmine_apijs.rb +46 -0
  48. data/lib/useragentparser.rb +175 -0
  49. data/lib/video.py +78 -0
  50. data/redmine_apijs.gemspec +69 -0
  51. 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)
@@ -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
@@ -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
@@ -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)