Files
koreader/frontend/ui/renderimage.lua
poire-z 4bb3999cbc RenderImage: factorize all image rendering and scaling code
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.
2018-04-22 17:00:29 +02:00

201 lines
7.5 KiB
Lua

--[[--
Image rendering module.
]]
local ffi = require("ffi")
local logger = require("logger")
-- Will be loaded when needed
local Mupdf = nil
local Pic = nil
local RenderImage = {}
--- Renders image file as a BlitBuffer with the best renderer
--
-- @string filename image file path
-- @bool[opt=false] want_frames whether to return a list of animated GIF frames
-- @int width requested width
-- @int height requested height
-- @treturn BlitBuffer or list of frames (each a function returning a Blitbuffer)
function RenderImage:renderImageFile(filename, want_frames, width, height)
local file = io.open(filename, "rb")
if not file then
logger.info("could not open image file:", filename)
return
end
local data = file:read("*a")
file:close()
return RenderImage:renderImageData(data, #data, want_frames, width, height)
end
--- Renders image data as a BlitBuffer with the best renderer
--
-- @tparam data string or userdata (pointer) with image bytes
-- @int size size of data
-- @bool[opt=false] want_frames whether to return a list of animated GIF frames
-- @int width requested width
-- @int height requested height
-- @treturn BlitBuffer or list of frames (each a function returning a Blitbuffer)
function RenderImage:renderImageData(data, size, want_frames, width, height)
if not data or not size or size == 0 then
return
end
-- Guess if it is a GIF
local buffer = ffi.cast("unsigned char*", data)
local header = ffi.string(buffer, math.min(4, size))
if header == "GIF8" then
logger.dbg("GIF file provided, renderImageData: using GifLib")
local image = self:renderGifImageDataWithGifLib(data, size, want_frames, width, height)
if image then
return image
end
-- fallback to rendering with MuPDF
end
logger.dbg("renderImageData: using MuPDF")
return self:renderImageDataWithMupdf(data, size, width, height)
end
--- Renders image data as a BlitBuffer with MuPDF
--
-- @tparam data string or userdata (pointer) with image bytes
-- @int size size of data
-- @int width requested width
-- @int height requested height
-- @treturn BlitBuffer
function RenderImage:renderImageDataWithMupdf(data, size, width, height)
if not Mupdf then Mupdf = require("ffi/mupdf") end
local ok, image = pcall(Mupdf.renderImage, data, size, width, height)
logger.dbg("Mupdf.renderImage", ok, image)
if not ok then
logger.info("failed rendering image (mupdf):", image)
return
end
return image
-- Our latest MuPDF does not seem to free() on error anymore.
-- So we can let that job to our caller who knows better.
-- XXX to remove:
-- if type(data) == 'userdata' then
-- if not ok and string.find(image, "could not load image data: unknown image file format") then
-- -- in that case, mupdf seems to have already freed data (see mupdf/source/fitz/image.c:494),
-- -- as doing outselves ffi.C.free(data) would result in a crash with :
-- -- *** Error in `./luajit': double free or corruption (!prev): 0x0000000000e48a40 ***
-- logger.warn("Mupdf says 'unknown image file format', assuming mupdf has already freed image data")
-- else
-- ffi.C.free(data) -- need that explicite clean
-- end
-- end
end
--- Renders image data as a BlitBuffer with GifLib
--
-- @tparam data string or userdata (pointer) with image bytes
-- @int size size of data
-- @bool[opt=false] want_frames whether to also return a list with animated GIF frames
-- @int width requested width
-- @int height requested height
-- @treturn BlitBuffer or list of frames (each a function returning a Blitbuffer)
function RenderImage:renderGifImageDataWithGifLib(data, size, want_frames, width, height)
if not data or not size or size == 0 then
return
end
if not Pic then Pic = require("ffi/pic") end
local ok, gif = pcall(Pic.openGIFDocumentFromData, data, size)
logger.dbg("Pic.openGIFDocumentFromData", ok)
if not ok then
logger.info("failed rendering image (giflib):", gif)
return
end
local nb_frames = gif:getPages()
logger.dbg("GifDocument, nb frames:", nb_frames)
if want_frames and nb_frames > 1 then
-- Returns a regular table, with functions (returning the BlitBuffer)
-- as values. Users will have to check via type() and call them.
-- (our luajit does not support __len via metatable, otherwise we
-- could have used setmetatable to avoid creating all the functions)
local frames = {}
-- As we don't cache the bb we build on the fly, let caller know it
-- will have to free them
frames.image_disposable = true
for i=1, nb_frames do
table.insert(frames, function()
local page = gif:openPage(i)
-- we do not page.close(), so image_bb is not freed
if page and page.image_bb then
return self:scaleBlitBuffer(page.image_bb, width, height)
end
end)
end
-- We can't close our GifDocument as long as we may fetch some
-- frame: we need to delay it till 'frames' is no more used.
frames.gif_close_needed = true
-- Should happen with that, but __gc seems never called...
frames = setmetatable(frames, {
__gc = function()
logger.dbg("frames.gc() called, closing GifDocument")
if frames.gif_close_needed then
gif:close()
frames.gif_close_needed = nil
end
end
})
-- so, also set this method, so that ImageViewer can explicitely
-- call it onClose.
frames.free = function()
logger.dbg("frames.free() called, closing GifDocument")
if frames.gif_close_needed then
gif:close()
frames.gif_close_needed = nil
end
end
return frames
else
local page = gif:openPage(1)
-- we do not page.close(), so image_bb is not freed
if page and page.image_bb then
gif:close()
return self:scaleBlitBuffer(page.image_bb, width, height)
end
gif:close()
end
logger.info("failed rendering image (giflib)")
end
--- Rescales a BlitBuffer to the requested size if needed
--
-- @tparam bb BlitBuffer
-- @int width
-- @int height
-- @bool[opt=true] free_orig_bb free() original bb if scaled
-- @treturn BlitBuffer
function RenderImage:scaleBlitBuffer(bb, width, height, free_orig_bb)
if not width or not height then
logger.dbg("RenderImage:scaleBlitBuffer: no need")
return bb
end
-- Ensure we give integer width and height to MuPDF, to
-- avoid a black 1-pixel line at right and bottom of image
width, height = math.floor(width), math.floor(height)
if bb:getWidth() == width and bb:getHeight() == height then
logger.dbg("RenderImage:scaleBlitBuffer: no need")
return bb
end
logger.dbg("RenderImage:scaleBlitBuffer: scaling")
local scaled_bb
if G_reader_settings:isTrue("legacy_image_scaling") then
-- Uses "simple nearest neighbour scaling"
scaled_bb = bb:scale(width, height)
else
-- Better quality scaling with MuPDF
if not Mupdf then Mupdf = require("ffi/mupdf") end
scaled_bb = Mupdf.scaleBlitBuffer(bb, width, height)
end
if not free_orig_bb == false then
bb:free()
end
return scaled_bb
end
return RenderImage