mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
New module RenderImage (alongside existing RenderText) to provides image rendering and scaling facilities. Uses MuPDF, but tries first giflib on GIF. Allows for getting all the frames from an animated GIF.
390 lines
15 KiB
Lua
390 lines
15 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 Cache = require("cache")
|
|
local CacheItem = require("cacheitem")
|
|
local Geom = require("ui/geometry")
|
|
local RenderImage = require("ui/renderimage")
|
|
local Screen = require("device").screen
|
|
local UIManager = require("ui/uimanager")
|
|
local Widget = require("ui/widget/widget")
|
|
local logger = require("logger")
|
|
|
|
-- DPI_SCALE can't change without a restart, so let's compute it now
|
|
local function get_dpi_scale()
|
|
local size_scale = math.min(Screen:getWidth(), Screen:getHeight())/600
|
|
local dpi_scale = Screen:getDPI() / 167
|
|
return math.pow(2, math.max(0, math.log((size_scale+dpi_scale)/2)/0.69))
|
|
end
|
|
local DPI_SCALE = get_dpi_scale()
|
|
|
|
local ImageCache = Cache:new{
|
|
max_memsize = 5*1024*1024, -- 5M 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 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 its 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
|
|
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)
|
|
scale_factor = nil,
|
|
|
|
-- Whether to use former blitbuffer:scale() (default to using MuPDF)
|
|
use_legacy_image_scaling = G_reader_settings:isTrue("legacy_image_scaling"),
|
|
|
|
-- 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
|
|
-- In our use cases for files (icons), we either provide width and height,
|
|
-- or just scale_for_dpi, and scale_factor should stay nil.
|
|
-- Other combinations will result in double scaling, and unexpected results.
|
|
-- We should anyway only give self.width and self.height to renderImageFile(),
|
|
-- and use them in cache hash, when self.scale_factor is nil, when we are sure
|
|
-- we don't need to keep aspect ratio.
|
|
local width, height
|
|
if self.scale_factor == nil then
|
|
width = self.width
|
|
height = self.height
|
|
end
|
|
local hash = "image|"..self.file.."|"..(width or "").."|"..(height or "")
|
|
-- Do the scaling for DPI here, so it can be cached and not re-done
|
|
-- each time in _render()
|
|
local scale_for_dpi_here = false
|
|
if self.scale_for_dpi and DPI_SCALE ~= 1 then
|
|
scale_for_dpi_here = true -- we'll do it before caching
|
|
hash = hash .. "|d"
|
|
self.already_scaled_for_dpi = true -- so we don't do it again in _render()
|
|
end
|
|
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
|
|
self._bb = RenderImage:renderImageFile(self.file, false, width, height)
|
|
if scale_for_dpi_here then
|
|
local bb_w, bb_h = self._bb:getWidth(), self._bb:getHeight()
|
|
self._bb = RenderImage:scaleBlitBuffer(self._bb, math.floor(bb_w * DPI_SCALE), math.floor(bb_h * DPI_SCALE))
|
|
end
|
|
if not self.file_do_cache then
|
|
self._bb_disposable = true -- we made it, we can modify and free it
|
|
else
|
|
self._bb_disposable = false -- don't touch or free a cached _bb
|
|
-- cache this image
|
|
logger.dbg("cache", hash)
|
|
cache = ImageCacheItem:new{ bb = self._bb }
|
|
cache.size = cache.bb.pitch * cache.bb.h * cache.bb:getBpp() / 8
|
|
ImageCache:insert(hash, cache)
|
|
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", self.file and self.file or "data", self.width, self.height)
|
|
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
|
|
-- Allow for easy switch to former scaling via blitbuffer methods
|
|
if self.use_legacy_image_scaling 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
|
|
else
|
|
-- If we use MuPDF for scaling, we can't use bb:rotate() anymore,
|
|
-- as it only flags rotation in the blitbuffer and rotation is dealt
|
|
-- with at painting time. MuPDF does not like such a blitbuffer, and
|
|
-- we get corrupted images when using it for scaling such blitbuffers.
|
|
-- We need to make a real new blitbuffer with rotated content:
|
|
local rot_bb = self._bb:rotatedCopy(self.rotation_angle)
|
|
-- 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 = rot_bb
|
|
self._bb_disposable = true -- new object will have to be freed
|
|
end
|
|
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 and not self.already_scaled_for_dpi then
|
|
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 resized one if needed
|
|
if self.scale_factor == nil then
|
|
-- no scaling, but strech to width and height, only if provided and needed
|
|
if self.width and self.height and (self.width ~= bb_w or self.height ~= bb_h) then
|
|
logger.dbg("ImageWidget: stretching")
|
|
self._bb = RenderImage:scaleBlitBuffer(self._bb, self.width, self.height, self._bb_disposable)
|
|
self._bb_disposable = true -- new bb will have to be freed
|
|
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)
|
|
self._bb = RenderImage:scaleBlitBuffer(self._bb, bb_w * self.scale_factor, bb_h * self.scale_factor, self._bb_disposable)
|
|
self._bb_disposable = true -- new bb will have to be freed
|
|
end
|
|
bb_w, bb_h = self._bb:getWidth(), self._bb:getHeight()
|
|
|
|
-- 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
|