gardesk/garfield / bc72349

Browse files

grid: add PDF thumbnail support

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
bc72349b0b7c025e8853280cd039e75345576210
Parents
1d6e7be
Tree
2aebf8d

2 changed files

StatusFile+-
M garfield/src/core/thumbnail.rs 79 0
M garfield/src/ui/grid_view.rs 10 7
garfield/src/core/thumbnail.rsmodified
@@ -91,6 +91,20 @@ impl ThumbnailLoader {
91
 
91
 
92
     /// Load and scale a thumbnail.
92
     /// Load and scale a thumbnail.
93
     fn load_thumbnail(path: &PathBuf, size: u32) -> Option<Thumbnail> {
93
     fn load_thumbnail(path: &PathBuf, size: u32) -> Option<Thumbnail> {
94
+        // Check if it's a PDF
95
+        let ext = path.extension()
96
+            .and_then(|e| e.to_str())
97
+            .map(|e| e.to_lowercase());
98
+
99
+        if ext.as_deref() == Some("pdf") {
100
+            return Self::load_pdf_thumbnail(path, size);
101
+        }
102
+
103
+        Self::load_image_thumbnail(path, size)
104
+    }
105
+
106
+    /// Load thumbnail from an image file.
107
+    fn load_image_thumbnail(path: &PathBuf, size: u32) -> Option<Thumbnail> {
94
         use image::GenericImageView;
108
         use image::GenericImageView;
95
 
109
 
96
         let img = image::open(path).ok()?;
110
         let img = image::open(path).ok()?;
@@ -118,6 +132,71 @@ impl ThumbnailLoader {
118
         })
132
         })
119
     }
133
     }
120
 
134
 
135
+    /// Load thumbnail from a PDF file (render first page).
136
+    fn load_pdf_thumbnail(path: &PathBuf, size: u32) -> Option<Thumbnail> {
137
+        use cairo::{Context, Format, ImageSurface};
138
+        use poppler::Document;
139
+
140
+        // Load the PDF document
141
+        let uri = format!("file://{}", path.display());
142
+        let doc = Document::from_file(&uri, None).ok()?;
143
+
144
+        if doc.n_pages() == 0 {
145
+            return None;
146
+        }
147
+
148
+        // Get the first page
149
+        let page = doc.page(0)?;
150
+        let (page_width, page_height) = page.size();
151
+
152
+        // Calculate scale to fit within thumbnail size
153
+        let scale = (size as f64 / page_width).min(size as f64 / page_height);
154
+        let width = (page_width * scale) as i32;
155
+        let height = (page_height * scale) as i32;
156
+
157
+        // Create a Cairo surface to render to
158
+        let mut surface = ImageSurface::create(Format::ARgb32, width, height).ok()?;
159
+
160
+        {
161
+            let ctx = Context::new(&surface).ok()?;
162
+
163
+            // Fill with white background
164
+            ctx.set_source_rgb(1.0, 1.0, 1.0);
165
+            ctx.paint().ok()?;
166
+
167
+            // Scale and render the page
168
+            ctx.scale(scale, scale);
169
+            page.render(&ctx);
170
+        }
171
+
172
+        // Get the pixel data
173
+        surface.flush();
174
+        let stride = surface.stride() as usize;
175
+        let data = surface.data().ok()?;
176
+
177
+        // Convert from ARGB (Cairo) to RGBA
178
+        let mut rgba = Vec::with_capacity((width * height * 4) as usize);
179
+        for y in 0..height as usize {
180
+            for x in 0..width as usize {
181
+                let offset = y * stride + x * 4;
182
+                let b = data[offset];
183
+                let g = data[offset + 1];
184
+                let r = data[offset + 2];
185
+                let a = data[offset + 3];
186
+                rgba.push(r);
187
+                rgba.push(g);
188
+                rgba.push(b);
189
+                rgba.push(a);
190
+            }
191
+        }
192
+
193
+        Some(Thumbnail {
194
+            data: rgba,
195
+            width: width as u32,
196
+            height: height as u32,
197
+        })
198
+    }
199
+
121
     /// Get a cached thumbnail, or request loading if not cached.
200
     /// Get a cached thumbnail, or request loading if not cached.
122
     /// Returns Some(thumbnail) if cached, None if loading or not an image.
201
     /// Returns Some(thumbnail) if cached, None if loading or not an image.
123
     pub fn get_or_load(&mut self, path: &PathBuf) -> Option<&Thumbnail> {
202
     pub fn get_or_load(&mut self, path: &PathBuf) -> Option<&Thumbnail> {
garfield/src/ui/grid_view.rsmodified
@@ -1,6 +1,6 @@
1
 //! Grid/icon view for displaying directory contents.
1
 //! Grid/icon view for displaying directory contents.
2
 
2
 
3
-use crate::core::{is_supported_image, EntryType, FileEntry, SortDirection, SortOrder, ThumbnailLoader};
3
+use crate::core::{is_supported_image, is_pdf, EntryType, FileEntry, SortDirection, SortOrder, ThumbnailLoader};
4
 use crate::ui::tab::RenameState;
4
 use crate::ui::tab::RenameState;
5
 use gartk_core::{Color, Modifiers, Point, Rect};
5
 use gartk_core::{Color, Modifiers, Point, Rect};
6
 use gartk_render::{Renderer, Surface, TextAlign, TextStyle};
6
 use gartk_render::{Renderer, Surface, TextAlign, TextStyle};
@@ -205,7 +205,7 @@ impl GridView {
205
         any_loaded
205
         any_loaded
206
     }
206
     }
207
 
207
 
208
-    /// Request thumbnails for visible image files.
208
+    /// Request thumbnails for visible image and PDF files.
209
     pub fn request_visible_thumbnails(&mut self) {
209
     pub fn request_visible_thumbnails(&mut self) {
210
         let visible_count = self.visible_count();
210
         let visible_count = self.visible_count();
211
         let visible_rows = self.visible_rows();
211
         let visible_rows = self.visible_rows();
@@ -216,9 +216,10 @@ impl GridView {
216
         let paths_to_load: Vec<PathBuf> = (start_index..end_index)
216
         let paths_to_load: Vec<PathBuf> = (start_index..end_index)
217
             .filter_map(|i| {
217
             .filter_map(|i| {
218
                 self.visible_entry(i).and_then(|entry| {
218
                 self.visible_entry(i).and_then(|entry| {
219
-                    if is_supported_image(entry.extension().as_deref())
219
+                    let ext_owned = entry.extension();
220
-                        && !self.thumbnail_cache.contains_key(&entry.path)
220
+                    let ext = ext_owned.as_deref();
221
-                    {
221
+                    let is_thumbnailable = is_supported_image(ext) || is_pdf(ext);
222
+                    if is_thumbnailable && !self.thumbnail_cache.contains_key(&entry.path) {
222
                         Some(entry.path.clone())
223
                         Some(entry.path.clone())
223
                     } else {
224
                     } else {
224
                         None
225
                         None
@@ -701,9 +702,11 @@ impl GridView {
701
                 renderer.stroke_rect(cell, theme.selection_background.with_alpha(0.5), 1.0)?;
702
                 renderer.stroke_rect(cell, theme.selection_background.with_alpha(0.5), 1.0)?;
702
             }
703
             }
703
 
704
 
704
-            // Check for cached thumbnail first (for image files)
705
+            // Check for cached thumbnail first (for image and PDF files)
705
             let mut rendered_thumbnail = false;
706
             let mut rendered_thumbnail = false;
706
-            if is_supported_image(entry.extension().as_deref()) {
707
+            let ext_owned = entry.extension();
708
+            let ext = ext_owned.as_deref();
709
+            if is_supported_image(ext) || is_pdf(ext) {
707
                 if let Some(surface) = self.thumbnail_cache.get(&entry.path) {
710
                 if let Some(surface) = self.thumbnail_cache.get(&entry.path) {
708
                     // Render the thumbnail centered in the icon area
711
                     // Render the thumbnail centered in the icon area
709
                     let thumb_w = surface.width();
712
                     let thumb_w = surface.width();