mirror of https://github.com/koreader/koreader
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.pull/3908/head
parent
7a2bf21434
commit
4bb3999cbc
@ -0,0 +1,200 @@
|
|||||||
|
--[[--
|
||||||
|
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
|
Loading…
Reference in New Issue