aboutsummaryrefslogtreecommitdiffstats
path: root/core/mui/gtk_web_image_loader.rb
blob: 6f86d2203a8b50556e93c7b4a2a1b1d5374b266d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# -*- coding: utf-8 -*-
# 画像のURLを受け取って、Gtk::Pixbufを返す

miquire :core, 'serialthread', 'skin'
miquire :mui, 'web_image_loader_image_cache'
miquire :lib, 'addressable/uri'
require 'net/http'
require 'uri'
require 'thread'
require 'fileutils'

module Gdk::WebImageLoader
  extend Gdk::WebImageLoader

  WebImageThread = SerialThreadGroup.new
  WebImageThread.max_threads = 16

  # URLから画像をダウンロードして、その内容を持ったGdk::Pixbufのインスタンスを返す
  # ==== Args
  # [url] 画像のURL
  # [rect] 画像のサイズ(Gdk::Rectangle) または幅(px)
  # [height] 画像の高さ(px)
  # [&load_callback]
  #   画像のダウンロードで処理がブロッキングされるような場合、ブロックが指定されていれば
  #   このメソッドはとりあえずloading中の画像のPixbufを返し、ロードが完了したらブロックを呼び出す
  # ==== Return
  # Pixbuf
  def pixbuf(url, rect, height = nil, &load_callback)
    url = Plugin.filtering(:web_image_loader_url_filter, url.freeze)[0].freeze
    rect = Gdk::Rectangle.new(0, 0, rect, height) if height
    pixbuf = ImageCache::Pixbuf.load(url, rect)
    return pixbuf if pixbuf
    if(is_local_path?(url))
      url = File.expand_path(url)
      if(FileTest.exist?(url))
        GdkPixbuf::Pixbuf.new(file: url, width: rect.width, height: rect.height)
      else
        notfound_pixbuf(rect) end
    else
      via_internet(url, rect, &load_callback) end
  rescue Gdk::PixbufError
    notfound_pixbuf(rect)
  rescue => e
    if into_debug_mode(e)
      raise e
    else
      notfound_pixbuf(rect) end end

  # _url_ が指している画像を任意のサイズにリサイズして、その画像のパスを返す。
  # このメソッドは画像のダウンロードが発生すると処理をブロッキングする。
  # 取得に失敗した場合は nil を返す。
  # ==== Args
  # [url] 画像のURL
  # [width] 幅(px)
  # [height] 高さ(px)
  # ==== Return
  # 画像のパス
  def local_path(url, width = 48, height = width)
    url.freeze
    ext = (File.extname(url).split("?", 2)[0] or File.extname(url))
    filename = File.expand_path(File.join(Environment::TMPDIR, Digest::MD5.hexdigest(url + "#{width}x#{height}") + ext + '.png'))
    pb = pixbuf(url, width, height)
    if(pb)
      pb.save(filename, 'png') if not FileTest.exist?(filename)
      local_path_files_add(filename)
      filename end end

  # urlが指している画像のデータを返す。
  # ==== Args
  # [url] 画像のURL
  # ==== Return
  # キャッシュがあればロード後のデータを即座に返す。
  # ブロックが指定されれば、キャッシュがない時は :wait を返して、ロードが完了したらブロックを呼び出す。
  # ブロックが指定されなければ、ロード完了まで待って、ロードが完了したらそのデータを返す。
  def get_raw_data(url, &load_callback) # :yield: raw, exception, url
    url.freeze
    raw = ImageCache::Raw.load(url)
    if raw and not raw.empty?
      raw
    else
      exception = nil
      if load_callback
        WebImageThread.new{
          get_raw_data_load_proc(url, &load_callback) }
        :wait
      else
        get_raw_data_load_proc(url, &load_callback) end end
  rescue Gdk::PixbufError
    nil end

  # get_raw_dataの内部関数。
  # HTTPコネクションを張り、 _url_ をダウンロードしてjpegとかpngとかの情報をそのまま返す。
  def get_raw_data_load_proc(url, &load_callback)
    ImageCache.synchronize(url) {
      forerunner_result = ImageCache::Raw.load(url)
      if(forerunner_result)
        raw = forerunner_result
        if load_callback
          load_callback.call(*[forerunner_result, nil, url][0..load_callback.arity])
          forerunner_result
        else
          forerunner_result end
      else
        no_mainthread
        begin
          res = get_icon_via_http(url)
          if(res.is_a?(Net::HTTPResponse)) and (res.code == '200')
            raw = res.body.to_s
          else
            exception = true end
        rescue Timeout::Error, StandardError => e
          exception = e end
        ImageCache::Raw.save(url, raw)
        if load_callback
          load_callback.call(*[raw, exception, url][0..load_callback.arity])
          raw
        else
          raw end end } end

  # get_raw_dataのdeferred版
  def get_raw_data_d(url)
    url.freeze
    promise = Deferred.new true
    Thread.new {
      result = get_raw_data(url){ |raw, e|
        begin
          if e
            promise.fail(e)
          elsif raw and not raw.empty?
            promise.call(raw)
          else
            promise.fail(raw) end
        rescue Exception => e
          promise.fail(e) end }
      if result
        if :wait != result
          promise.call(result) end
      else
        promise.fail(result) end }
    promise end

  # _url_ が、インターネット上のリソースを指しているか、ローカルのファイルを指しているかを返す
  # ==== Args
  # [url] ファイルのパス又はURL
  # ==== Return
  # ローカルのファイルならtrue
  def is_local_path?(url)
    not url.start_with?('http') end

  # ロード中の画像のPixbufを返す
  # ==== Args
  # [rect] サイズ(Gtk::Rectangle) 又は幅(px)
  # [height] 高さ
  # ==== Return
  # Pixbuf
  def loading_pixbuf(rect, height = nil)
    if height
      _loading_pixbuf(rect, height)
    else
      _loading_pixbuf(rect.width, rect.height) end end
  def _loading_pixbuf(width, height)
    GdkPixbuf::Pixbuf.new(file: File.expand_path(Skin.get("loading.png")), width: width, height: height).freeze end
  memoize :_loading_pixbuf

  # 画像が見つからない場合のPixbufを返す
  # ==== Args
  # [rect] サイズ(Gtk::Rectangle) 又は幅(px)
  # [height] 高さ
  # ==== Return
  # Pixbuf
  def notfound_pixbuf(rect, height = nil)
    if height
      _notfound_pixbuf(rect, height)
    else
      _notfound_pixbuf(rect.width, rect.height) end end
  def _notfound_pixbuf(width, height)
    GdkPixbuf::Pixbuf.new(file: File.expand_path(Skin.get("notfound.png")), width: width, height: height).freeze
  end
  memoize :_notfound_pixbuf

  # _src_ が _rect_ にアスペクト比を維持した状態で内接するように縮小した場合のサイズを返す
  # ==== Args
  # [src] 元の寸法(Gtk::Rectangle)
  # [dst] 収めたい枠の寸法(Gtk::Rectangle)
  # ==== Return
  # Pixbuf
  def calc_fitclop(src, dst)
    if (dst.width * src.height) > (dst.height * src.width)
      return src.width * dst.height / src.height, dst.height
    else
      return dst.width, src.height * dst.width / src.width end end

  private

  # urlが指している画像を引っ張ってきてPixbufを返す。
  # 画像をダウンロードする場合は、読み込み中の画像を返して、ロードが終わったらブロックを実行する
  # ==== Args
  # [url] 画像のURL
  # [rect] 画像のサイズ(Gdk::Rectangle)
  # [&load_callback] ロードが終わったら実行されるブロック
  # ==== Return
  # ロード中のPixbufか、キャッシュがあればロード後のPixbufを即座に返す
  # ブロックが指定されなければ、ロード完了まで待って、ロードが完了したらそのPixbufを返す
  def via_internet(url, rect, &load_callback) # :yield: pixbuf, exception, url
    url.freeze
    if block_given?
      raw = get_raw_data(url){ |raw, exception|
        pixbuf = notfound_pixbuf(rect)
        begin
          pixbuf = ImageCache::Pixbuf.save(url, rect, inmemory2pixbuf(raw, rect, true)) if raw
        rescue Gdk::PixbufError => e
          exception = e
        end
        Delayer.new{ load_callback.call(pixbuf, exception, url) } }
      if raw.is_a?(String)
        ImageCache::Pixbuf.save(url, rect, inmemory2pixbuf(raw, rect))
      else
        loading_pixbuf(rect) end
    else
      raw = get_raw_data(url)
      if raw
        ImageCache::Pixbuf.save(url, rect, inmemory2pixbuf(raw, rect))
      else
        notfound_pixbuf(rect) end end
  rescue Gdk::PixbufError
    notfound_pixbuf(rect) end

  # メモリ上の画像データをPixbufにロードする
  # ==== Args
  # [image_data] メモリ上の画像データ
  # [rect] サイズ(Gdk::Rectangle)
  # [raise_exception] 真PixbufError例外を投げる(default: false)
  # ==== Exceptions
  # Gdk::PixbufError例外が発生したら、notfound_pixbufを返します。
  # ただし、 _raise_exception_ が真なら例外を投げます。
  # ==== Return
  # Pixbuf
  def inmemory2pixbuf(image_data, rect, raise_exception = false)
    rect = rect.dup
    loader = Gdk::PixbufLoader.new
    # loader.set_size(rect.width, rect.height) if rect
    loader.write image_data
    loader.close
    pb = loader.pixbuf
    pb.scale(*calc_fitclop(pb, rect))
  rescue Gdk::PixbufError => e
    if raise_exception
      raise e
    else
      notfound_pixbuf(rect) end end

  def http(host, port, scheme)
    result = nil
    atomic{
      @http_pool = Hash.new{|h, k|h[k] = Hash.new{|_h, _k|_h[_k] = {} } } if not defined? @http_pool
      if not @http_pool[host][port][scheme]
        pool = []
        @http_pool[host][port][scheme] = Queue.new
        4.times { |index|
          http = case scheme
                 when 'http'.freeze
                   Net::HTTP.new(host, port || 80)
                 when 'https'.freeze
                   Net::HTTP.new(host, port || 443).tap{|_h|
                     _h.use_ssl = true } end
          http.open_timeout=5
          http.read_timeout=30
          pool << http
          @http_pool[host][port][scheme].push(pool) } end }
    pool = @http_pool[host][port][scheme].pop
    http = pool.pop
    result = yield(http)
  ensure
    pool.push(http) if defined? http
    @http_pool[host][port][scheme].push(pool) if defined? pool
    result
  end

  def get_icon_via_http(url)
    uri = Addressable::URI.parse(url)
    request = Net::HTTP::Get.new(uri.request_uri)
    request['Connection'] = 'Keep-Alive'
    http(uri.host, uri.port, uri.scheme) do |http|
      begin
        http.request(request)
      rescue EOFError => e
        http.finish
        http.start
        notice "open connection for #{uri.host}"
        http.request(request) end end end

  def local_path_files_add(path)
    atomic{
      if not defined?(@local_path_files)
        @local_path_files = Set.new
        at_exit{ FileUtils.rm(@local_path_files.to_a) } end }
    @local_path_files << path
  end
end