1
0

Revert "chore(attachments): refactor building image preview"

This reverts commit e70f5bcce39ae19060be62aec8b75f75c26d1b9e.
This commit is contained in:
kolaente 2024-09-29 11:54:20 +02:00
parent 83b27d813a
commit 17618301bc
No known key found for this signature in database
GPG Key ID: F40E70337AB24C9B
7 changed files with 26 additions and 131 deletions

View File

@ -180,7 +180,7 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import {canPreview} from '@/models/attachment' import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
@ -274,6 +274,10 @@ async function viewOrDownload(attachment: IAttachment) {
} }
} }
function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
function copyUrl(attachment: IAttachment) { function copyUrl(attachment: IAttachment) {

View File

@ -22,7 +22,7 @@
import {ref, shallowReactive, watchEffect} from 'vue' import {ref, shallowReactive, watchEffect} from 'vue'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import {canPreview} from '@/models/attachment' import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
const props = defineProps<{ const props = defineProps<{
modelValue?: IAttachment modelValue?: IAttachment
@ -33,9 +33,13 @@ const blobUrl = ref<string | undefined>(undefined)
watchEffect(async () => { watchEffect(async () => {
if (props.modelValue && canPreview(props.modelValue)) { if (props.modelValue && canPreview(props.modelValue)) {
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue, PREVIEW_SIZE.MD) blobUrl.value = await attachmentService.getBlobUrl(props.modelValue)
} }
}) })
function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -165,7 +165,7 @@ async function maybeDownloadCoverImage() {
} }
const attachmentService = new AttachmentService() const attachmentService = new AttachmentService()
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment, PREVIEW_SIZE.LG) coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} }
watch( watch(

View File

@ -7,10 +7,6 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
export const SUPPORTED_IMAGE_SUFFIX = ['.jpg', '.png', '.bmp', '.gif'] export const SUPPORTED_IMAGE_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
export function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment { export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {
id = 0 id = 0
taskId = 0 taskId = 0

View File

@ -37,13 +37,8 @@ export default class AttachmentService extends AbstractService<IAttachment> {
return data return data
} }
getBlobUrl(model: IAttachment, size?: PREVIEW_SIZE) { getBlobUrl(model: IAttachment) {
let mainUrl = '/tasks/' + model.taskId + '/attachments/' + model.id return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id)
if (size !== undefined) {
mainUrl += `?preview_size=${size}`
}
return AbstractService.prototype.getBlobUrl.call(this, mainUrl)
} }
async download(model: IAttachment) { async download(model: IAttachment) {

View File

@ -17,21 +17,14 @@
package models package models
import ( import (
"bytes"
"image"
"image/png"
"io" "io"
"strconv"
"time" "time"
"code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/web" "code.vikunja.io/api/pkg/web"
"github.com/disintegration/imaging"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -192,110 +185,6 @@ func (ta *TaskAttachment) ReadAll(s *xorm.Session, _ web.Auth, _ string, page in
return attachments, len(attachments), numberOfTotalItems, err return attachments, len(attachments), numberOfTotalItems, err
} }
func cacheKeyForTaskAttachmentPreview(id int64, size PreviewSize) string {
return "task_attachment_preview_" + strconv.FormatInt(id, 10) + "_size_" + string(size)
}
func (ta *TaskAttachment) GetPreviewFromCache(previewSize PreviewSize) []byte {
cacheKey := cacheKeyForTaskAttachmentPreview(ta.ID, previewSize)
var cached []byte
exists, err := keyvalue.GetWithValue(cacheKey, &cached)
// If the preview is not cached, return nil
if err != nil || !exists || cached == nil {
return nil
}
return cached
}
type PreviewSize string
const (
PreviewSizeUnknown PreviewSize = "unknown"
PreviewSmall PreviewSize = "sm"
PreviewMedium PreviewSize = "md"
PreviewLarge PreviewSize = "lg"
PreviewExtraLarge PreviewSize = "xl"
)
func (previewSize PreviewSize) GetSize() int {
switch previewSize {
case PreviewSmall:
return 100
case PreviewMedium:
return 200
case PreviewLarge:
return 400
case PreviewExtraLarge:
return 800
case PreviewSizeUnknown:
return 0
default:
return 200
}
}
func GetPreviewSizeFromString(size string) PreviewSize {
switch size {
case "sm":
return PreviewSmall
case "md":
return PreviewMedium
case "lg":
return PreviewLarge
case "xl":
return PreviewExtraLarge
}
return PreviewSizeUnknown
}
func resizeImage(img image.Image, width int) *image.NRGBA {
resizedImg := imaging.Resize(img, width, 0, imaging.Lanczos)
log.Debugf(
"Resized attachment image from %vx%v to %vx%v for a preview",
img.Bounds().Size().X,
img.Bounds().Size().Y,
resizedImg.Bounds().Size().X,
resizedImg.Bounds().Size().Y,
)
return resizedImg
}
func (ta *TaskAttachment) GenerateAndSavePreviewToCache(previewSize PreviewSize) []byte {
img, _, err := image.Decode(ta.File.File)
if err != nil {
return nil
}
// Scale down the image to a minimum size
resizedImg := resizeImage(img, previewSize.GetSize())
// Get the raw bytes of the resized image
buf := &bytes.Buffer{}
if err := png.Encode(buf, resizedImg); err != nil {
return nil
}
previewImage, err := io.ReadAll(buf)
if err != nil {
return nil
}
// Store the preview image in the cache
cacheKey := cacheKeyForTaskAttachmentPreview(ta.ID, previewSize)
err = keyvalue.Put(cacheKey, previewImage)
if err != nil {
return nil
}
log.Infof("Attachment image preview for task attachment %v of size %v created and cached", ta.ID, previewSize)
return previewImage
}
// Delete removes an attachment // Delete removes an attachment
// @Summary Delete an attachment // @Summary Delete an attachment
// @Description Delete an attachment. // @Description Delete an attachment.

View File

@ -117,7 +117,8 @@ func UploadTaskAttachment(c echo.Context) error {
// @Produce octet-stream // @Produce octet-stream
// @Param id path int true "Task ID" // @Param id path int true "Task ID"
// @Param attachmentID path int true "Attachment ID" // @Param attachmentID path int true "Attachment ID"
// @Param preview_size query string false "The size of the preview image. Can be sm = 100px, md = 200px, lg = 400px or xl = 800px. If provided, a preview image will be returned if the attachment is an image." // @Param preview query string false "If set to true, a preview image will be returned if the attachment is an image."
// @Param size query string false "The size of the preview image. Can be sm = 100px, md = 200px, lg = 400px or xl = 800px."
// @Security JWTKeyAuth // @Security JWTKeyAuth
// @Success 200 {file} blob "The attachment file." // @Success 200 {file} blob "The attachment file."
// @Failure 403 {object} models.Message "No access to this task." // @Failure 403 {object} models.Message "No access to this task."
@ -156,9 +157,15 @@ func GetTaskAttachment(c echo.Context) error {
return handler.HandleHTTPError(err) return handler.HandleHTTPError(err)
} }
// Reading the 'preview' query parameter
preview := c.QueryParam("preview") == "true"
previewSize := models.PreviewSize(c.QueryParam("size"))
if previewSize == "" {
previewSize = models.PreviewMedium
}
// If the preview query parameter is set and the preview was already generated and cached, return the cached preview image // If the preview query parameter is set and the preview was already generated and cached, return the cached preview image
previewSize := models.GetPreviewSizeFromString(c.QueryParam("preview_size")) if preview && strings.HasPrefix(taskAttachment.File.Mime, "image") {
if previewSize != models.PreviewSizeUnknown && strings.HasPrefix(taskAttachment.File.Mime, "image") {
previewFileBytes := taskAttachment.GetPreviewFromCache(previewSize) previewFileBytes := taskAttachment.GetPreviewFromCache(previewSize)
if previewFileBytes != nil { if previewFileBytes != nil {
log.Debugf("Cached attachment image preview found for task attachment %v", taskAttachment.ID) log.Debugf("Cached attachment image preview found for task attachment %v", taskAttachment.ID)
@ -180,7 +187,7 @@ func GetTaskAttachment(c echo.Context) error {
} }
// If a preview is requested and the preview was not cached, we create the preview and cache it // If a preview is requested and the preview was not cached, we create the preview and cache it
if previewSize != models.PreviewSizeUnknown { if preview {
previewFileBytes := taskAttachment.GenerateAndSavePreviewToCache(previewSize) previewFileBytes := taskAttachment.GenerateAndSavePreviewToCache(previewSize)
if previewFileBytes != nil { if previewFileBytes != nil {
return c.Blob(http.StatusOK, "image/png", previewFileBytes) return c.Blob(http.StatusOK, "image/png", previewFileBytes)