tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

include-cbz.go (8692B)


      1 package main
      2 
      3 import (
      4 	"archive/zip"
      5 	"fmt"
      6 	"image"
      7 	"image/jpeg"
      8 	"io"
      9 	"log"
     10 	"net/http"
     11 	"os"
     12 	"path/filepath"
     13 	"sort"
     14 	"strings"
     15 )
     16 
     17 // generateCBZThumbnail creates a 2x2 collage thumbnail from a CBZ file
     18 func generateCBZThumbnail(cbzPath, uploadDir, filename string) error {
     19 
     20 	thumbDir := filepath.Join(uploadDir, "thumbnails")
     21 
     22 	if err := os.MkdirAll(thumbDir, 0755); err != nil {
     23 		return fmt.Errorf("failed to create thumbnails directory: %v", err)
     24 	}
     25 
     26 	thumbPath := filepath.Join(thumbDir, filename+".jpg")
     27 
     28 	// Open the CBZ (ZIP) file
     29 	r, err := zip.OpenReader(cbzPath)
     30 	if err != nil {
     31 		return fmt.Errorf("failed to open CBZ: %v", err)
     32 	}
     33 	defer r.Close()
     34 
     35 	// Get list of image files from the archive
     36 	var imageFiles []*zip.File
     37 	for _, f := range r.File {
     38 		if !f.FileInfo().IsDir() {
     39 			ext := strings.ToLower(filepath.Ext(f.Name))
     40 			if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".webp" {
     41 				imageFiles = append(imageFiles, f)
     42 			}
     43 		}
     44 	}
     45 
     46 	if len(imageFiles) == 0 {
     47 		return fmt.Errorf("no images found in CBZ")
     48 	}
     49 
     50 	// Sort files by name to get consistent ordering
     51 	sort.Slice(imageFiles, func(i, j int) bool {
     52 		return imageFiles[i].Name < imageFiles[j].Name
     53 	})
     54 
     55 	// Select up to 4 images evenly distributed
     56 	var selectedFiles []*zip.File
     57 	if len(imageFiles) <= 4 {
     58 		selectedFiles = imageFiles
     59 	} else {
     60 		// Pick 4 images evenly distributed through the comic
     61 		step := len(imageFiles) / 4
     62 		for i := 0; i < 4; i++ {
     63 			selectedFiles = append(selectedFiles, imageFiles[i*step])
     64 		}
     65 	}
     66 
     67 	// Load the selected images
     68 	var images []image.Image
     69 	for _, f := range selectedFiles {
     70 		rc, err := f.Open()
     71 		if err != nil {
     72 			log.Printf("Warning: generateCBZThumbnail: failed to open %s: %v", f.Name, err)
     73 			continue
     74 		}
     75 
     76 		img, _, err := image.Decode(rc)
     77 		rc.Close()
     78 		if err != nil {
     79 			log.Printf("Warning: generateCBZThumbnail: failed to decode %s: %v", f.Name, err)
     80 			continue
     81 		}
     82 		images = append(images, img)
     83 	}
     84 
     85 	if len(images) == 0 {
     86 		return fmt.Errorf("failed to decode any images from CBZ")
     87 	}
     88 
     89 	// Create collage
     90 	collage := createCollage(images, 400) // 400px target width
     91 
     92 	// Save as JPEG
     93 	outFile, err := os.Create(thumbPath)
     94 	if err != nil {
     95 		return fmt.Errorf("failed to create thumbnail file: %v", err)
     96 	}
     97 	defer outFile.Close()
     98 
     99 	if err := jpeg.Encode(outFile, collage, &jpeg.Options{Quality: 85}); err != nil {
    100 		return fmt.Errorf("failed to encode JPEG: %v", err)
    101 	}
    102 
    103 	return nil
    104 }
    105 
    106 // createCollage creates a 2x2 grid from up to 4 images
    107 func createCollage(images []image.Image, targetWidth int) image.Image {
    108 	// Calculate cell size (half of target width)
    109 	cellSize := targetWidth / 2
    110 
    111 	// Create output image
    112 	collageImg := image.NewRGBA(image.Rect(0, 0, targetWidth, targetWidth))
    113 
    114 	// Fill with white background
    115 	for y := 0; y < targetWidth; y++ {
    116 		for x := 0; x < targetWidth; x++ {
    117 			collageImg.Set(x, y, image.White)
    118 		}
    119 	}
    120 
    121 	// Positions for the 2x2 grid
    122 	positions := []image.Point{
    123 		{0, 0},                    // Top-left
    124 		{cellSize, 0},             // Top-right
    125 		{0, cellSize},             // Bottom-left
    126 		{cellSize, cellSize},      // Bottom-right
    127 	}
    128 
    129 	// Draw each image
    130 	for i, img := range images {
    131 		if i >= 4 {
    132 			break
    133 		}
    134 
    135 		// Resize image to fit cell
    136 		resized := resizeImage(img, cellSize, cellSize)
    137 
    138 		// Draw at position
    139 		pos := positions[i]
    140 		drawImage(collageImg, resized, pos.X, pos.Y)
    141 	}
    142 
    143 	return collageImg
    144 }
    145 
    146 // resizeImage resizes an image to fit within maxWidth x maxHeight while maintaining aspect ratio
    147 func resizeImage(img image.Image, maxWidth, maxHeight int) image.Image {
    148 	bounds := img.Bounds()
    149 	srcWidth := bounds.Dx()
    150 	srcHeight := bounds.Dy()
    151 
    152 	// Calculate scale to fit
    153 	scaleX := float64(maxWidth) / float64(srcWidth)
    154 	scaleY := float64(maxHeight) / float64(srcHeight)
    155 	scale := scaleX
    156 	if scaleY < scale {
    157 		scale = scaleY
    158 	}
    159 
    160 	newWidth := int(float64(srcWidth) * scale)
    161 	newHeight := int(float64(srcHeight) * scale)
    162 
    163 	// Create new image
    164 	dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
    165 
    166 	// Simple nearest-neighbor scaling
    167 	for y := 0; y < newHeight; y++ {
    168 		for x := 0; x < newWidth; x++ {
    169 			srcX := int(float64(x) / scale)
    170 			srcY := int(float64(y) / scale)
    171 			dst.Set(x, y, img.At(srcX+bounds.Min.X, srcY+bounds.Min.Y))
    172 		}
    173 	}
    174 
    175 	return dst
    176 }
    177 
    178 // drawImage draws src onto dst at the given position
    179 func drawImage(dst *image.RGBA, src image.Image, x, y int) {
    180 	bounds := src.Bounds()
    181 	for dy := 0; dy < bounds.Dy(); dy++ {
    182 		for dx := 0; dx < bounds.Dx(); dx++ {
    183 			dst.Set(x+dx, y+dy, src.At(bounds.Min.X+dx, bounds.Min.Y+dy))
    184 		}
    185 	}
    186 }
    187 
    188 // getCBZImages returns a list of images in a CBZ file
    189 func getCBZImages(cbzPath string) ([]CBZImage, error) {
    190 	r, err := zip.OpenReader(cbzPath)
    191 	if err != nil {
    192 		return nil, err
    193 	}
    194 	defer r.Close()
    195 
    196 	var images []CBZImage
    197 	for i, f := range r.File {
    198 		if !f.FileInfo().IsDir() {
    199 			ext := strings.ToLower(filepath.Ext(f.Name))
    200 			if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".webp" {
    201 				images = append(images, CBZImage{
    202 					Filename: f.Name,
    203 					Index:    i,
    204 				})
    205 			}
    206 		}
    207 	}
    208 
    209 	// Sort by filename for consistent ordering
    210 	sort.Slice(images, func(i, j int) bool {
    211 		return images[i].Filename < images[j].Filename
    212 	})
    213 
    214 	// Re-index after sorting
    215 	for i := range images {
    216 		images[i].Index = i
    217 	}
    218 
    219 	return images, nil
    220 }
    221 
    222 // serveCBZImage extracts and serves a specific image from a CBZ file
    223 func serveCBZImage(w http.ResponseWriter, cbzPath string, imageIndex int) error {
    224 	r, err := zip.OpenReader(cbzPath)
    225 	if err != nil {
    226 		return err
    227 	}
    228 	defer r.Close()
    229 
    230 	// Get sorted list of images
    231 	var imageFiles []*zip.File
    232 	for _, f := range r.File {
    233 		if !f.FileInfo().IsDir() {
    234 			ext := strings.ToLower(filepath.Ext(f.Name))
    235 			if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".webp" {
    236 				imageFiles = append(imageFiles, f)
    237 			}
    238 		}
    239 	}
    240 
    241 	sort.Slice(imageFiles, func(i, j int) bool {
    242 		return imageFiles[i].Name < imageFiles[j].Name
    243 	})
    244 
    245 	if imageIndex < 0 || imageIndex >= len(imageFiles) {
    246 		return fmt.Errorf("image index out of range")
    247 	}
    248 
    249 	targetFile := imageFiles[imageIndex]
    250 
    251 	// Set content type based on extension
    252 	ext := strings.ToLower(filepath.Ext(targetFile.Name))
    253 	switch ext {
    254 	case ".jpg", ".jpeg":
    255 		w.Header().Set("Content-Type", "image/jpeg")
    256 	case ".png":
    257 		w.Header().Set("Content-Type", "image/png")
    258 	case ".webp":
    259 		w.Header().Set("Content-Type", "image/webp")
    260 	}
    261 
    262 	rc, err := targetFile.Open()
    263 	if err != nil {
    264 		return err
    265 	}
    266 	defer rc.Close()
    267 
    268 	_, err = io.Copy(w, rc)
    269 	return err
    270 }
    271 
    272 // cbzViewerHandler handles the CBZ gallery viewer
    273 func cbzViewerHandler(w http.ResponseWriter, r *http.Request) {
    274 	// Extract file ID from URL
    275 	parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/cbz/"), "/")
    276 	if len(parts) < 1 {
    277 		renderError(w, "Invalid CBZ viewer path", http.StatusBadRequest)
    278 		return
    279 	}
    280 
    281 	fileID := parts[0]
    282 
    283 	// Get the file from database
    284 	var f File
    285 	err := db.QueryRow("SELECT id, filename, path, COALESCE(description, '') FROM files WHERE id = ?", fileID).
    286 		Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
    287 	if err != nil {
    288 		log.Printf("Error: cbzViewerHandler: file not found for id=%s: %v", fileID, err)
    289 		renderError(w, "File not found", http.StatusNotFound)
    290 		return
    291 	}
    292 
    293 	cbzPath := filepath.Join(config.UploadDir, f.Path)
    294 
    295 	// Check if requesting a specific image
    296 	if len(parts) >= 3 && parts[1] == "image" {
    297 		imageIndex := 0
    298 		fmt.Sscanf(parts[2], "%d", &imageIndex)
    299 
    300 		if err := serveCBZImage(w, cbzPath, imageIndex); err != nil {
    301 			log.Printf("Error: cbzViewerHandler: failed to serve image index %d from %s: %v", imageIndex, cbzPath, err)
    302 			renderError(w, "Failed to serve image", http.StatusInternalServerError)
    303 		}
    304 		return
    305 	}
    306 
    307 	// Get list of images
    308 	images, err := getCBZImages(cbzPath)
    309 	if err != nil {
    310 		log.Printf("Error: cbzViewerHandler: failed to read CBZ contents at %s: %v", cbzPath, err)
    311 		renderError(w, "Failed to read CBZ contents", http.StatusInternalServerError)
    312 		return
    313 	}
    314 
    315 	// Determine which image to display
    316 	currentIndex := 0
    317 	if len(parts) >= 2 {
    318 		fmt.Sscanf(parts[1], "%d", &currentIndex)
    319 	}
    320 
    321 	if currentIndex < 0 {
    322 		currentIndex = 0
    323 	}
    324 	if currentIndex >= len(images) {
    325 		currentIndex = len(images) - 1
    326 	}
    327 
    328 	// Prepare data for template
    329 	type CBZViewData struct {
    330 		File         File
    331 		Images       []CBZImage
    332 		CurrentIndex int
    333 		TotalImages  int
    334 		HasPrev      bool
    335 		HasNext      bool
    336 	}
    337 
    338 	viewData := CBZViewData{
    339 		File:         f,
    340 		Images:       images,
    341 		CurrentIndex: currentIndex,
    342 		TotalImages:  len(images),
    343 		HasPrev:      currentIndex > 0,
    344 		HasNext:      currentIndex < len(images)-1,
    345 	}
    346 
    347 	pageData := buildPageData(f.Filename, viewData)
    348 
    349 	renderTemplate(w, "cbz_viewer.html", pageData)
    350 }