Compare commits

..

No commits in common. "7ec3a110375ac20846785c3a68256b7eea8b9f61" and "1c82313c985484c69d1cdb686320578972f064b4" have entirely different histories.

5 changed files with 28 additions and 164 deletions

View File

@ -40,7 +40,3 @@ load_image = "2.12.0"
[[bench]] [[bench]]
name = "image" name = "image"
harness = false harness = false
# Build dependencies with release optimizations even in dev mode.
[profile.dev.package."*"]
opt-level = 3

View File

@ -5,21 +5,8 @@ import {
Route, Route,
useParams useParams
} from "react-router-dom"; } from "react-router-dom";
import Random from './rand';
import './App.css';
let CONFIG; import './App.css';
if (process.env.NODE_ENV === 'production') {
CONFIG = {
sleepTimeSeconds: 5 * 60,
showUI: false,
}
} else {
CONFIG = {
sleepTimeSeconds: 10,
showUI: true,
}
}
const IMAGE_CHUNK = 256; const IMAGE_CHUNK = 256;
const roundup = (v, mod) => { const roundup = (v, mod) => {
@ -31,20 +18,6 @@ const roundup = (v, mod) => {
} }
}; };
/**
* Shuffles array in place. ES6 version
* From https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
* @param {Array} a items An array containing the items.
*/
function shuffle(a) {
let rng = new Random(new Date().getDay());
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(rng.nextFloat() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
class Album extends React.Component { class Album extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -52,7 +25,6 @@ class Album extends React.Component {
error: null, error: null,
mediaItems: null, mediaItems: null,
idx: 0, idx: 0,
showUI: props.showUI,
}; };
} }
componentDidMount() { componentDidMount() {
@ -80,44 +52,18 @@ class Album extends React.Component {
h = roundup(h, IMAGE_CHUNK); h = roundup(h, IMAGE_CHUNK);
w = (h/ratio).toFixed(); w = (h/ratio).toFixed();
} }
console.log(`Window size ${window.innerWidth}x${window.innerHeight} with a devicePixelRatio of ${window.devicePixelRatio} for a total size of ${w}x${h}`);
//let w = roundup(window.innerWidth*window.devicePixelRatio, IMAGE_CHUNK); //let w = roundup(window.innerWidth*window.devicePixelRatio, IMAGE_CHUNK);
//let h = roundup(window.innerHeight*window.devicePixelRatio, IMAGE_CHUNK); //let h = roundup(window.innerHeight*window.devicePixelRatio, IMAGE_CHUNK);
let {idx, error, mediaItems, showUI} = this.state; let {idx, error, mediaItems} = this.state;
if (error !== null) { if (error !== null) {
return <h2>Error: {JSON.stringify(error)}</h2>; return <h2>Error: {JSON.stringify(error)}</h2>;
} else if (mediaItems !== null) { } else if (mediaItems !== null) {
let landscapes = mediaItems.filter((mi) => { let numImages = mediaItems.length;
let md = mi.mediaMetadata;
let ratio = md.width/md.height;
return ratio > 1;
});
let portraits = mediaItems.filter((mi) => {
let md = mi.mediaMetadata;
let ratio = md.width/md.height;
return ratio <= 1;
});
console.log(`${landscapes.length} landscape photos`);
console.log(`${portraits.length} portraits photos`);
let photos;
if (ratio > 1) {
console.log('display in landscape mode');
photos = landscapes;
} else {
console.log('display in portrait mode');
photos = portraits;
}
photos = shuffle(photos);
let numImages = photos.length;
let nextIdx = (idx+1)%numImages; let nextIdx = (idx+1)%numImages;
let prevIdx = (numImages+idx-1)%numImages; let prevIdx = (numImages+idx-1)%numImages;
let image = photos[idx]; let image = mediaItems[idx];
let nextImage = photos[nextIdx]; let nextImage = mediaItems[nextIdx];
let prevImage = photos[prevIdx];
let style = { let style = {
height: '100%', height: '100%',
width: '100%', width: '100%',
@ -128,44 +74,26 @@ class Album extends React.Component {
backgroundSize: 'cover', backgroundSize: 'cover',
}; };
let prefetchStyle = { let prefetchStyle = {
// display: 'none'
position: 'absolute', position: 'absolute',
right: 0,
bottom: 0,
width: '25%', width: '25%',
height: '25%', height: '25%',
bottom: 0,
}; };
let leftPrefetchStyle = { console.log(`window.devicePixelRatio ${window.devicePixelRatio}`);
left: 0, return <div style={style} onClick={(e)=>{
...prefetchStyle e.stopPropagation();
}; this.setState({idx: nextIdx})
let rightPrefetchStyle = { }}>
right: 0,
...prefetchStyle
};
let ui;
if (showUI) {
ui = <div>
<img <img
style={leftPrefetchStyle} style={prefetchStyle}
onClick={(e)=>{ onClick={(e)=>{
e.stopPropagation(); e.stopPropagation();
this.setState({idx: prevIdx}) this.setState({idx: prevIdx})
}} }}
src={`/api/image/${prevImage.id}?w=${w}&h=${h}`} alt="prefetch prev" />
<img
style={rightPrefetchStyle}
onClick={(e)=>{
e.stopPropagation();
this.setState({idx: nextIdx})
}}
src={`/api/image/${nextImage.id}?w=${w}&h=${h}`} alt="prefetch next" /> src={`/api/image/${nextImage.id}?w=${w}&h=${h}`} alt="prefetch next" />
</div>; </div>;
}
return <div style={style} onClick={(e)=>{
e.stopPropagation();
this.setState({showUI: !showUI})
}}>
{ ui }
</div>;
} else { } else {
return <h2>Loading...</h2>; return <h2>Loading...</h2>;
} }
@ -213,15 +141,14 @@ class AlbumIndex extends React.Component {
} }
} }
const AlbumRoute = ({showUI}) => { const AlbumRoute = () => {
// We can use the `useParams` hook here to access // We can use the `useParams` hook here to access
// the dynamic pieces of the URL. // the dynamic pieces of the URL.
let { albumId } = useParams(); let { albumId } = useParams();
return <Album album={albumId} showUI={showUI} />; return <Album album={albumId} />;
} }
const App = () => { const App = () => {
let {showUI} = CONFIG;
return <Router> return <Router>
<Switch> <Switch>
<Route exact path="/"> <Route exact path="/">
@ -230,7 +157,7 @@ const App = () => {
</div> </div>
</Route> </Route>
<Route exact path="/:albumId"> <Route exact path="/:albumId">
<AlbumRoute showUI={showUI} /> <AlbumRoute />
</Route> </Route>
</Switch> </Switch>
</Router> </Router>

View File

@ -1,30 +0,0 @@
// From https://gist.github.com/blixt/f17b47c62508be59987b
/**
* Creates a pseudo-random value generator. The seed must be an integer.
*
* Uses an optimized version of the Park-Miller PRNG.
* http://www.firstpr.com.au/dsp/rand31/
*/
function Random(seed) {
console.log(`Seeding prng with ${seed}`);
this._seed = seed % 2147483647;
if (this._seed <= 0) this._seed += 2147483646;
}
/**
* Returns a pseudo-random value between 1 and 2^32 - 2.
*/
Random.prototype.next = function () {
return this._seed = this._seed * 16807 % 2147483647;
};
/**
* Returns a pseudo-random floating point number in range [0, 1).
*/
Random.prototype.nextFloat = function (opt_minOrMax, opt_max) {
// We know that result of next() will be 1 to 2147483646 (inclusive).
return (this.next() - 1) / 2147483646;
};
export default Random;

View File

@ -25,7 +25,7 @@ use rocksdb::IteratorMode;
use rocksdb::DB; use rocksdb::DB;
// Used to ensure DB is invalidated after schema changes. // Used to ensure DB is invalidated after schema changes.
const LIBRARY_GENERATION: &'static str = "14"; const LIBRARY_GENERATION: &'static str = "13";
#[derive(Clone)] #[derive(Clone)]
pub struct Library { pub struct Library {
@ -116,25 +116,6 @@ pub fn resize(
img: &DynamicImage, img: &DynamicImage,
dimensions: (Option<u32>, Option<u32>), dimensions: (Option<u32>, Option<u32>),
filter: FilterType, filter: FilterType,
) -> DynamicImage {
let (w, h) = dimensions;
let (orig_w, orig_h) = img.dimensions();
let (w, h) = match (w, h) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => (w, orig_h * w / orig_w),
(None, Some(h)) => (orig_w * h / orig_h, h),
(None, None) => (orig_w, orig_h),
};
match filter {
FilterType::Builtin(filter) => img.resize(w, h, filter),
FilterType::Nearest => unimplemented!(), //resize_to_fill_nearest(w, h, img),
}
}
pub fn resize_to_fill(
img: &DynamicImage,
dimensions: (Option<u32>, Option<u32>),
filter: FilterType,
) -> DynamicImage { ) -> DynamicImage {
let (w, h) = dimensions; let (w, h) = dimensions;
let (orig_w, orig_h) = img.dimensions(); let (orig_w, orig_h) = img.dimensions();
@ -280,13 +261,11 @@ impl Library {
fn generational_key(generation: &str, key: &str) -> String { fn generational_key(generation: &str, key: &str) -> String {
format!("{}/{}", generation, key) format!("{}/{}", generation, key)
} }
pub fn generate_thumbnail( pub fn generate_thumbnail(
&self, &self,
media_items_id: &str, media_items_id: &str,
dimensions: (Option<u32>, Option<u32>), dimensions: (Option<u32>, Option<u32>),
filter: FilterType, filter: FilterType,
fill: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
match self.original(&media_items_id) { match self.original(&media_items_id) {
None => { None => {
@ -296,11 +275,7 @@ impl Library {
Some(path) => { Some(path) => {
let orig_img = load_image(&path, dimensions.0, dimensions.1)?; let orig_img = load_image(&path, dimensions.0, dimensions.1)?;
//.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; //.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let img = if fill { let img = resize(&orig_img, dimensions, filter);
resize_to_fill(&orig_img, dimensions, filter)
} else {
resize(&orig_img, dimensions, filter)
};
let buf = save_to_jpeg_bytes(&img) let buf = save_to_jpeg_bytes(&img)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(buf) Ok(buf)
@ -311,7 +286,6 @@ impl Library {
&self, &self,
media_items_id: &str, media_items_id: &str,
dimensions: (Option<u32>, Option<u32>), dimensions: (Option<u32>, Option<u32>),
fill: bool,
) -> Option<Vec<u8>> { ) -> Option<Vec<u8>> {
fn cache_key(media_items_id: &str, dimensions: (Option<u32>, Option<u32>)) -> String { fn cache_key(media_items_id: &str, dimensions: (Option<u32>, Option<u32>)) -> String {
let dim = match dimensions { let dim = match dimensions {
@ -326,15 +300,18 @@ impl Library {
let db = self.cache_db.clone(); let db = self.cache_db.clone();
match db.get(key.as_bytes()) { match db.get(key.as_bytes()) {
// Cache hit, return bytes as-is. // Cache hit, return bytes as-is.
Ok(Some(bytes)) => Some(bytes), Ok(Some(bytes)) => {
info!("cache HIT {}", key);
Some(bytes)
}
// Cache miss, fill cache and return. // Cache miss, fill cache and return.
Ok(None) => { Ok(None) => {
info!("cache MISS {}", key); info!("cache MISS {}", key);
let bytes = match self.generate_thumbnail( let bytes = match self.generate_thumbnail(
media_items_id, media_items_id,
dimensions, dimensions,
FilterType::Builtin(imageops::FilterType::Lanczos3), FilterType::Nearest,
fill, //FilterType::Builtin(imageops::FilterType::Lanczos3),
) { ) {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(e) => { Err(e) => {

View File

@ -73,7 +73,6 @@ fn album(lib: Library, id: String) -> Result<impl warp::Reply, warp::Rejection>
struct ImageParams { struct ImageParams {
w: Option<u32>, w: Option<u32>,
h: Option<u32>, h: Option<u32>,
fill: Option<bool>,
} }
fn image( fn image(
@ -82,11 +81,7 @@ fn image(
params: ImageParams, params: ImageParams,
) -> Result<impl warp::Reply, warp::Rejection> { ) -> Result<impl warp::Reply, warp::Rejection> {
// TODO(wathiede): add caching headers. // TODO(wathiede): add caching headers.
match lib.thumbnail( match lib.thumbnail(&media_items_id, (params.w, params.h)) {
&media_items_id,
(params.w, params.h),
params.fill.unwrap_or(false),
) {
None => { None => {
warn!("Couldn't find original {}", &media_items_id); warn!("Couldn't find original {}", &media_items_id);
Err(warp::reject::not_found()) Err(warp::reject::not_found())
@ -126,8 +121,7 @@ pub fn run(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
// Fallback, always keep this last. // Fallback, always keep this last.
let api = api.or(index); let api = api.or(index);
//let api = api.with(warp::log("photosync")); let api = api.with(warp::log("photosync"));
// We don't want metrics & heath checking filling up the logs, so we add this handler after // We don't want metrics & heath checking filling up the logs, so we add this handler after
// wrapping with the log filter. // wrapping with the log filter.
let routes = metrics().or(api); let routes = metrics().or(api);