add image size metadata to the filesystem

This commit is contained in:
りき萌 2024-11-23 20:43:26 +01:00
parent 5ce9cfc022
commit 9022fb4ce9
13 changed files with 580 additions and 56 deletions

View file

@ -20,7 +20,7 @@ env_logger = "0.10.0"
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
handlebars = "4.3.7"
http-body = "1.0.0"
image = "0.24.8"
image = "0.25.5"
indexmap = { version = "2.2.6", features = ["serde"] }
jotdown = { version = "0.4.1", default-features = false }
log = { workspace = true }

View file

@ -10,7 +10,7 @@ use crate::{
Syntax,
},
import_map::ImportRoot,
vfs::{self, Dir, VPath, VPathBuf},
vfs::{self, Dir, ImageSize, VPath, VPathBuf},
};
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -101,7 +101,7 @@ pub enum Markup {
impl Config {
pub fn autopopulate_emoji(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
if path.extension().is_some_and(is_emoji_file) {
if path.extension().is_some_and(is_image_file) {
if let Some(emoji_name) = path.file_stem() {
if !self.emoji.contains_key(emoji_name) {
self.emoji.insert(emoji_name.to_owned(), path.to_owned());
@ -117,7 +117,7 @@ impl Config {
pub fn autopopulate_pics(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
if path.extension().is_some_and(is_pic_file) {
if path.extension().is_some_and(is_image_file) {
if let Some(pic_name) = path.file_stem() {
let pic_id = pic_name
.split_once('-')
@ -151,6 +151,10 @@ impl Config {
.expect("pics_dir is not anchored anywhere")
}
pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option<ImageSize> {
self.pics.get(id).and_then(|path| pics_dir.image_size(path))
}
/// Loads all syntax definition files.
pub fn load_syntaxes(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
@ -185,10 +189,6 @@ impl Config {
}
}
fn is_emoji_file(extension: &str) -> bool {
matches!(extension, "png" | "svg")
}
fn is_pic_file(extension: &str) -> bool {
pub fn is_image_file(extension: &str) -> bool {
matches!(extension, "png" | "svg" | "jpg" | "jpeg" | "webp")
}

View file

@ -379,19 +379,17 @@ impl<'a> Writer<'a> {
write_attr(&pic_url, out);
out.push('"');
// TODO: Image size derivation.
// let image_size = filename.and_then(|filename| {
// self.renderer
// .config_derived_data
// .image_size(&format!("static/pic/{filename}"))
// });
// if let Some(image_size) = image_size {
// write!(
// out,
// r#" width="{}" height="{}""#,
// image_size.width, image_size.height
// )?;
// }
if let Some(image_size) = self
.renderer
.config
.pic_size(&*self.renderer.dirs.pic, placeholder_pic_id)
{
write!(
out,
r#" width="{}" height="{}""#,
image_size.width, image_size.height
)?;
}
out.push('>');
}
@ -480,6 +478,7 @@ impl<'a> Writer<'a> {
if !src.is_empty() {
out.push_str(r#"" src=""#);
if let SpanLinkType::Unresolved = link_type {
// TODO: Image size.
if let Some(resolved) = self.resolve_link(src) {
write_attr(&resolved, out);
} else {
@ -577,18 +576,13 @@ impl<'a> Writer<'a> {
write_attr(&url, out);
out.push('"');
// TODO: Image size derivation.
// if let Some(image_size) = self
// .renderer
// .config_derived_data
// .image_size(&format!("static/emoji/{vpath}"))
// {
// write!(
// out,
// r#" width="{}" height="{}""#,
// image_size.width, image_size.height
// )?;
// }
if let Some(image_size) = self.renderer.dirs.emoji.image_size(vpath) {
write!(
out,
r#" width="{}" height="{}""#,
image_size.width, image_size.height
)?;
}
out.push('>');

View file

@ -9,7 +9,9 @@ use treehouse::cli::serve::serve;
use treehouse::dirs::Dirs;
use treehouse::generate::{self, Sources};
use treehouse::vfs::asynch::AsyncDir;
use treehouse::vfs::{AnchoredAtExt, Blake3ContentVersionCache, DynDir, ToDynDir, VPathBuf};
use treehouse::vfs::{
AnchoredAtExt, Blake3ContentVersionCache, DynDir, ImageSizeCache, ToDynDir, VPathBuf,
};
use treehouse::vfs::{Cd, PhysicalDir};
use treehouse::{
cli::{
@ -43,6 +45,7 @@ fn vfs_sources() -> anyhow::Result<DynDir> {
);
let root = Blake3ContentVersionCache::new(root);
let root = ImageSizeCache::new(root);
Ok(root.to_dyn())
}

View file

@ -56,6 +56,7 @@ mod content_version_cache;
mod edit;
mod empty;
mod file;
mod image_size_cache;
mod mem_dir;
mod overlay;
mod path;
@ -67,6 +68,7 @@ pub use content_version_cache::*;
pub use edit::*;
pub use empty::*;
pub use file::*;
pub use image_size_cache::*;
pub use mem_dir::*;
pub use overlay::*;
pub use path::*;
@ -77,6 +79,12 @@ pub struct DirEntry {
pub path: VPathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageSize {
pub width: u32,
pub height: u32,
}
pub trait Dir: Debug {
/// List all entries under the provided path.
fn dir(&self, path: &VPath) -> Vec<DirEntry>;
@ -90,6 +98,12 @@ pub trait Dir: Debug {
/// Returns None if there is no content or no version string is available.
fn content_version(&self, path: &VPath) -> Option<String>;
/// Returns the size of the image at the given path, or `None` if the entry is not an image
/// (or its size cannot be known.)
fn image_size(&self, _path: &VPath) -> Option<ImageSize> {
None
}
/// Returns a path relative to `config.site` indicating where the file will be available
/// once served.
///
@ -123,6 +137,10 @@ where
(**self).content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
(**self).image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
(**self).anchor(path)
}
@ -150,6 +168,10 @@ impl Dir for DynDir {
self.arc.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.arc.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.arc.anchor(path)
}

View file

@ -1,6 +1,6 @@
use std::fmt;
use super::{Dir, DirEntry, VPath, VPathBuf};
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
pub struct Anchored<T> {
inner: T,
@ -29,9 +29,17 @@ where
self.inner.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
Some(self.at.join(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
}
}
impl<T> fmt::Debug for Anchored<T>

View file

@ -1,6 +1,6 @@
use std::fmt;
use super::{Dir, DirEntry, EditPath, VPath, VPathBuf};
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
pub struct Cd<T> {
parent: T,
@ -39,6 +39,10 @@ where
self.parent.content(&self.path.join(path))
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.parent.image_size(&self.path.join(path))
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.parent.anchor(&self.path.join(path))
}

View file

@ -2,7 +2,7 @@ use std::fmt::{self, Debug};
use dashmap::DashMap;
use super::{Dir, DirEntry, EditPath, VPath, VPathBuf};
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
pub struct Blake3ContentVersionCache<T> {
inner: T,
@ -42,6 +42,10 @@ where
.clone()
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}

View file

@ -0,0 +1,89 @@
use std::{fmt, io::Cursor};
use anyhow::Context;
use dashmap::DashMap;
use log::{debug, warn};
use crate::config;
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
pub struct ImageSizeCache<T> {
inner: T,
cache: DashMap<VPathBuf, Option<ImageSize>>,
}
impl<T> ImageSizeCache<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
cache: DashMap::new(),
}
}
}
impl<T> ImageSizeCache<T>
where
T: Dir,
{
fn compute_image_size(&self, path: &VPath) -> anyhow::Result<Option<ImageSize>> {
if path.extension().is_some_and(config::is_image_file) {
if let Some(content) = self.content(path) {
let reader = image::ImageReader::new(Cursor::new(content))
.with_guessed_format()
.context("cannot guess image format")?;
let (width, height) = reader.into_dimensions()?;
debug!("image_size({path}) = ({width}, {height})");
return Ok(Some(ImageSize { width, height }));
}
}
Ok(None)
}
}
impl<T> Dir for ImageSizeCache<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.inner.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.cache
.entry(path.to_owned())
.or_insert_with(|| {
self.compute_image_size(path)
.inspect_err(|err| warn!("compute_image_size({path}) failed: {err:?}"))
.ok()
.flatten()
})
.clone()
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
}
}
impl<T> fmt::Debug for ImageSizeCache<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ImageSizeCache({:?})", self.inner)
}
}

View file

@ -1,6 +1,6 @@
use std::{collections::HashMap, fmt};
use super::{Dir, DirEntry, DynDir, EditPath, VPath, VPathBuf};
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf};
pub struct MemDir {
mount_points: HashMap<String, DynDir>,
@ -110,6 +110,17 @@ impl Dir for MemDir {
}
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
match self.resolve(path) {
Resolved::MountPoint {
fs,
fs_path: _,
subpath,
} => fs.image_size(subpath),
Resolved::Root | Resolved::None => None,
}
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
match self.resolve(path) {
Resolved::MountPoint {

View file

@ -1,6 +1,6 @@
use std::fmt;
use super::{Dir, DirEntry, DynDir, EditPath, VPath, VPathBuf};
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf};
pub struct Overlay {
base: DynDir,
@ -34,6 +34,12 @@ impl Dir for Overlay {
.or_else(|| self.base.content_version(path))
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.overlay
.image_size(path)
.or_else(|| self.base.image_size(path))
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.overlay.anchor(path).or_else(|| self.base.anchor(path))
}

View file

@ -1,5 +1,3 @@
use std::sync::Arc;
use treehouse::vfs::{BufferedFile, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf};
const HEWWO: &[u8] = b"hewwo :3";