Skip to content

Commit

Permalink
Implement a line preserving fit to width zoom mode
Browse files Browse the repository at this point in the history
  • Loading branch information
baskerville committed Dec 7, 2018
1 parent 6ebc25b commit 713a623
Show file tree
Hide file tree
Showing 16 changed files with 906 additions and 258 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Documentation: [GUIDE](doc/GUIDE.md), [MANUAL](doc/MANUAL.md) and [BUILD](doc/BU
- Hierarchical categories.
- The metadata for each document is read from a single JSON file.
- Crop margins of non-reflowable documents.
- Continuous fit-to-width zoom mode with line preserving cuts.

[![Tn01](artworks/thumbnail01.png)](artworks/screenshot01.png) [![Tn02](artworks/thumbnail02.png)](artworks/screenshot02.png) [![Tn03](artworks/thumbnail03.png)](artworks/screenshot03.png)

Expand Down
6 changes: 6 additions & 0 deletions doc/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ Gestures by region:

Swipe west/east to go to the next/previous page.

Swipe north/south to scroll the page stream when the zoom mode is fit-to-width.

Rotate to change the screen orientation (one finger is the center, the other describes the desired rotation with a circular motion around the center: the two fingers should land and take off simultaneously).

Spread horizontally to switch the zoom mode to fit-to-width.

## Bottom bar

Hold the next/previous page icon to go the next/previous chapter.
Expand Down
1 change: 0 additions & 1 deletion doc/TODO.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
- Fit to width.
- Dictionary.
- Metadata view.
- Pocket articles.
Expand Down
3 changes: 2 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,8 +689,9 @@ pub fn run() -> Result<(), Error> {
let fb_rect = Rectangle::from(dims);
if context.display.dims != dims {
context.display.dims = dims;
handle_event(view.as_mut(), &Event::RotateView(n), &tx, &mut bus, &mut context);
view.resize(fb_rect, &tx, &mut context);
} else {
tx.send(Event::Render(fb.rect(), UpdateMode::Full)).unwrap();
}
}
},
Expand Down
58 changes: 33 additions & 25 deletions src/document/djvu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,25 +134,11 @@ impl Document for DjvuDocument {
}

fn words(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
unsafe {
let index = self.resolve_location(loc)?;
let page = self.page(index)?;
let height = page.height() as i32;
let grain = CString::new("word").unwrap();
let mut exp = ddjvu_document_get_pagetext(self.doc, index as libc::c_int, grain.as_ptr());
while exp == MINIEXP_DUMMY {
self.ctx.handle_message();
exp = ddjvu_document_get_pagetext(self.doc, index as libc::c_int, grain.as_ptr());
}
if exp == MINIEXP_NIL {
None
} else {
let mut words = Vec::new();
Self::walk_words(exp, height, &mut words);
ddjvu_miniexp_release(self.doc, exp);
Some((words, index))
}
}
self.text(loc, b"word")
}

fn lines(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
self.text(loc, b"line")
}

fn links(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
Expand Down Expand Up @@ -186,7 +172,7 @@ impl Document for DjvuDocument {
let y_max = height - (miniexp_nth(2, area) as i32 >> 2);
let r_width = miniexp_nth(3, area) as i32 >> 2;
let r_height = miniexp_nth(4, area) as i32 >> 2;
rect![x_min, y_max - r_height, x_min + r_width, y_max]
bndr![x_min as f32, (y_max - r_height) as f32, (x_min + r_width) as f32, y_max as f32]
};
result.push(BoundedText { text, rect });
}
Expand Down Expand Up @@ -266,28 +252,50 @@ impl DjvuDocument {
}
}

fn walk_words(exp: *mut MiniExp, height: i32, words: &mut Vec<BoundedText>) {
fn text(&mut self, loc: Location, kind: &[u8]) -> Option<(Vec<BoundedText>, usize)> {
unsafe {
let index = self.resolve_location(loc)?;
let page = self.page(index)?;
let height = page.height() as i32;
let grain = CString::new(kind).unwrap();
let mut exp = ddjvu_document_get_pagetext(self.doc, index as libc::c_int, grain.as_ptr());
while exp == MINIEXP_DUMMY {
self.ctx.handle_message();
exp = ddjvu_document_get_pagetext(self.doc, index as libc::c_int, grain.as_ptr());
}
if exp == MINIEXP_NIL {
None
} else {
let mut words = Vec::new();
Self::walk_text(exp, height, kind, &mut words);
ddjvu_miniexp_release(self.doc, exp);
Some((words, index))
}
}
}

fn walk_text(exp: *mut MiniExp, height: i32, kind: &[u8], data: &mut Vec<BoundedText>) {
unsafe {
let len = miniexp_length(exp);
let rect = {
let x_min = miniexp_nth(1, exp) as i32 >> 2;
let y_max = height - (miniexp_nth(2, exp) as i32 >> 2);
let x_max = miniexp_nth(3, exp) as i32 >> 2;
let y_min = height - (miniexp_nth(4, exp) as i32 >> 2);
rect![x_min, y_min, x_max, y_max]
bndr![x_min as f32, y_min as f32, x_max as f32, y_max as f32]
};
let grain = {
let raw = miniexp_to_name(miniexp_nth(0, exp));
CStr::from_ptr(raw).to_bytes()
};
if grain == b"word" && miniexp_stringp(miniexp_nth(5, exp)) == 1 {
if grain == kind && miniexp_stringp(miniexp_nth(5, exp)) == 1 {
let raw = miniexp_to_str(miniexp_nth(5, exp));
let c_str = CStr::from_ptr(raw);
let text = c_str.to_string_lossy().into_owned();
words.push(BoundedText { rect, text });
data.push(BoundedText { rect, text });
} else {
for i in 5..len {
Self::walk_words(miniexp_nth(i, exp), height, words);
Self::walk_text(miniexp_nth(i, exp), height, kind, data);
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/document/epub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1774,7 +1774,7 @@ impl Document for EpubDocument {
DrawCommand::Text(TextCommand { text, rect, .. }) => {
Some(BoundedText {
text: text.clone(),
rect: *rect,
rect: (*rect).into(),
})
},
_ => None,
Expand All @@ -1783,6 +1783,10 @@ impl Document for EpubDocument {
})
}

fn lines(&mut self, _loc: Location) -> Option<(Vec<BoundedText>, usize)> {
None
}

fn links(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
if self.spine.is_empty() {
return None;
Expand All @@ -1799,7 +1803,7 @@ impl Document for EpubDocument {
DrawCommand::Image(ImageCommand { uri, rect, .. }) if uri.is_some() => {
Some(BoundedText {
text: uri.clone().unwrap(),
rect: *rect,
rect: (*rect).into(),
})
},
_ => None,
Expand Down
5 changes: 3 additions & 2 deletions src/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use fnv::FnvHashSet;
use isbn::Isbn;
use unicode_normalization::UnicodeNormalization;
use unicode_normalization::char::{is_combining_mark};
use geom::{Rectangle, CycleDir};
use geom::{Boundary, CycleDir};
use document::djvu::DjvuOpener;
use document::pdf::PdfOpener;
use document::epub::EpubDocument;
Expand All @@ -32,7 +32,7 @@ pub enum Location<'a> {
#[derive(Debug, Clone)]
pub struct BoundedText {
pub text: String,
pub rect: Rectangle,
pub rect: Boundary,
}

#[derive(Debug, Clone)]
Expand All @@ -54,6 +54,7 @@ pub trait Document: Send+Sync {

fn toc(&mut self) -> Option<Vec<TocEntry>>;
fn words(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)>;
fn lines(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)>;
fn links(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)>;

fn pixmap(&mut self, loc: Location, scale: f32) -> Option<(Pixmap, usize)>;
Expand Down
63 changes: 42 additions & 21 deletions src/document/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,14 @@ use failure::Error;
use super::{Document, Location, BoundedText, TocEntry};
use unit::pt_to_px;
use framebuffer::Pixmap;
use geom::Rectangle;

impl Into<FzRect> for Rectangle {
fn into(self) -> FzRect {
FzRect {
x0: self.min.y as libc::c_float,
y0: self.min.x as libc::c_float,
x1: (self.max.x - 1) as libc::c_float,
y1: (self.max.y - 1) as libc::c_float,
}
}
}
use geom::Boundary;

impl Into<Rectangle> for FzRect {
fn into(self) -> Rectangle {
rect![
self.x0.floor() as i32,
self.y0.floor() as i32,
self.x1.ceil() as i32,
self.y1.ceil() as i32,
]
impl Into<Boundary> for FzRect {
fn into(self) -> Boundary {
Boundary {
min: vec2!(self.x0, self.y0),
max: vec2!(self.x1, self.y1),
}
}
}

Expand Down Expand Up @@ -212,6 +199,11 @@ impl Document for PdfDocument {
self.page(index).and_then(|page| page.words()).map(|words| (words, index))
}

fn lines(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
let index = self.resolve_location(loc)?;
self.page(index).and_then(|page| page.lines()).map(|lines| (lines, index))
}

fn links(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
let index = self.resolve_location(loc)?;
self.page(index).and_then(|page| page.links()).map(|links| (links, index))
Expand Down Expand Up @@ -250,6 +242,35 @@ impl Document for PdfDocument {
}

impl<'a> PdfPage<'a> {
pub fn lines(&self) -> Option<Vec<BoundedText>> {
unsafe {
let mut lines = Vec::new();
let tp = mp_new_stext_page_from_page(self.ctx.0, self.page, ptr::null());
if tp.is_null() {
return None;
}
let mut block = (*tp).first_block;

while !block.is_null() {
if (*block).kind == FZ_PAGE_BLOCK_TEXT {
let text_block = (*block).u.text;
let mut line = text_block.first_line;

while !line.is_null() {
let rect = (*line).bbox.clone().into();
lines.push(BoundedText { text: "".to_string(), rect });
line = (*line).next;
}
}

block = (*block).next;
}

fz_drop_stext_page(self.ctx.0, tp);
Some(lines)
}
}

pub fn words(&self) -> Option<Vec<BoundedText>> {
unsafe {
let mut words = Vec::new();
Expand Down Expand Up @@ -351,7 +372,7 @@ impl<'a> PdfPage<'a> {
}
}

pub fn boundary_box(&self) -> Option<Rectangle> {
pub fn boundary_box(&self) -> Option<Boundary> {
unsafe {
let mut rect = FzRect::default();
let dev = fz_new_bbox_device(self.ctx.0, &mut rect);
Expand Down
37 changes: 26 additions & 11 deletions src/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ use font::Fonts;
use app::Context;

pub const APP_NAME: &str = "Plato";
const DEFAULT_ROTATION: i8 = 1;

const CLOCK_REFRESH_INTERVAL: Duration = Duration::from_secs(60);

Expand Down Expand Up @@ -176,7 +177,7 @@ impl Framebuffer for WindowCanvas {
}

fn rotation(&self) -> i8 {
1
DEFAULT_ROTATION
}

fn set_rotation(&mut self, n: i8) -> Result<(u32, u32), Error> {
Expand Down Expand Up @@ -231,10 +232,8 @@ pub fn run() -> Result<(), Error> {
}
});

let fb_rect = fb.rect();

let mut history: Vec<Box<View>> = Vec::new();
let mut view: Box<View> = Box::new(Home::new(fb_rect, &tx, &mut context)?);
let mut view: Box<View> = Box::new(Home::new(fb.rect(), &tx, &mut context)?);

let mut updating = FnvHashMap::default();

Expand All @@ -249,8 +248,8 @@ pub fn run() -> Result<(), Error> {

println!("{} is running on a Kobo {}.", APP_NAME,
CURRENT_DEVICE.model);
println!("The framebuffer resolution is {} by {}.", fb_rect.width(),
fb_rect.height());
println!("The framebuffer resolution is {} by {}.", fb.rect().width(),
fb.rect().height());

let mut bus = VecDeque::with_capacity(4);

Expand Down Expand Up @@ -326,22 +325,39 @@ pub fn run() -> Result<(), Error> {
}
},
Event::Open(info) => {
let rotation = context.display.rotation;
if let Some(n) = info.reader.as_ref().and_then(|r| r.rotation) {
if n != rotation {
if let Ok(dims) = fb.set_rotation(n) {
context.display.rotation = n;
context.display.dims = dims;
}
}
}
let info2 = info.clone();
if let Some(r) = Reader::new(fb_rect, *info, &tx, &mut context) {
if let Some(r) = Reader::new(fb.rect(), *info, &tx, &mut context) {
history.push(view as Box<View>);
view = Box::new(r) as Box<View>;
} else {
handle_event(view.as_mut(), &Event::Invalid(info2), &tx, &mut bus, &mut context);
}
},
Event::OpenToc(ref toc, current_page, next_page) => {
let r = Reader::from_toc(fb_rect, toc, current_page, next_page, &tx, &mut context);
let r = Reader::from_toc(fb.rect(), toc, current_page, next_page, &tx, &mut context);
history.push(view as Box<View>);
view = Box::new(r) as Box<View>;
},
Event::Back => {
if let Some(v) = history.pop() {
view = v;
if view.is::<Home>() {
if context.display.rotation % 2 != 1 {
if let Ok(dims) = fb.set_rotation(DEFAULT_ROTATION) {
context.display.rotation = DEFAULT_ROTATION;
context.display.dims = dims;
}
}
}
view.handle_event(&Event::Reseed, &tx, &mut bus, &mut context);
}
},
Expand Down Expand Up @@ -388,20 +404,19 @@ pub fn run() -> Result<(), Error> {
let fb_rect = Rectangle::from(dims);
if context.display.dims != dims {
context.display.dims = dims;
handle_event(view.as_mut(), &Event::RotateView(n), &tx, &mut bus, &mut context);
view.resize(fb_rect, &tx, &mut context);
}
}
},
Event::Select(EntryId::ToggleInverted) => {
fb.toggle_inverted();
context.inverted = !context.inverted;
tx.send(Event::Render(fb_rect, UpdateMode::Gui)).unwrap();
tx.send(Event::Render(fb.rect(), UpdateMode::Gui)).unwrap();
},
Event::Select(EntryId::ToggleMonochrome) => {
fb.toggle_monochrome();
context.monochrome = !context.monochrome;
tx.send(Event::Render(fb_rect, UpdateMode::Gui)).unwrap();
tx.send(Event::Render(fb.rect(), UpdateMode::Gui)).unwrap();
},
Event::Select(EntryId::TakeScreenshot) => {
let name = Local::now().format("screenshot-%Y%m%d_%H%M%S.png");
Expand Down
Loading

0 comments on commit 713a623

Please sign in to comment.