forked from koreader/kindlepdfviewer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
koptreader.lua
614 lines (558 loc) · 18.3 KB
/
koptreader.lua
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
require "unireader"
require "inputbox"
require "koptconfig"
Configurable = {}
function Configurable:hash(sep)
local hash = ""
local excluded = {multi_threads = true,}
for key,value in pairs(self) do
if type(value) == "number" and not excluded[key] then
hash = hash..sep..value
end
end
return hash
end
function Configurable:loadDefaults()
for i=1,#KOPTOptions do
local key = KOPTOptions[i].name
self[key] = KOPTOptions[i].default_value
end
end
function Configurable:loadSettings(settings, prefix)
for key,value in pairs(self) do
if type(value) == "number" then
saved_value = settings:readSetting(prefix..key)
self[key] = (saved_value == nil) and self[key] or saved_value
--Debug("Configurable:loadSettings", "key", key, "saved value", saved_value,"Configurable.key", self[key])
end
end
--Debug("loaded config:", dump(Configurable))
end
function Configurable:saveSettings(settings, prefix)
for key,value in pairs(self) do
if type(value) == "number" then
settings:saveSetting(prefix..key, value)
end
end
end
KOPTReader = UniReader:new{
configurable = {}
}
function KOPTReader:makeContext()
local kc = KOPTContext.new()
kc:setTrim(self.configurable.trim_page)
kc:setWrap(self.configurable.text_wrap)
kc:setIndent(self.configurable.detect_indent)
kc:setRotate(self.configurable.screen_rotation)
kc:setColumns(self.configurable.max_columns)
kc:setDeviceDim(G_width, G_height)
kc:setStraighten(self.configurable.auto_straighten)
kc:setJustification(self.configurable.justification)
kc:setZoom(self.configurable.font_size)
kc:setMargin(self.configurable.page_margin)
kc:setQuality(self.configurable.quality)
kc:setContrast(self.configurable.contrast)
kc:setDefectSize(self.configurable.defect_size)
kc:setLineSpacing(self.configurable.line_spacing)
kc:setWordSpacing(self.configurable.word_spacing)
return kc
end
-- open a PDF/DJVU file and its settings store
function KOPTReader:open(filename)
-- muPDF manages its own cache, set second parameter
-- to the maximum size you want it to grow
self.filename = filename
local file_type = string.lower(string.match(filename, ".+%.([^.]+)") or "")
if file_type == "pdf" then
local ok
ok, self.doc = pcall(pdf.openDocument, filename, self.cache_document_size)
if not ok then
return false, self.doc -- will contain error message
end
if self.doc:needsPassword() then
local password = InputBox:input(G_height-100, 100, "Pass:")
if not password or not self.doc:authenticatePassword(password) then
self.doc:close()
self.doc = nil
return false, "wrong or missing password"
end
-- password wrong or not entered
end
local ok, err = pcall(self.doc.getPages, self.doc)
if not ok then
-- for PDFs, they might trigger errors later when accessing page tree
self.doc:close()
self.doc = nil
return false, "damaged page tree"
end
return true
elseif file_type == "djvu" then
if not validDJVUFile(filename) then
return false, "Not a valid DjVu file"
end
local ok
ok, self.doc = pcall(djvu.openDocument, filename, self.cache_document_size)
if not ok then
return ok, self.doc -- this will be the error message instead
end
return ok
end
end
-- draw original page
function KOPTReader:showOrigPage()
local no = self.pageno
local ok, page = pcall(self.doc.openPage, self.doc, no)
local width, height = G_width, G_height
local pwidth, pheight = page:getSize(self.nulldc)
if not ok then
-- TODO: error handling
return nil
end
local dc = DrawContext.new()
self.globalzoom = width / pwidth
if height / pheight < self.globalzoom then
self.globalzoom = height / pheight
end
dc:setZoom(self.globalzoom)
self.offset_x = 0
self.offset_y = 0
local pagehash = no..'_orig_full_page'
if self.cache[pagehash] ~= nil then
page:close()
local bb = self.cache[pagehash].bb
self.dest_x = 0
self.dest_y = 0
if bb:getWidth() < width then
self.dest_x = (width - (bb:getWidth())) / 2
end
if bb:getHeight() < height then
self.dest_y = (height - (bb:getHeight())) / 2
end
if self.dest_x or self.dest_y then
fb.bb:paintRect(0, 0, width, height, DBACKGROUND_COLOR)
end
fb.bb:blitFrom(self.cache[pagehash].bb, self.dest_x, self.dest_y, 0, 0, width, height)
fb:refresh(1)
return
end
local tile = { x = 0, y = 0, w = width, h = height }
-- can we cache the full page?
local max_cache = self.cache_max_memsize
local fullwidth, fullheight = page:getSize(dc)
if (fullwidth * fullheight / 2) <= max_cache then
-- yes we can, so do this with offset 0, 0
tile.x = 0
tile.y = 0
tile.w = fullwidth
tile.h = fullheight
else
Debug("ERROR not enough memory in cache left, probably a bug.")
return nil
end
self:cacheClaim(tile.w * tile.h / 2);
self.cache[pagehash] = {
x = tile.x,
y = tile.y,
w = tile.w,
h = tile.h,
ttl = self.cache_max_ttl,
size = tile.w * tile.h / 2,
bb = Blitbuffer.new(tile.w, tile.h)
}
--debug ("# new biltbuffer:"..dump(self.cache[pagehash]))
dc:setOffset(-tile.x, -tile.y)
Debug("rendering page", no)
page:draw(dc, self.cache[pagehash].bb, 0, 0, self.render_mode)
page:close()
local bb = self.cache[pagehash].bb
self.dest_x = 0
self.dest_y = 0
if bb:getWidth() < width then
self.dest_x = (width - (bb:getWidth())) / 2
end
if bb:getHeight() < height then
self.dest_y = (height - (bb:getHeight())) / 2
end
if self.dest_x or self.dest_y then
fb.bb:paintRect(0, 0, width, height, DBACKGROUND_COLOR)
end
fb.bb:blitFrom(bb, self.dest_x, self.dest_y, 0, 0, width, height)
fb:refresh(1)
end
function KOPTReader:drawOrCache(no, preCache)
-- our general caching strategy is as follows:
-- #1 goal: we must render the needed area.
-- #2 goal: we render as much of the requested page as we can
-- #3 goal: we render the full page
-- #4 goal: we render next page, too. (TODO)
-- ideally, this should be factored out and only be called when needed (TODO)
local ok, page = pcall(self.doc.openPage, self.doc, no)
local width, height = G_width, G_height
if not ok then
-- TODO: error handling
return nil
end
local kc = self:getContext(page, no, preCache)
self.globalzoom_mode = self.ZOOM_FIT_TO_CONTENT_WIDTH_PAN
-- check if we have relevant cache contents
local bbox = self.cur_bbox
local pagehash = no..self.configurable:hash('_')..'_'..bbox.x0..'_'..bbox.y0..'_'..bbox.x1..'_'..bbox.y1
Debug('page hash', pagehash)
if self.cache[pagehash] ~= nil then
-- we have something in cache
-- requested part is within cached tile
-- ...so properly clean page
page:close()
self.min_offset_x = fb.bb:getWidth() - self.cache[pagehash].w
self.min_offset_y = fb.bb:getHeight() - self.cache[pagehash].h
if(self.min_offset_x > 0) then
self.min_offset_x = 0
end
if(self.min_offset_y > 0) then
self.min_offset_y = 0
end
if self.offset_y <= -201253 then
self.offset_y = self.min_offset_y
end
-- offset_x_in_page & offset_y_in_page is the offset within zoomed page
-- they are always positive.
-- you can see self.offset_x_& self.offset_y as the offset within
-- draw space, which includes the page. So it can be negative and positive.
local offset_x_in_page = -self.offset_x
local offset_y_in_page = -self.offset_y
if offset_x_in_page < 0 then offset_x_in_page = 0 end
if offset_y_in_page < 0 then offset_y_in_page = 0 end
Debug("cached page offset_x",self.offset_x,"offset_y",self.offset_y,"min_offset_x",self.min_offset_x,"min_offset_y",self.min_offset_y)
-- ...and give it more time to live (ttl), except if we're precaching
if not preCache then
self.cache[pagehash].ttl = self.cache_max_ttl
end
-- ...and return blitbuffer plus offset into it
self.cached_pagehash = pagehash
self.cached_offset_x = offset_x_in_page - self.cache[pagehash].x
self.cached_offset_y = offset_y_in_page - self.cache[pagehash].y
return pagehash,
offset_x_in_page - self.cache[pagehash].x,
offset_y_in_page - self.cache[pagehash].y
end
-- okay, we do not have it in cache yet.
-- so render now.
-- start off with the requested area
local use_threads = self.configurable.multi_threads == 1 and true or false
if use_threads and preCache then
Debug("start precache on page", no)
if self.precache_kc ~= nil then
if self.precache_kc:isPreCache() == 1 then
Debug("waiting threaded precache to finish.")
return nil
else
Debug("threaded preCache is finished.")
Debug("current pagehash", pagehash)
Debug("precache pagehash", self.precache_pagehash)
if self.precache_pagehash == pagehash then
Debug("write cache ", self.precache_pagehash)
return self:writeToCache(self.precache_kc, page, self.precache_pagehash, true)
else
self.precache_kc = nil
self.precache_pagehash = nil
Debug("discard cache ", self.precache_pagehash)
return nil
end
end
else
self.precache_kc = kc
self.precache_pagehash = pagehash
self.precache_kc:setPreCache()
page:reflow(self.precache_kc, self.render_mode)
Debug("threaded preCache is returned.")
end
else
if use_threads and self.precache_kc ~= nil then
if self.precache_kc:isPreCache() == 1 and self.cache[self.cached_pagehash] then
InfoMessage:inform("Rendering in background...", DINFO_DELAY, 1, MSG_WARN)
return self.cached_pagehash, self.cached_offset_x, self.cached_offset_y
elseif self.precache_kc:isPreCache() == 0 then -- cache is ready to be written
Debug("write cache", self.precache_pagehash)
self:writeToCache(self.precache_kc, page, self.precache_pagehash, true)
Debug("reflow page", pagehash)
local ok, page = pcall(self.doc.openPage, self.doc, no) -- reopen current page
kc = self:getContext(page, no, true)
page:reflow(kc, self.render_mode)
return self:writeToCache(kc, page, pagehash, false)
else
Debug("ERROR something wrong happens .. why cached page is missing?")
return nil
end
else
--local secs, usecs = util.gettime()
Debug("reflow page", pagehash)
page:reflow(kc, self.render_mode)
--local nsecs, nusecs = util.gettime()
--local dur = (nsecs - secs) * 1000000 + nusecs - usecs
--Debug("Reflow duration:", dur)
--self:logReflowDuration(no, dur)
return self:writeToCache(kc, page, pagehash, false)
end
end
end
function KOPTReader:logReflowDuration(pageno, dur)
local file = io.open("reflowlog.txt", "a+")
if file then
if file:seek("end") == 0 then -- write the header only once
file:write("FILE\tPAGE\tDUR\n")
end
file:write(string.format("%s\t%s\t%s\n", self.filename, pageno, dur))
file:close()
end
end
function KOPTReader:logMemoryUsage(pageno)
local status_file = io.open("/proc/self/status", "r")
local log_file = io.open("reflow_mem_log.txt", "a+")
local data = -1
if status_file then
for line in status_file:lines() do
local s, n
s, n = line:gsub("VmData:%s-(%d+) kB", "%1")
if n ~= 0 then data = tonumber(s) end
if data ~= -1 then break end
end
status_file:close()
end
if log_file then
if log_file:seek("end") == 0 then -- write the header only once
log_file:write("PAGE\tMEM\n")
end
log_file:write(string.format("%s\t%s\n", pageno, data))
log_file:close()
end
end
function KOPTReader:writeToCache(kc, page, pagehash, preCache)
--self:logMemoryUsage(self.pageno)
local tile = { x = 0, y = 0, w = G_width, h = G_height }
-- can we cache the full page?
local max_cache = self.cache_max_memsize
local fullwidth, fullheight = kc:getPageDim()
self.reflow_zoom = kc:getZoom()
Debug("page::reflowPage:", "fullwidth:", fullwidth, "fullheight:", fullheight)
if (fullwidth * fullheight / 2) <= max_cache then
-- yes we can, so do this with offset 0, 0
tile.x = 0
tile.y = 0
tile.w = fullwidth
tile.h = fullheight
else
Debug("ERROR not enough memory in cache left, reflowed page is too large.")
return nil
end
Debug("cache capacity", max_cache, "cache claim", tile.w * tile.h / 2, "cache current", self.cache_current_memsize)
if not self:cacheClaim(tile.w * tile.h / 2) then
Debug("ERROR not enough memory in cache left, cache claim failed.")
return nil
else
Debug("Cache claim succeed.")
end
self.cache[pagehash] = {
x = tile.x,
y = tile.y,
w = tile.w,
h = tile.h,
ttl = self.cache_max_ttl,
size = tile.w * tile.h / 2,
bb = Blitbuffer.new(tile.w, tile.h)
}
--Debug ("new biltbuffer:"..dump(self.cache[pagehash]))
Debug("page::drawReflowedPage", "width:", self.cache[pagehash].w, "height:", self.cache[pagehash].h)
page:rfdraw(kc, self.cache[pagehash].bb)
page:close()
if preCache then
self.precache_kc = nil
end
self.min_offset_x = fb.bb:getWidth() - self.cache[pagehash].w
self.min_offset_y = fb.bb:getHeight() - self.cache[pagehash].h
if(self.min_offset_x > 0) then
self.min_offset_x = 0
end
if(self.min_offset_y > 0) then
self.min_offset_y = 0
end
if self.offset_y <= -201253 then
self.offset_y = self.min_offset_y
end
local offset_x_in_page = -self.offset_x
local offset_y_in_page = -self.offset_y
if offset_x_in_page < 0 then offset_x_in_page = 0 end
if offset_y_in_page < 0 then offset_y_in_page = 0 end
-- return hash and offset within blitbuffer
return pagehash,
offset_x_in_page - tile.x,
offset_y_in_page - tile.y
end
-- get reflow context
function KOPTReader:getContext(page, pnumber, preCache)
local kc = self:makeContext()
local pwidth, pheight = page:getSize(self.nulldc)
local width, height = G_width, G_height
-- rounds down pwidth and pheight to 2 decimals, because page:getUsedBBox() returns only 2 decimals.
-- without it, later check whether to use margins will fail for some documents
pwidth = math.floor(pwidth * 100) / 100
pheight = math.floor(pheight * 100) / 100
Debug("Context preCache:", preCache and "true" or "false")
Debug("Context page::getSize",pwidth,pheight)
local x0, y0, x1, y1 = page:getUsedBBox()
if x0 == 0.01 and y0 == 0.01 and x1 == -0.01 and y1 == -0.01 then
x0 = 0
y0 = 0
x1 = pwidth
y1 = pheight
end
if x1 == 0 then x1 = pwidth end
if y1 == 0 then y1 = pheight end
-- clamp to page BBox
if x0 < 0 then x0 = 0 end
if x1 > pwidth then x1 = pwidth end
if y0 < 0 then y0 = 0 end
if y1 > pheight then y1 = pheight end
if self.bbox.enabled then
Debug("ORIGINAL page::getUsedBBox", x0,y0, x1,y1 )
local bbox = self.bbox[pnumber] -- exact
local oddEven = self:oddEven(pnumber)
if bbox ~= nil then
Debug("bbox from", pnumber)
else
bbox = self.bbox[oddEven] -- odd/even
end
if bbox ~= nil then -- last used up to this page
Debug("bbox from", oddEven)
else
for i = 0,pnumber do
bbox = self.bbox[ pnumber - i ]
if bbox ~= nil then
Debug("bbox from", pnumber - i)
break
end
end
end
if bbox ~= nil then
x0 = bbox["x0"]
y0 = bbox["y0"]
x1 = bbox["x1"]
y1 = bbox["y1"]
end
end
Debug("Context page::getUsedBBox", x0, y0, x1, y1 )
if kc:getTrim() == 1 then
kc:setBBox(0, 0, pwidth, pheight)
else
kc:setBBox(x0, y0, x1, y1)
end
self.cur_bbox = {
["x0"] = x0,
["y0"] = y0,
["x1"] = x1,
["y1"] = y1,
}
Debug("Context cur_bbox", self.cur_bbox)
return kc
end
function KOPTReader:nextView()
local pageno = self.pageno
Debug("nextView offset_y", self.offset_y, "min_offset_y", self.min_offset_y)
if self.offset_y <= self.min_offset_y then
-- hit content bottom, turn to next page top
local numpages = self.doc:getPages()
if pageno < numpages then
self.offset_x = 0
self.offset_y = 0
end
pageno = pageno + 1
else
-- goto next view of current page
self.offset_y = self.offset_y - G_height + self.pan_overlap_vertical
end
return pageno
end
function KOPTReader:prevView()
local pageno = self.pageno
Debug("preView offset_y", self.offset_y, "min_offset_y", self.min_offset_y)
if self.offset_y >= 0 then
-- hit content top, turn to previous page bottom
if pageno > 1 then
self.offset_x = 0
self.offset_y = -2012534
end
pageno = pageno - 1
else
-- goto previous view of current page
self.offset_y = self.offset_y + G_height - self.pan_overlap_vertical
end
return pageno
end
function KOPTReader:setDefaults()
self.show_overlap_enable = DKOPTREADER_SHOW_OVERLAP_ENABLE
self.show_links_enable = DKOPTREADER_SHOW_LINKS_ENABLE
self.comics_mode_enable = DKOPTREADER_COMICS_MODE_ENABLE
self.rtl_mode_enable = DKOPTREADER_RTL_MODE_ENABLE
self.page_mode_enable = DKOPTREADER_PAGE_MODE_ENABLE
end
-- backup global variables from UniReader
function KOPTReader:loadSettings(filename)
UniReader.loadSettings(self,filename)
self.offset_y = self.settings:readSetting("kopt_offset_y") or 0
self.configurable = Configurable
self.configurable:loadDefaults()
--Debug("default configurable:", dump(self.configurable))
self.configurable:loadSettings(self.settings, 'kopt_')
--Debug("loaded configurable:", dump(self.configurable))
-- backup global variable that may be changed in koptreader
self.orig_globalzoom_mode = self.settings:readSetting("globalzoom_mode") or -1
self.orig_dbackground_color = DBACKGROUND_COLOR
DBACKGROUND_COLOR = 0
end
function KOPTReader:saveSpecialSettings()
self.settings:saveSetting("kopt_offset_y", self.offset_y)
self.configurable:saveSettings(self.settings, 'kopt_')
--Debug("saved configurable:", dump(self.configurable))
-- restore global variable from backups
self.settings:saveSetting("globalzoom_mode", self.orig_globalzoom_mode)
DBACKGROUND_COLOR = self.orig_dbackground_color
end
function KOPTReader:init()
self:addAllCommands()
self:adjustCommands()
end
function KOPTReader:redrawWithoutPrecache()
self:show(self.pageno)
end
function KOPTReader:adjustCommands()
self.commands:del(KEY_A, nil,"A")
self.commands:del(KEY_A, MOD_SHIFT, "A")
self.commands:del(KEY_C, nil,"C")
self.commands:del(KEY_U, nil,"U")
self.commands:del(KEY_D, nil,"D")
self.commands:del(KEY_D, MOD_SHIFT, "D")
self.commands:del(KEY_S, nil,"S")
self.commands:del(KEY_S, MOD_SHIFT, "S")
self.commands:del(KEY_F, nil,"F")
self.commands:del(KEY_F, MOD_SHIFT, "F")
self.commands:del(KEY_Z, nil,"Z")
self.commands:del(KEY_Z, MOD_ALT, "Z")
self.commands:del(KEY_Z, MOD_SHIFT, "Z")
self.commands:del(KEY_X, nil,"X")
self.commands:del(KEY_X, MOD_SHIFT, "X")
self.commands:del(KEY_N, nil,"N")
self.commands:del(KEY_N, MOD_SHIFT, "N")
self.commands:del(KEY_L, nil, "L")
self.commands:del(KEY_L, MOD_SHIFT, "L")
self.commands:del(KEY_M, nil, "M")
self.commands:delGroup(MOD_ALT.."< >")
self.commands:delGroup(MOD_SHIFT.."< >")
self.commands:delGroup("vol-/+")
self.commands:del(KEY_P, nil, "P")
self.commands:add({KEY_F,KEY_AA}, nil, "F",
"change koptreader configuration",
function(self)
KOPTConfig:config(self)
self:redrawCurrentPage()
end
)
end