tagliatelle

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

include-cbz.go (8749B)


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