You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/frontend/ui/widget/imagewidget.lua

355 lines
13 KiB
Lua

--[[--
ImageWidget shows an image from a file or memory
Show image from file example:
UIManager:show(ImageWidget:new{
file = "resources/info-i.png",
-- Make sure alpha is set to true if png has transparent background
-- alpha = true,
})
Show image from memory example:
UIManager:show(ImageWidget:new{
-- bitmap_buffer should be a block of memory that holds the raw
-- uncompressed bitmap.
image = bitmap_buffer,
})
]]
local Widget = require("ui/widget/widget")
local Screen = require("device").screen
local UIManager = require("ui/uimanager")
local CacheItem = require("cacheitem")
local Mupdf = require("ffi/mupdf")
local Geom = require("ui/geometry")
local Cache = require("cache")
local logger = require("logger")
local ImageCache = Cache:new{
max_memsize = 2*1024*1024, -- 2M of image cache
current_memsize = 0,
cache = {},
-- this will hold the LRU order of the cache
cache_order = {}
}
local ImageCacheItem = CacheItem:new{}
function ImageCacheItem:onFree()
if self.bb.free then
logger.dbg("free image blitbuffer", self.bb)
self.bb:free()
end
end
local ImageWidget = Widget:new{
-- Can be provided with a path to a file
file = nil,
-- or an already made BlitBuffer (ie: made by Mupdf.renderImage())
image = nil,
-- Whether BlitBuffer rendered from file should be cached
file_do_cache = true,
-- Whether provided BlitBuffer can be modified by us and SHOULD be free() by us,
-- normally true unless our caller wants to reuse it's provided image
image_disposable = true,
-- Width and height of container, to limit rendering to this area
-- (if provided, and scale_factor is nil, image will be resized to
-- these width and height without regards to original aspect ratio)
width = nil,
height = nil,
hide = nil, -- to not be painted
-- Settings that apply at paintTo() time
invert = nil,
dim = nil,
alpha = false, -- honors alpha values from the image
-- When rotation_angle is not 0, native image is rotated by this angle
-- before scaling.
rotation_angle = 0,
-- If scale_for_dpi is true image will be rescaled according to screen dpi
-- (x2 if DPI > 332) - (formerly known as 'autoscale')
scale_for_dpi = false,
-- When scale_factor is not nil, native image is scaled by this factor
-- (if scale_factor == 1, native image size is kept)
-- Special case : scale_factor == 0 : image will be scaled to best fit provided
-- width and height, keeping aspect ratio (scale_factor will be updated
-- from 0 to the factor used at _render() time)
-- (former 'autostrech' setting removed, use "scale_factor=0" instead)
scale_factor = nil,
-- For initial positionning, if (possibly scaled) image overflows width/height
center_x_ratio = 0.5, -- default is centered on image's center
center_y_ratio = 0.5,
-- For pan & zoom management:
-- offsets to use in blitFrom()
_offset_x = 0,
_offset_y = 0,
-- limits to center_x_ratio variation around 0.5 (0.5 +/- these values)
-- to keep image centered (0 means center_x_ratio will be forced to 0.5)
_max_off_center_x_ratio = 0,
_max_off_center_y_ratio = 0,
-- So we can reset self.scale_factor to its initial value in free(), in
-- case this same object is free'd but re-used and and re-render'ed
_initial_scale_factor = nil,
_bb = nil,
_bb_disposable = true, -- whether we should free() our _bb
_bb_w = nil,
_bb_h = nil,
}
function ImageWidget:_loadimage()
self._bb = self.image
-- don't touch or free if caller doesn't want that
self._bb_disposable = self.image_disposable
end
function ImageWidget:_loadfile()
local itype = string.lower(string.match(self.file, ".+%.([^.]+)") or "")
if itype == "png" or itype == "jpg" or itype == "jpeg"
or itype == "tiff" then
local hash = "image|"..self.file.."|"..(self.width or "").."|"..(self.height or "")
local cache = ImageCache:check(hash)
if cache then
-- hit cache
self._bb = cache.bb
self._bb_disposable = false -- don't touch or free a cached _bb
else
if not self.file_do_cache then
self._bb = Mupdf.renderImageFile(self.file, self.width, self.height)
self._bb_disposable = true -- we made it, we can modify and free it
else
-- cache this image
logger.dbg("cache", hash)
cache = ImageCacheItem:new{
bb = Mupdf.renderImageFile(self.file, self.width, self.height),
}
cache.size = cache.bb.pitch * cache.bb.h * cache.bb:getBpp() / 8
ImageCache:insert(hash, cache)
self._bb = cache.bb
self._bb_disposable = false -- don't touch or free a cached _bb
end
end
else
error("Image file type not supported.")
end
end
function ImageWidget:_render()
if self._bb then -- already rendered
return
end
logger.dbg("ImageWidget: _render'ing")
if self.image then
self:_loadimage()
elseif self.file then
self:_loadfile()
else
error("cannot render image")
end
-- Store initial scale factor
self._initial_scale_factor = self.scale_factor
-- First, rotation
if self.rotation_angle ~= 0 then
if not self._bb_disposable then
-- we can't modify _bb, make a copy
self._bb = self._bb:copy()
self._bb_disposable = true -- new object will have to be freed
end
self._bb:rotate(self.rotation_angle) -- rotate in-place
end
local bb_w, bb_h = self._bb:getWidth(), self._bb:getHeight()
-- scale_for_dpi setting: update scale_factor (even if not set) with it
if self.scale_for_dpi then
local dpi_scale = Screen:getDPI() / 167
-- rounding off to power of 2 to avoid alias with pow(2, floor(log(x)/log(2))
dpi_scale = math.pow(2, math.max(0, math.floor(math.log(dpi_scale)/0.69)))
if self.scale_factor == nil then
self.scale_factor = 1
end
self.scale_factor = self.scale_factor * dpi_scale
end
-- scale to best fit container : compute scale_factor for that
if self.scale_factor == 0 then
if self.width and self.height then
self.scale_factor = math.min(self.width / bb_w, self.height / bb_h)
logger.dbg("ImageWidget: scale to fit, setting scale_factor to", self.scale_factor)
else
-- no width and height provided (inconsistencies from caller),
self.scale_factor = 1 -- native image size
end
end
-- replace blitbuffer with a resizd one if needed
local new_bb = nil
if self.scale_factor == nil then
-- no scaling, but strech to width and height, only if provided
if self.width and self.height then
logger.dbg("ImageWidget: stretching")
new_bb = self._bb:scale(self.width, self.height)
end
elseif self.scale_factor ~= 1 then
-- scale by scale_factor (not needed if scale_factor == 1)
logger.dbg("ImageWidget: scaling by", self.scale_factor)
new_bb = self._bb:scale(bb_w * self.scale_factor, bb_h * self.scale_factor)
end
if new_bb then
-- We made a new blitbuffer, we need to explicitely free
-- the old one to not leak memory
if self._bb_disposable then
self._bb:free()
end
self._bb = new_bb
self._bb_disposable = true -- new object will have to be freed
bb_w, bb_h = self._bb:getWidth(), self._bb:getHeight()
end
-- deal with positionning
if self.width and self.height then
-- if image is bigger than paint area, allow center_ratio variation
-- around 0.5 so we can pan till image border
if bb_w > self.width then
self._max_off_center_x_ratio = 0.5 - self.width/2 / bb_w
end
if bb_h > self.height then
self._max_off_center_y_ratio = 0.5 - self.height/2 / bb_h
end
-- correct provided center ratio if out limits
if self.center_x_ratio < 0.5 - self._max_off_center_x_ratio then
self.center_x_ratio = 0.5 - self._max_off_center_x_ratio
elseif self.center_x_ratio > 0.5 + self._max_off_center_x_ratio then
self.center_x_ratio = 0.5 + self._max_off_center_x_ratio
end
if self.center_y_ratio < 0.5 - self._max_off_center_y_ratio then
self.center_y_ratio = 0.5 - self._max_off_center_y_ratio
elseif self.center_y_ratio > 0.5 + self._max_off_center_y_ratio then
self.center_y_ratio = 0.5 + self._max_off_center_y_ratio
end
-- set offsets to reflect center ratio, whether oversized or not
self._offset_x = self.center_x_ratio * bb_w - self.width/2
self._offset_y = self.center_y_ratio * bb_h - self.height/2
logger.dbg("ImageWidget: initial offsets", self._offset_x, self._offset_y)
end
-- store final bb's width and height
self._bb_w = bb_w
self._bb_h = bb_h
end
function ImageWidget:getSize()
self:_render()
-- getSize will be used by the widget stack for centering/padding
if not self.width or not self.height then
-- no width/height provided, return bb size to let widget stack do the centering
return Geom:new{ w = self._bb:getWidth(), h = self._bb:getHeight() }
end
-- if width or height provided, return them as is, even if image is smaller
-- and would be centered: we'll do the centering ourselves with offsets
return Geom:new{ w = self.width, h = self.height }
end
function ImageWidget:getScaleFactor()
-- return computed scale_factor, useful if 0 (scale to fit) was used
return self.scale_factor
end
function ImageWidget:getPanByCenterRatio(x, y)
-- returns center ratio (without limits check) we would get with this panBy
local center_x_ratio = (x + self._offset_x + self.width/2) / self._bb_w
local center_y_ratio = (y + self._offset_y + self.height/2) / self._bb_h
return center_x_ratio, center_y_ratio
end
function ImageWidget:panBy(x, y)
-- update center ratio from new offset
self.center_x_ratio = (x + self._offset_x + self.width/2) / self._bb_w
self.center_y_ratio = (y + self._offset_y + self.height/2) / self._bb_h
-- correct new center ratio if out limits
if self.center_x_ratio < 0.5 - self._max_off_center_x_ratio then
self.center_x_ratio = 0.5 - self._max_off_center_x_ratio
elseif self.center_x_ratio > 0.5 + self._max_off_center_x_ratio then
self.center_x_ratio = 0.5 + self._max_off_center_x_ratio
end
if self.center_y_ratio < 0.5 - self._max_off_center_y_ratio then
self.center_y_ratio = 0.5 - self._max_off_center_y_ratio
elseif self.center_y_ratio > 0.5 + self._max_off_center_y_ratio then
self.center_y_ratio = 0.5 + self._max_off_center_y_ratio
end
-- new offsets that reflect this new center ratio
local new_offset_x = self.center_x_ratio * self._bb_w - self.width/2
local new_offset_y = self.center_y_ratio * self._bb_h - self.height/2
-- only trigger screen refresh it we actually pan
if new_offset_x ~= self._offset_x or new_offset_y ~= self._offset_y then
self._offset_x = new_offset_x
self._offset_y = new_offset_y
UIManager:setDirty("all", function()
return "partial", self.dimen
end)
end
-- return new center ratio, so caller can use them later to create a new
-- ImageWidget with a different scale_factor, while keeping center point
return self.center_x_ratio, self.center_y_ratio
end
function ImageWidget:paintTo(bb, x, y)
if self.hide then return end
-- self:_render is called in getSize method
local size = self:getSize()
self.dimen = Geom:new{
x = x, y = y,
w = size.w,
h = size.h
}
logger.dbg("blitFrom", x, y, self._offset_x, self._offset_y, size.w, size.h)
if self.alpha == true then
bb:alphablitFrom(self._bb, x, y, self._offset_x, self._offset_y, size.w, size.h)
else
bb:blitFrom(self._bb, x, y, self._offset_x, self._offset_y, size.w, size.h)
end
if self.invert then
bb:invertRect(x, y, size.w, size.h)
end
if self.dim then
bb:dimRect(x, y, size.w, size.h)
end
end
-- This will normally be called by our WidgetContainer:free()
-- But it SHOULD explicitely be called if we are getting replaced
-- (ie: in some other widget's update()), to not leak memory with
-- BlitBuffer zombies
function ImageWidget:free()
if self._bb and self._bb_disposable and self._bb.free then
self._bb:free()
self._bb = nil
end
-- reset self.scale_factor to its initial value, in case
-- self._render() is called again (happens with iconbutton,
-- avoids x2 x2 x2 if high dpi and icon scaled x8 after 3 calls)
self.scale_factor = self._initial_scale_factor
end
function ImageWidget:onCloseWidget()
-- free when UIManager:close() was called
self:free()
end
return ImageWidget