Я пытаюсь взять файл .cbz
/комикса, прочитать изображения в однобайтовый массив и вернуть его как одно изображение для использования в веб-приложении. В целях тестирования файл .cbz
представляет собой просто RAR-файл с измененным расширением, поэтому вы можете создать свой собственный файл .cbz
, сжимая список файлов .jpg
и переименовывая расширение, а затем пробуя код.
Вот мой текущий код:
package main
import (
"archive/zip"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
func main() {
http.HandleFunc("/rendercbz", handleRenderCBZ)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleRenderCBZ(w http.ResponseWriter, r *http.Request) {
// Example path to .cbz file on the server filesystem
filePath := "/home/my-home-dir/my-comic-book.cbz"
// Open .cbz file from the filesystem
file, err := os.Open(filePath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open file: %v", err), http.StatusInternalServerError)
return
}
defer file.Close()
// Combine images from .cbz file into a single JPEG byte slice
combinedData, err := combineImagesFromCBZ(file)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to combine images from CBZ: %v", err), http.StatusInternalServerError)
return
}
// Serve the combined image as response
w.Header().Set("Content-Type", "image/jpeg")
if _, err := w.Write(combinedData); err != nil {
http.Error(w, fmt.Sprintf("Failed to write image response: %v", err), http.StatusInternalServerError)
return
}
}
func combineImagesFromCBZ(file *os.File) ([]byte, error) {
var combinedData []byte
var imageCount int
// Get file info to determine file size
fileInfo, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %v", err)
}
// Create a zip.Reader from the file
reader, err := zip.NewReader(file, fileInfo.Size())
if err != nil {
return nil, fmt.Errorf("failed to create zip reader: %v", err)
}
// Iterate through each file in the .cbz archive
for _, zipFile := range reader.File {
// Log which image is being processed
log.Printf("Processing image: %s", zipFile.Name)
// Skip files named "thumbnail.jpg" and non-image files
if filepath.Base(zipFile.Name) == "thumbnail.jpg" {
log.Printf("Skipping thumbnail file: %s", zipFile.Name)
continue
}
ext := filepath.Ext(zipFile.Name)
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" {
log.Printf("Skipping non-image file: %s", zipFile.Name)
continue
}
// Open each image file in the .cbz archive
rc, err := zipFile.Open()
if err != nil {
log.Printf("Failed to open file in CBZ archive: %v", err)
continue
}
// Read image file data
fileData, err := io.ReadAll(rc)
rc.Close()
if err != nil {
log.Printf("Failed to read file %s: %v", zipFile.Name, err)
continue
}
// Validate that the image ends with 0xff, 0xd9
if len(fileData) >= 2 && fileData[len(fileData)-2] == 0xff && fileData[len(fileData)-1] == 0xd9 {
// Append image file data to combinedData
combinedData = append(combinedData, fileData...)
imageCount++
} else {
log.Printf("Invalid image ending for file: %s", zipFile.Name)
}
}
// Append a single EOF marker to the end of combinedData
combinedData = append(combinedData, []byte{0xff, 0xd9}...)
// Log the final size of combinedData and the number of valid images found
log.Printf("Final combinedData size = %d", len(combinedData))
log.Printf("Number of valid images found: %d", imageCount)
// Check if we have any data
if len(combinedData) == 0 {
return nil, fmt.Errorf("no valid image data found in CBZ file")
}
return combinedData, nil
}
А вот соответствующий журнал консоли:
> go run main.go
2024/07/10 07:33:44 Processing image: 01.jpg
2024/07/10 07:33:44 Processing image: 02.jpg
2024/07/10 07:33:44 Processing image: 03.jpg
2024/07/10 07:33:44 Processing image: 04.jpg
2024/07/10 07:33:44 Processing image: 05.jpg
2024/07/10 07:33:45 Processing image: 06.jpg
2024/07/10 07:33:45 Processing image: 07.jpg
2024/07/10 07:33:45 Processing image: 08.jpg
2024/07/10 07:33:45 Processing image: 09.jpg
2024/07/10 07:33:45 Processing image: 10.jpg
2024/07/10 07:33:45 Processing image: 11.jpg
2024/07/10 07:33:45 Processing image: ComicInfo.xml
2024/07/10 07:33:45 Skipping non-image file: ComicInfo.xml
2024/07/10 07:33:45 Processing image: thumbnail.jpg
2024/07/10 07:33:45 Skipping thumbnail file: thumbnail.jpg
2024/07/10 07:33:45 Final combinedData size = 35750599
2024/07/10 07:33:45 Number of valid images found: 11
Если я сравню исходное первое изображение в архиве и загруженное изображение из API. Несмотря на то, что API вернул только одно изображение, размер существенно отличается:
Может ли кто-нибудь понять, почему API возвращает только первое изображение в файле .cbz при переходе на localhost:8080/rendercbz
? Похоже, что-то не так со построенным изображением, из-за чего остальные изображения не отображаются должным образом.
Примечание. Я не могу использовать jpeg.Encode, поскольку в файле .cbz содержится много больших изображений, вызывающих эту ошибку: Failed to encode and send image: failed to encode image: jpeg: image is too large to encode
.
У меня заработал выбор одной страницы с помощью параметра запроса ?page=#
, но я хотел узнать, возможно ли создать одно изображение.
Некоторые комиксы имеют формат с длинной прокруткой (вебмульт), поэтому кодирование всех изображений в одно вертикальное изображение может быть хорошим способом его обслуживания. Другой способ — отложенная загрузка каждой страницы по мере загрузки веб-приложения (это тривиально). Но я хотел попробовать и посмотреть, возможно ли это поэкспериментировать.
Я видел веб-комиксы, в которых страницы разбиваются на тонкие горизонтальные ленты... это ужасно, но нет ничего, что нельзя было бы исправить на стороне представления, то есть в браузере. куча элементов IMG и готово.
Да, я тоже это видел, я практически хочу сделать наоборот. Возьмите отдельные изображения и отформатируйте их в одно неподвижное изображение.
минимальный воспроизводимый пример было бы хорошей идеей. удалите все ненужное, например доставку по HTTP (и замените тривиальным кодом, например, записью на диск). ваша цель, похоже, состоит в том, чтобы составить одно большое изображение из меньших, а затем закодировать его в JPEG в памяти (а не на диске).
Да, именно, я отказался от примера с прямым копированием на диск, поскольку хочу, чтобы все операции находились в памяти. Итак, этот пример намеренно приведён таким, какой он есть.
Мне удалось заставить его работать, но, как и ожидалось, он работает очень плохо (возможно, просто потому, что я сейчас использую старое оборудование). Если кто-то еще хочет попробовать или улучшить работу, не стесняйтесь.
package main
import (
"archive/zip"
"bytes"
"fmt"
"image"
"image/draw"
"image/jpeg"
"image/png"
"io"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"golang.org/x/image/webp"
)
const (
port = 8080
cbzDirectory = "./" // Directory where .cbz files are stored
)
func main() {
http.HandleFunc("/webtoon", handleWebtoon)
log.Printf("Server starting on port %d...\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
func handleWebtoon(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
filename := r.URL.Query().Get("file")
if filename == "" {
http.Error(w, "File parameter is required", http.StatusBadRequest)
return
}
if filepath.Ext(filename) != ".cbz" {
http.Error(w, "Invalid file extension. Only .cbz files are allowed", http.StatusBadRequest)
return
}
filePath := filepath.Join(cbzDirectory, filepath.Clean(filename))
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
img, err := CreateWebtoonStrip(filePath)
if err != nil {
log.Printf("Error creating webtoon strip: %v", err)
http.Error(w, fmt.Sprintf("Error processing file: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.png\"", filepath.Base(filename)))
err = streamPNG(w, img)
if err != nil {
log.Printf("Error streaming PNG: %v", err)
http.Error(w, "Error sending image", http.StatusInternalServerError)
return
}
}
func CreateWebtoonStrip(cbzFilePath string) (image.Image, error) {
reader, err := zip.OpenReader(cbzFilePath)
if err != nil {
return nil, fmt.Errorf("error opening CBZ file: %v", err)
}
defer reader.Close()
sort.Slice(reader.File, func(i, j int) bool {
return reader.File[i].Name < reader.File[j].Name
})
var images []image.Image
var totalHeight int
var commonWidth int
for _, file := range reader.File {
if isImageFile(file.Name) {
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("error opening file %s: %v", file.Name, err)
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("error reading file %s: %v", file.Name, err)
}
img, format, err := decodeImage(bytes.NewReader(data))
if err != nil {
log.Printf("Error decoding file %s: %v", file.Name, err)
continue // Skip this file and try the next one
}
log.Printf("Successfully decoded %s as %s", file.Name, format)
width := img.Bounds().Dx()
if commonWidth == 0 {
commonWidth = width
} else if width != commonWidth {
log.Printf("Skipping %s: width %d doesn't match common width %d", file.Name, width, commonWidth)
continue
}
images = append(images, img)
totalHeight += img.Bounds().Dy()
}
}
if len(images) == 0 {
return nil, fmt.Errorf("no valid images found with matching width in the CBZ file")
}
finalImage := image.NewRGBA(image.Rect(0, 0, commonWidth, totalHeight))
currentY := 0
for _, img := range images {
draw.Draw(finalImage, image.Rect(0, currentY, commonWidth, currentY+img.Bounds().Dy()), img, image.Point{}, draw.Src)
currentY += img.Bounds().Dy()
}
return finalImage, nil
}
func isImageFile(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".webp"
}
func decodeImage(r io.Reader) (image.Image, string, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, "", fmt.Errorf("error reading image data: %v", err)
}
// Try decoding as JPEG
img, err := jpeg.Decode(bytes.NewReader(data))
if err == nil {
return img, "jpeg", nil
}
// Try decoding as PNG
img, err = png.Decode(bytes.NewReader(data))
if err == nil {
return img, "png", nil
}
// Try decoding as WebP
img, err = webp.Decode(bytes.NewReader(data))
if err == nil {
return img, "webp", nil
}
return nil, "", fmt.Errorf("unsupported image format")
}
func streamPNG(w io.Writer, img image.Image) error {
encoder := png.Encoder{
CompressionLevel: png.DefaultCompression,
}
return encoder.Encode(w, img)
}
Он ни в коем случае не идеален, но свою работу выполняет. Возьмите любое изображение, ширина которого имеет общий размер, чтобы в конечном изображении не было мертвого пространства, и визуализируйте его как одно МАССИВНОЕ неподвижное изображение.
С учетом вышесказанного я придерживаюсь ленивой загрузки через параметр запроса ?page=#
.
Примечание. Мне пришлось отказаться от использования jepg в качестве типа контента, поскольку максимальный размер JPEG составляет 65535x65535. Когда каждая страница вебтуна имеет высоту около 12 000 и ширину 720, размер быстро нарушается. Итак, мы декодируем JPEG и возвращаем большой PNG. (что, вероятно, не помогает с производительностью)
А еще я загрузил его сюда: https://github.com/alexander-bruun/go-cbz-to-png/tree/main на случай, если люди захотят поститься git clone
.
поэтому файл RAR содержит файлы JPEG. почему бы тебе просто не обслужить их? почему вообще задействовано какое-либо кодирование?