fix: uploads from drawer and focal point positioning (#7244)
## Description V3 PR [here](https://github.com/payloadcms/payload/pull/7117) - [x] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import React, { forwardRef, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactCrop, { type Crop as CropType } from 'react-image-crop'
|
||||
import ReactCrop from 'react-image-crop'
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
|
||||
import type { Data } from '../../forms/Form/types'
|
||||
import type { UploadEdits } from '../../../../uploads/types'
|
||||
|
||||
import Plus from '../../icons/Plus'
|
||||
import { useUploadEdits } from '../../utilities/UploadEdits'
|
||||
import { editDrawerSlug } from '../../views/collections/Edit/Upload'
|
||||
import Button from '../Button'
|
||||
import './index.scss'
|
||||
@@ -42,58 +41,80 @@ type FocalPosition = {
|
||||
y: number
|
||||
}
|
||||
|
||||
export const EditUpload: React.FC<{
|
||||
doc?: Data
|
||||
export type EditUploadProps = {
|
||||
fileName: string
|
||||
fileSrc: string
|
||||
imageCacheTag?: string
|
||||
initialCrop?: UploadEdits['crop']
|
||||
initialFocalPoint?: FocalPosition
|
||||
onSave?: (uploadEdits: UploadEdits) => void
|
||||
showCrop?: boolean
|
||||
showFocalPoint?: boolean
|
||||
}> = ({ doc, fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => {
|
||||
}
|
||||
|
||||
const defaultCrop: UploadEdits['crop'] = {
|
||||
height: 100,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
|
||||
export const EditUpload: React.FC<EditUploadProps> = ({
|
||||
fileName,
|
||||
fileSrc,
|
||||
imageCacheTag,
|
||||
initialCrop,
|
||||
initialFocalPoint,
|
||||
onSave,
|
||||
showCrop,
|
||||
showFocalPoint,
|
||||
}) => {
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation(['general', 'upload'])
|
||||
const { updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||
|
||||
const [focalPosition, setFocalPosition] = useState<FocalPosition>({
|
||||
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
|
||||
})
|
||||
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
|
||||
...defaultCrop,
|
||||
...(initialCrop || {}),
|
||||
}))
|
||||
|
||||
const defaultFocalPosition: FocalPosition = {
|
||||
x: 50,
|
||||
y: 50,
|
||||
}
|
||||
|
||||
const [focalPosition, setFocalPosition] = useState<FocalPosition>(() => ({
|
||||
...defaultFocalPosition,
|
||||
...initialFocalPoint,
|
||||
}))
|
||||
|
||||
const [checkBounds, setCheckBounds] = useState<boolean>(false)
|
||||
const [originalHeight, setOriginalHeight] = useState<number>(0)
|
||||
const [originalWidth, setOriginalWidth] = useState<number>(0)
|
||||
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
|
||||
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
|
||||
|
||||
const focalWrapRef = useRef<HTMLDivElement | undefined>()
|
||||
const imageRef = useRef<HTMLImageElement | undefined>()
|
||||
const cropRef = useRef<HTMLDivElement | undefined>()
|
||||
|
||||
const heightRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [crop, setCrop] = useState<CropType>({
|
||||
height: 100,
|
||||
heightPixels: 0,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const heightInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
|
||||
|
||||
const onImageLoad = (e) => {
|
||||
setOriginalHeight(e.currentTarget.naturalHeight)
|
||||
setOriginalWidth(e.currentTarget.naturalWidth)
|
||||
// set the default image height/width on load
|
||||
setUncroppedPixelHeight(e.currentTarget.naturalHeight)
|
||||
setUncroppedPixelWidth(e.currentTarget.naturalWidth)
|
||||
setImageLoaded(true)
|
||||
}
|
||||
|
||||
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
|
||||
const intValue = parseInt(value)
|
||||
if (dimension === 'width' && intValue >= originalWidth) return null
|
||||
if (dimension === 'height' && intValue >= originalHeight) return null
|
||||
if (dimension === 'width' && intValue >= uncroppedPixelWidth) return null
|
||||
if (dimension === 'height' && intValue >= uncroppedPixelHeight) return null
|
||||
|
||||
const percentage = 100 * (intValue / (dimension === 'width' ? originalWidth : originalHeight))
|
||||
const percentage =
|
||||
100 * (intValue / (dimension === 'width' ? uncroppedPixelWidth : uncroppedPixelHeight))
|
||||
|
||||
if (percentage === 100 || percentage === 0) return null
|
||||
|
||||
@@ -117,16 +138,13 @@ export const EditUpload: React.FC<{
|
||||
}
|
||||
|
||||
const saveEdits = () => {
|
||||
updateUploadEdits({
|
||||
crop: crop
|
||||
? {
|
||||
...crop,
|
||||
heightPixels: Number(heightRef.current?.value ?? crop.heightPixels),
|
||||
widthPixels: Number(widthRef.current?.value ?? crop.widthPixels),
|
||||
}
|
||||
: undefined,
|
||||
focalPoint: focalPosition ? focalPosition : undefined,
|
||||
})
|
||||
if (typeof onSave === 'function')
|
||||
onSave({
|
||||
crop: crop ? crop : undefined,
|
||||
focalPoint: focalPosition,
|
||||
heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight),
|
||||
widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth),
|
||||
})
|
||||
closeModal(editDrawerSlug)
|
||||
}
|
||||
|
||||
@@ -181,7 +199,7 @@ export const EditUpload: React.FC<{
|
||||
className={`${baseClass}__focal-wrapper`}
|
||||
ref={focalWrapRef}
|
||||
style={{
|
||||
aspectRatio: `${originalWidth / originalHeight}`,
|
||||
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
|
||||
}}
|
||||
>
|
||||
{showCrop ? (
|
||||
@@ -237,10 +255,8 @@ export const EditUpload: React.FC<{
|
||||
onClick={() =>
|
||||
setCrop({
|
||||
height: 100,
|
||||
heightPixels: originalHeight,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: originalWidth,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
@@ -257,14 +273,14 @@ export const EditUpload: React.FC<{
|
||||
<Input
|
||||
name={`${t('upload:width')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
|
||||
ref={widthRef}
|
||||
value={((crop.width / 100) * originalWidth).toFixed(0)}
|
||||
ref={widthInputRef}
|
||||
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
|
||||
/>
|
||||
<Input
|
||||
name={`${t('upload:height')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
|
||||
ref={heightRef}
|
||||
value={((crop.height / 100) * originalHeight).toFixed(0)}
|
||||
ref={heightInputRef}
|
||||
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import type { UploadEdits } from '../../../../../../uploads/types'
|
||||
import type { Props } from './types'
|
||||
|
||||
import isImage from '../../../../../../uploads/isImage'
|
||||
@@ -13,6 +14,7 @@ import FileDetails from '../../../../elements/FileDetails'
|
||||
import PreviewSizes from '../../../../elements/PreviewSizes'
|
||||
import Thumbnail from '../../../../elements/Thumbnail'
|
||||
import Error from '../../../../forms/Error'
|
||||
import { useForm } from '../../../../forms/Form/context'
|
||||
import reduceFieldsToValues from '../../../../forms/Form/reduceFieldsToValues'
|
||||
import { fieldBaseClass } from '../../../../forms/field-types/shared'
|
||||
import useField from '../../../../forms/useField'
|
||||
@@ -55,7 +57,8 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
const [replacingFile, setReplacingFile] = useState(false)
|
||||
const [fileSrc, setFileSrc] = useState<null | string>(null)
|
||||
const { t } = useTranslation(['upload', 'general'])
|
||||
const { resetUploadEdits } = useUploadEdits()
|
||||
const { setModified } = useForm()
|
||||
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||
const [doc, setDoc] = useState(reduceFieldsToValues(internalState || {}, true))
|
||||
const { docPermissions } = useDocumentInfo()
|
||||
const { errorMessage, setValue, showError, value } = useField<File>({
|
||||
@@ -133,6 +136,14 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
setShowUrlInput(false)
|
||||
}, [handleFileChange, resetUploadEdits])
|
||||
|
||||
const onEditsSave = useCallback(
|
||||
(args: UploadEdits) => {
|
||||
setModified(true)
|
||||
updateUploadEdits(args)
|
||||
},
|
||||
[setModified, updateUploadEdits],
|
||||
)
|
||||
|
||||
const handlePasteUrlClick = () => {
|
||||
setShowUrlInput((prev) => !prev)
|
||||
}
|
||||
@@ -222,7 +233,7 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
onClick={handleUrlSubmit}
|
||||
type="button"
|
||||
>
|
||||
{t('upload:addImage')}
|
||||
{t('upload:addFile')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,10 +285,15 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
{(value || doc.filename) && (
|
||||
<Drawer header={null} slug={editDrawerSlug}>
|
||||
<EditUpload
|
||||
doc={doc || undefined}
|
||||
fileName={value?.name || doc?.filename}
|
||||
fileSrc={doc?.url || fileSrc}
|
||||
imageCacheTag={doc.updatedAt}
|
||||
initialCrop={uploadEdits?.crop ?? undefined}
|
||||
initialFocalPoint={{
|
||||
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
|
||||
}}
|
||||
onSave={onEditsSave}
|
||||
showCrop={showCrop}
|
||||
showFocalPoint={showFocalPoint}
|
||||
/>
|
||||
|
||||
@@ -4,15 +4,3 @@ export type IndexProps = {
|
||||
collection: SanitizedCollectionConfig
|
||||
isEditing?: boolean
|
||||
}
|
||||
export type UploadEdits = {
|
||||
crop?: {
|
||||
height?: number
|
||||
width?: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
focalPoint?: {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "قريب من"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "إضافة صورة",
|
||||
"addFile": "إضافة ملف",
|
||||
"crop": "محصول",
|
||||
"cropToolDescription": "اسحب الزوايا المحددة للمنطقة، رسم منطقة جديدة أو قم بضبط القيم أدناه.",
|
||||
"dragAndDrop": "قم بسحب وإسقاط ملفّ",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "yaxın"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Şəkil əlavə et",
|
||||
"addFile": "Fayl əlavə et",
|
||||
"crop": "Məhsul",
|
||||
"cropToolDescription": "Seçilmiş sahənin köşələrini sürükləyin, yeni bir sahə çəkin və ya aşağıdakı dəyərləri düzəltin.",
|
||||
"dragAndDrop": "Faylı buraya sürükləyin və buraxın",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "близко"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Добавяне на изображение",
|
||||
"addFile": "Добавяне на файл",
|
||||
"crop": "Изрязване",
|
||||
"cropToolDescription": "Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.",
|
||||
"dragAndDrop": "Дръпни и пусни файл",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "blízko"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Přidat obrázek",
|
||||
"addFile": "Přidat soubor",
|
||||
"crop": "Ořez",
|
||||
"cropToolDescription": "Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte níže uvedené hodnoty.",
|
||||
"dragAndDrop": "Přetáhněte soubor",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "in der Nähe"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Bild hinzufügen",
|
||||
"addFile": "Datei hinzufügen",
|
||||
"crop": "Zuschneiden",
|
||||
"cropToolDescription": "Ziehen Sie die Ecken des ausgewählten Bereichs, zeichnen Sie einen neuen Bereich oder passen Sie die Werte unten an.",
|
||||
"dragAndDrop": "Ziehen Sie eine Datei per Drag-and-Drop",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "near"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Add Image",
|
||||
"addFile": "Add File",
|
||||
"crop": "Crop",
|
||||
"cropToolDescription": "Drag the corners of the selected area, draw a new area or adjust the values below.",
|
||||
"dragAndDrop": "Drag and drop a file",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "cerca"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Añadir imagen",
|
||||
"addFile": "Añadir archivo",
|
||||
"crop": "Cultivo",
|
||||
"cropToolDescription": "Arrastra las esquinas del área seleccionada, dibuja un nuevo área o ajusta los valores a continuación.",
|
||||
"dragAndDrop": "Arrastra y suelta un archivo",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "نزدیک"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "اضافه کردن تصویر",
|
||||
"addFile": "اضافه کردن فایل",
|
||||
"crop": "محصول",
|
||||
"cropToolDescription": "گوشههای منطقه انتخاب شده را بکشید، یک منطقه جدید رسم کنید یا مقادیر زیر را تنظیم کنید.",
|
||||
"dragAndDrop": "یک سند را بکشید و رها کنید",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "proche"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Ajouter une image",
|
||||
"addFile": "Ajouter un fichier",
|
||||
"crop": "Recadrer",
|
||||
"cropToolDescription": "Faites glisser les coins de la zone sélectionnée, dessinez une nouvelle zone ou ajustez les valeurs ci-dessous.",
|
||||
"dragAndDrop": "Glisser-déposer un fichier",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "blizu"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Dodaj sliku",
|
||||
"addFile": "Dodaj datoteku",
|
||||
"crop": "Usjev",
|
||||
"cropToolDescription": "Povucite kutove odabranog područja, nacrtajte novo područje ili prilagodite vrijednosti ispod.",
|
||||
"dragAndDrop": "Povucite i ispustite datoteku",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "közel"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Kép hozzáadása",
|
||||
"addFile": "Fájl hozzáadása",
|
||||
"crop": "Termés",
|
||||
"cropToolDescription": "Húzza a kijelölt terület sarkait, rajzoljon új területet, vagy igazítsa a lentebb található értékeket.",
|
||||
"dragAndDrop": "Húzzon ide egy fájlt",
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
"near": "vicino"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Aggiungi immagine",
|
||||
"addFile": "Aggiungi file",
|
||||
"crop": "Raccolto",
|
||||
"cropToolDescription": "Trascina gli angoli dell'area selezionata, disegna una nuova area o regola i valori qui sotto.",
|
||||
"dragAndDrop": "Trascina e rilascia un file",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "近く"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "画像を追加",
|
||||
"addFile": "ファイルを追加",
|
||||
"crop": "クロップ",
|
||||
"cropToolDescription": "選択したエリアのコーナーをドラッグしたり、新たなエリアを描画したり、下記の値を調整してください。",
|
||||
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "근처"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "이미지 추가",
|
||||
"addFile": "파일 추가",
|
||||
"crop": "자르기",
|
||||
"cropToolDescription": "선택한 영역의 모퉁이를 드래그하거나 새로운 영역을 그리거나 아래의 값을 조정하세요.",
|
||||
"dragAndDrop": "파일을 끌어다 놓으세요",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "နီး"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "ပုံ ထည့်ပါ",
|
||||
"addFile": "ဖိုင်ထည့်ပါ",
|
||||
"crop": "သုန်း",
|
||||
"cropToolDescription": "ရွေးထားသည့်ဧရိယာတွင်မွေးလျှက်မှုများကိုဆွဲပြီး, အသစ်တည်ပြီးသို့မဟုတ်အောက်ပါတ",
|
||||
"dragAndDrop": "ဖိုင်တစ်ဖိုင်ကို ဆွဲချလိုက်ပါ။",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "nær"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Legg til bilde",
|
||||
"addFile": "Legg til fil",
|
||||
"crop": "Beskjær",
|
||||
"cropToolDescription": "Dra hjørnene av det valgte området, tegn et nytt område eller juster verdiene nedenfor.",
|
||||
"dragAndDrop": "Dra og slipp en fil",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "nabij"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Afbeelding toevoegen",
|
||||
"addFile": "Bestand toevoegen",
|
||||
"crop": "Bijsnijden",
|
||||
"cropToolDescription": "Sleep de hoeken van het geselecteerde gebied, teken een nieuw gebied of pas de waarden hieronder aan.",
|
||||
"dragAndDrop": "Sleep een bestand",
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
"near": "blisko"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Dodaj obraz",
|
||||
"addFile": "Dodaj plik",
|
||||
"crop": "Przytnij",
|
||||
"cropToolDescription": "Przeciągnij narożniki wybranego obszaru, narysuj nowy obszar lub dostosuj poniższe wartości.",
|
||||
"dragAndDrop": "Przeciągnij i upuść plik",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "perto"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Adicionar imagem",
|
||||
"addFile": "Adicionar arquivo",
|
||||
"crop": "Cultura",
|
||||
"cropToolDescription": "Arraste as bordas da área selecionada, desenhe uma nova área ou ajuste os valores abaixo.",
|
||||
"dragAndDrop": "Arraste e solte um arquivo",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "în apropiere de"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Adaugă imagine",
|
||||
"addFile": "Adaugă fișier",
|
||||
"crop": "Cultură",
|
||||
"cropToolDescription": "Trageți colțurile zonei selectate, desenați o nouă zonă sau ajustați valorile de mai jos.",
|
||||
"dragAndDrop": "Trageți și plasați un fișier",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "blizu"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Dodaj sliku",
|
||||
"addFile": "Dodaj datoteku",
|
||||
"crop": "Isecite sliku",
|
||||
"cropToolDescription": "Prevucite uglove izabranog područja, nacrtajte novo područje ili prilagodite vrednosti ispod.",
|
||||
"dragAndDrop": "Prevucite i ispustite datoteku",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "близу"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Додај слику",
|
||||
"addFile": "Додај датотеку",
|
||||
"crop": "Исеците слику",
|
||||
"cropToolDescription": "Превуците углове изабраног подручја, нацртајте ново подручје или прилагодите вредности испод.",
|
||||
"dragAndDrop": "Превуците и испустите датотеку",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "рядом"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Добавить изображение",
|
||||
"addFile": "Добавить файл",
|
||||
"crop": "Обрезать",
|
||||
"cropToolDescription": "Перетащите углы выбранной области, нарисуйте новую область или отрегулируйте значения ниже.",
|
||||
"dragAndDrop": "Перетащите файл",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "nära"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Lägg till bild",
|
||||
"addFile": "Lägg till fil",
|
||||
"crop": "Skörd",
|
||||
"cropToolDescription": "Dra i hörnen på det valda området, rita ett nytt område eller justera värdena nedan.",
|
||||
"dragAndDrop": "Dra och släpp en fil",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "ใกล้"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "เพิ่มรูปภาพ",
|
||||
"addFile": "เพิ่มไฟล์",
|
||||
"crop": "พืชผล",
|
||||
"cropToolDescription": "ลากมุมของพื้นที่ที่เลือก, วาดพื้นที่ใหม่หรือปรับค่าด้านล่าง",
|
||||
"dragAndDrop": "ลากและวางไฟล์",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "yakın"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Resim ekle",
|
||||
"addFile": "Dosya ekle",
|
||||
"crop": "Kırp",
|
||||
"cropToolDescription": "Seçili alanın köşelerini sürükleyin, yeni bir alan çizin veya aşağıdaki değerleri ayarlayın.",
|
||||
"dragAndDrop": "Bir dosyayı sürükleyip bırakın",
|
||||
|
||||
@@ -1108,7 +1108,7 @@
|
||||
"upload": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"addImage": {
|
||||
"addFile": {
|
||||
"type": "string"
|
||||
},
|
||||
"crop": {
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "поруч"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Додати зображення",
|
||||
"addFile": "Додати файл",
|
||||
"crop": "Обрізати",
|
||||
"cropToolDescription": "Перетягніть кути обраної області, намалюйте нову область або скоригуйте значення нижче.",
|
||||
"dragAndDrop": "Перемістіть файл",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "gần"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "Thêm hình ảnh",
|
||||
"addFile": "Thêm tập tin",
|
||||
"crop": "Mùa vụ",
|
||||
"cropToolDescription": "Kéo các góc của khu vực đã chọn, vẽ một khu vực mới hoặc điều chỉnh các giá trị dưới đây.",
|
||||
"dragAndDrop": "Kéo và thả một tập tin",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "附近"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "添加圖片",
|
||||
"addFile": "添加文件",
|
||||
"crop": "裁剪",
|
||||
"cropToolDescription": "拖動所選區域的角落,繪製一個新區域或調整以下的值。",
|
||||
"dragAndDrop": "拖放一個檔案",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"near": "附近"
|
||||
},
|
||||
"upload": {
|
||||
"addImage": "添加图片",
|
||||
"addFile": "添加文件",
|
||||
"crop": "作物",
|
||||
"cropToolDescription": "拖动所选区域的角落,绘制一个新区域或调整以下的值。",
|
||||
"dragAndDrop": "拖放一个文件",
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import type { UploadedFile } from 'express-fileupload'
|
||||
import type { SharpOptions } from 'sharp'
|
||||
|
||||
import sharp from 'sharp'
|
||||
|
||||
export const percentToPixel = (value: string, dimension: number): number => {
|
||||
import type { UploadEdits } from './types'
|
||||
|
||||
export const percentToPixel = (value, dimension): number => {
|
||||
if (!value) return 0
|
||||
return Math.floor((parseFloat(value) / 100) * dimension)
|
||||
}
|
||||
export default async function cropImage({ cropData, dimensions, file }) {
|
||||
try {
|
||||
const fileIsAnimatedType = ['image/avif', 'image/gif', 'image/webp'].includes(file.mimetype)
|
||||
|
||||
const { heightPixels, widthPixels, x, y } = cropData
|
||||
type CropImageArgs = {
|
||||
cropData: UploadEdits['crop']
|
||||
dimensions: { height: number; width: number }
|
||||
file: UploadedFile
|
||||
heightInPixels: number
|
||||
widthInPixels: number
|
||||
}
|
||||
export async function cropImage({
|
||||
cropData,
|
||||
dimensions,
|
||||
file,
|
||||
heightInPixels,
|
||||
widthInPixels,
|
||||
}: CropImageArgs) {
|
||||
try {
|
||||
const { x, y } = cropData
|
||||
|
||||
const fileIsAnimatedType = ['image/avif', 'image/gif', 'image/webp'].includes(file.mimetype)
|
||||
|
||||
const sharpOptions: SharpOptions = {}
|
||||
|
||||
if (fileIsAnimatedType) sharpOptions.animated = true
|
||||
|
||||
const formattedCropData: sharp.Region = {
|
||||
height: Number(heightPixels),
|
||||
const formattedCropData = {
|
||||
height: Number(heightInPixels),
|
||||
left: percentToPixel(x, dimensions.width),
|
||||
top: percentToPixel(y, dimensions.height),
|
||||
width: Number(widthPixels),
|
||||
width: Number(widthInPixels),
|
||||
}
|
||||
|
||||
const cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData)
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { FileData, FileToSave, ProbedImageSize, UploadEdits } from './types
|
||||
import { FileUploadError, MissingFile } from '../errors'
|
||||
import FileRetrievalError from '../errors/FileRetrievalError'
|
||||
import canResizeImage from './canResizeImage'
|
||||
import cropImage from './cropImage'
|
||||
import { cropImage } from './cropImage'
|
||||
import { getExternalFile } from './getExternalFile'
|
||||
import getFileByPath from './getFileByPath'
|
||||
import getImageSize from './getImageSize'
|
||||
@@ -211,7 +211,13 @@ export const generateFileData = async <T>({
|
||||
let fileForResize = file
|
||||
|
||||
if (cropData) {
|
||||
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file })
|
||||
const { data: croppedImage, info } = await cropImage({
|
||||
cropData,
|
||||
dimensions,
|
||||
file,
|
||||
heightInPixels: uploadEdits.heightInPixels,
|
||||
widthInPixels: uploadEdits.widthInPixels,
|
||||
})
|
||||
|
||||
filesToSave.push({
|
||||
buffer: croppedImage,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UploadedFile } from 'express-fileupload'
|
||||
import type { OutputInfo, Sharp, SharpOptions } from 'sharp'
|
||||
import type { Sharp, Metadata as SharpMetadata, SharpOptions } from 'sharp'
|
||||
|
||||
import { fromBuffer } from 'file-type'
|
||||
import fs from 'fs'
|
||||
@@ -68,11 +68,21 @@ const getSanitizedImageData = (sourceImage: string): SanitizedImageData => {
|
||||
* @param extension - the extension to use
|
||||
* @returns the new image name that is not taken
|
||||
*/
|
||||
const createImageName = (
|
||||
outputImageName: string,
|
||||
{ height, width }: OutputInfo,
|
||||
extension: string,
|
||||
) => `${outputImageName}-${width}x${height}.${extension}`
|
||||
|
||||
type CreateImageNameArgs = {
|
||||
extension: string
|
||||
height: number
|
||||
outputImageName: string
|
||||
width: number
|
||||
}
|
||||
const createImageName = ({
|
||||
extension,
|
||||
height,
|
||||
outputImageName,
|
||||
width,
|
||||
}: CreateImageNameArgs): string => {
|
||||
return `${outputImageName}-${width}x${height}.${extension}`
|
||||
}
|
||||
|
||||
type CreateResultArgs = {
|
||||
filename?: FileSize['filename']
|
||||
@@ -122,46 +132,61 @@ const createResult = ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the image needs to be resized according to the requested dimensions
|
||||
* and the original image size. If the resize options withoutEnlargement or withoutReduction are provided,
|
||||
* the image will be resized regardless of the requested dimensions, given that the
|
||||
* width or height to be resized is provided.
|
||||
* Determine whether or not to resize the image.
|
||||
* - resize using image config
|
||||
* - resize using image config with focal adjustments
|
||||
* - do not resize at all
|
||||
*
|
||||
* @param resizeConfig - object containing the requested dimensions and resize options
|
||||
* @param original - the original image size
|
||||
* @returns true if resizing is not needed, false otherwise
|
||||
* `imageResizeConfig.withoutEnlargement`:
|
||||
* - undefined [default]: uploading images with smaller width AND height than the image size will return null
|
||||
* - false: always enlarge images to the image size
|
||||
* - true: if the image is smaller than the image size, return the original image
|
||||
*
|
||||
* `imageResizeConfig.withoutReduction`:
|
||||
* - false [default]: always enlarge images to the image size
|
||||
* - true: if the image is smaller than the image size, return the original image
|
||||
*
|
||||
* @return 'omit' | 'resize' | 'resizeWithFocalPoint'
|
||||
*/
|
||||
const preventResize = (
|
||||
{ height: desiredHeight, width: desiredWidth, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
original: ProbedImageSize,
|
||||
): boolean => {
|
||||
// default is to allow reduction
|
||||
if (withoutReduction !== undefined) {
|
||||
return false // needs resize
|
||||
const getImageResizeAction = ({
|
||||
dimensions: originalImage,
|
||||
hasFocalPoint,
|
||||
imageResizeConfig,
|
||||
}: {
|
||||
dimensions: ProbedImageSize
|
||||
hasFocalPoint?: boolean
|
||||
imageResizeConfig: ImageSize
|
||||
}): 'omit' | 'resize' | 'resizeWithFocalPoint' => {
|
||||
const {
|
||||
fit,
|
||||
height: targetHeight,
|
||||
width: targetWidth,
|
||||
withoutEnlargement,
|
||||
withoutReduction,
|
||||
} = imageResizeConfig
|
||||
|
||||
// prevent upscaling by default when x and y are both smaller than target image size
|
||||
if (targetHeight && targetWidth) {
|
||||
const originalImageIsSmallerXAndY =
|
||||
originalImage.width < targetWidth && originalImage.height < targetHeight
|
||||
if (withoutEnlargement === undefined && originalImageIsSmallerXAndY) {
|
||||
return 'omit' // prevent image size from being enlarged
|
||||
}
|
||||
}
|
||||
|
||||
// default is to prevent enlargement
|
||||
if (withoutEnlargement !== undefined) {
|
||||
return false // needs resize
|
||||
}
|
||||
const originalImageIsSmallerXOrY =
|
||||
originalImage.width < targetWidth || originalImage.height < targetHeight
|
||||
if (fit === 'contain' || fit === 'inside') return 'resize'
|
||||
if (!isNumber(targetHeight) && !isNumber(targetWidth)) return 'resize'
|
||||
|
||||
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
|
||||
if (isWidthOrHeightNotDefined) {
|
||||
// If width and height are not defined, it means there is a format conversion
|
||||
// and the image needs to be "resized" (transformed).
|
||||
return false // needs resize
|
||||
}
|
||||
const targetAspectRatio = targetWidth / targetHeight
|
||||
const originalAspectRatio = originalImage.width / originalImage.height
|
||||
if (originalAspectRatio === targetAspectRatio) return 'resize'
|
||||
|
||||
const hasInsufficientWidth = desiredWidth > original.width
|
||||
const hasInsufficientHeight = desiredHeight > original.height
|
||||
if (hasInsufficientWidth && hasInsufficientHeight) {
|
||||
// doesn't need resize - prevent enlargement. This should only happen if both width and height are insufficient.
|
||||
// if only one dimension is insufficient and the other is sufficient, resizing needs to happen, as the image
|
||||
// should be resized to the sufficient dimension.
|
||||
return true // do not create a new size
|
||||
}
|
||||
if (withoutEnlargement && originalImageIsSmallerXOrY) return 'resize'
|
||||
if (withoutReduction && !originalImageIsSmallerXOrY) return 'resize'
|
||||
|
||||
return false // needs resize
|
||||
return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,6 +234,19 @@ const sanitizeResizeConfig = (resizeConfig: ImageSize): ImageSize => {
|
||||
return resizeConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to extract height from images, animated or not.
|
||||
*
|
||||
* @param sharpMetadata - the sharp metadata
|
||||
* @returns the height of the image
|
||||
*/
|
||||
function extractHeightFromImage(sharpMetadata: SharpMetadata): number {
|
||||
if (sharpMetadata?.pages) {
|
||||
return sharpMetadata.height / sharpMetadata.pages
|
||||
}
|
||||
return sharpMetadata.height
|
||||
}
|
||||
|
||||
/**
|
||||
* For the provided image sizes, handle the resizing and the transforms
|
||||
* (format, trim, etc.) of each requested image size and return the result object.
|
||||
@@ -259,24 +297,28 @@ export default async function resizeAndTransformImageSizes({
|
||||
if (fileIsAnimatedType) sharpOptions.animated = true
|
||||
|
||||
const sharpBase: Sharp | undefined = sharp(file.tempFilePath || file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||
const originalImageMeta = await sharpBase.metadata()
|
||||
|
||||
const resizeImageMeta = {
|
||||
height: extractHeightFromImage(originalImageMeta),
|
||||
width: originalImageMeta.width,
|
||||
}
|
||||
|
||||
const results: ImageSizesResult[] = await Promise.all(
|
||||
imageSizes.map(async (imageResizeConfig): Promise<ImageSizesResult> => {
|
||||
imageResizeConfig = sanitizeResizeConfig(imageResizeConfig)
|
||||
|
||||
// This checks if a resize should happen. If not, the resized image will be
|
||||
// skipped COMPLETELY and thus will not be included in the resulting images.
|
||||
// All further format/trim options will thus be skipped as well.
|
||||
if (preventResize(imageResizeConfig, dimensions)) {
|
||||
return createResult({ name: imageResizeConfig.name })
|
||||
}
|
||||
const resizeAction = getImageResizeAction({
|
||||
dimensions,
|
||||
hasFocalPoint: Boolean(incomingFocalPoint),
|
||||
imageResizeConfig,
|
||||
})
|
||||
if (resizeAction === 'omit') return createResult({ name: imageResizeConfig.name })
|
||||
|
||||
const imageToResize = sharpBase.clone()
|
||||
let resized = imageToResize
|
||||
|
||||
const metadata = await sharpBase.metadata()
|
||||
|
||||
if (incomingFocalPoint && applyPayloadAdjustments(imageResizeConfig, dimensions)) {
|
||||
if (resizeAction === 'resizeWithFocalPoint') {
|
||||
let { height: resizeHeight, width: resizeWidth } = imageResizeConfig
|
||||
|
||||
const originalAspectRatio = dimensions.width / dimensions.height
|
||||
@@ -291,44 +333,62 @@ export default async function resizeAndTransformImageSizes({
|
||||
resizeHeight = Math.round(resizeWidth / originalAspectRatio)
|
||||
}
|
||||
|
||||
// Scale the image up or down to fit the resize dimensions
|
||||
const scaledImage = imageToResize.resize({
|
||||
height: resizeHeight,
|
||||
width: resizeWidth,
|
||||
})
|
||||
if (!resizeHeight) resizeHeight = resizeImageMeta.height
|
||||
if (!resizeWidth) resizeWidth = resizeImageMeta.width
|
||||
|
||||
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true })
|
||||
// if requested image is larger than the incoming size, then resize using sharp and then extract with focal point
|
||||
if (resizeHeight > resizeImageMeta.height || resizeWidth > resizeImageMeta.width) {
|
||||
const resizeAspectRatio = resizeWidth / resizeHeight
|
||||
const prioritizeHeight = resizeAspectRatio < originalAspectRatio
|
||||
resized = imageToResize.resize({
|
||||
height: prioritizeHeight ? resizeHeight : undefined,
|
||||
width: prioritizeHeight ? undefined : resizeWidth,
|
||||
})
|
||||
|
||||
const safeResizeWidth = resizeWidth ?? scaledImageInfo.width
|
||||
const maxOffsetX = scaledImageInfo.width - safeResizeWidth
|
||||
const leftFocalEdge = Math.round(
|
||||
scaledImageInfo.width * (incomingFocalPoint.x / 100) - safeResizeWidth / 2,
|
||||
)
|
||||
const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX)
|
||||
|
||||
const isAnimated = fileIsAnimatedType && metadata.pages
|
||||
|
||||
let safeResizeHeight = resizeHeight ?? scaledImageInfo.height
|
||||
|
||||
if (isAnimated && resizeHeight === undefined) {
|
||||
safeResizeHeight = scaledImageInfo.height / metadata.pages
|
||||
// must read from buffer, resize.metadata will return the original image metadata
|
||||
const { info } = await resized.toBuffer({ resolveWithObject: true })
|
||||
resizeImageMeta.height = extractHeightFromImage({
|
||||
...originalImageMeta,
|
||||
height: info.height,
|
||||
})
|
||||
resizeImageMeta.width = info.width
|
||||
}
|
||||
|
||||
const maxOffsetY = isAnimated
|
||||
? safeResizeHeight - (resizeHeight ?? safeResizeHeight)
|
||||
: scaledImageInfo.height - safeResizeHeight
|
||||
const halfResizeX = resizeWidth / 2
|
||||
const xFocalCenter = resizeImageMeta.width * (incomingFocalPoint.x / 100)
|
||||
const calculatedRightPixelBound = xFocalCenter + halfResizeX
|
||||
let leftBound = xFocalCenter - halfResizeX
|
||||
|
||||
const topFocalEdge = Math.round(
|
||||
scaledImageInfo.height * (incomingFocalPoint.y / 100) - safeResizeHeight / 2,
|
||||
)
|
||||
const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY)
|
||||
// if the right bound is greater than the image width, adjust the left bound
|
||||
// keeping focus on the right
|
||||
if (calculatedRightPixelBound > resizeImageMeta.width) {
|
||||
leftBound = resizeImageMeta.width - resizeWidth
|
||||
}
|
||||
|
||||
// extract the focal area from the scaled image
|
||||
resized = (fileIsAnimatedType ? imageToResize : scaledImage).extract({
|
||||
height: safeResizeHeight,
|
||||
left: safeOffsetX,
|
||||
top: safeOffsetY,
|
||||
width: safeResizeWidth,
|
||||
// if the left bound is less than 0, adjust the left bound to 0
|
||||
// keeping the focus on the left
|
||||
if (leftBound < 0) leftBound = 0
|
||||
|
||||
const halfResizeY = resizeHeight / 2
|
||||
const yFocalCenter = resizeImageMeta.height * (incomingFocalPoint.y / 100)
|
||||
const calculatedBottomPixelBound = yFocalCenter + halfResizeY
|
||||
let topBound = yFocalCenter - halfResizeY
|
||||
|
||||
// if the bottom bound is greater than the image height, adjust the top bound
|
||||
// keeping the image as far right as possible
|
||||
if (calculatedBottomPixelBound > resizeImageMeta.height) {
|
||||
topBound = resizeImageMeta.height - resizeHeight
|
||||
}
|
||||
|
||||
// if the top bound is less than 0, adjust the top bound to 0
|
||||
// keeping the image focus near the top
|
||||
if (topBound < 0) topBound = 0
|
||||
|
||||
resized = resized.extract({
|
||||
height: resizeHeight,
|
||||
left: Math.floor(leftBound),
|
||||
top: Math.floor(topBound),
|
||||
width: resizeWidth,
|
||||
})
|
||||
} else {
|
||||
resized = imageToResize.resize(imageResizeConfig)
|
||||
@@ -357,11 +417,15 @@ export default async function resizeAndTransformImageSizes({
|
||||
|
||||
const mimeInfo = await fromBuffer(bufferData)
|
||||
|
||||
const imageNameWithDimensions = createImageName(
|
||||
sanitizedImage.name,
|
||||
bufferInfo,
|
||||
mimeInfo?.ext || sanitizedImage.ext,
|
||||
)
|
||||
const imageNameWithDimensions = createImageName({
|
||||
extension: mimeInfo?.ext || sanitizedImage.ext,
|
||||
height: extractHeightFromImage({
|
||||
...originalImageMeta,
|
||||
height: bufferInfo.height,
|
||||
}),
|
||||
outputImageName: sanitizedImage.name,
|
||||
width: bufferInfo.width,
|
||||
})
|
||||
|
||||
const imagePath = `${staticPath}/${imageNameWithDimensions}`
|
||||
|
||||
@@ -378,7 +442,8 @@ export default async function resizeAndTransformImageSizes({
|
||||
name: imageResizeConfig.name,
|
||||
filename: imageNameWithDimensions,
|
||||
filesize: size,
|
||||
height: fileIsAnimatedType && metadata.pages ? height / metadata.pages : height,
|
||||
height:
|
||||
fileIsAnimatedType && originalImageMeta.pages ? height / originalImageMeta.pages : height,
|
||||
mimeType: mimeInfo?.mime || mimeType,
|
||||
sizesToSave: [{ buffer: bufferData, path: imagePath }],
|
||||
width,
|
||||
@@ -386,9 +451,12 @@ export default async function resizeAndTransformImageSizes({
|
||||
}),
|
||||
)
|
||||
|
||||
return results.reduce((acc, result) => {
|
||||
Object.assign(acc.sizeData, result.sizeData)
|
||||
acc.sizesToSave.push(...result.sizesToSave)
|
||||
return acc
|
||||
}, defaultResult)
|
||||
return results.reduce(
|
||||
(acc, result) => {
|
||||
Object.assign(acc.sizeData, result.sizeData)
|
||||
acc.sizesToSave.push(...result.sizesToSave)
|
||||
return acc
|
||||
},
|
||||
{ ...defaultResult },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,15 +124,22 @@ export type FileToSave = {
|
||||
path: string
|
||||
}
|
||||
|
||||
export type UploadEdits = {
|
||||
crop?: {
|
||||
height?: number
|
||||
width?: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
focalPoint?: {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
type Crop = {
|
||||
height: number
|
||||
unit: '%' | 'px'
|
||||
width: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
type FocalPoint = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type UploadEdits = {
|
||||
crop?: Crop
|
||||
focalPoint?: FocalPoint
|
||||
heightInPixels?: number
|
||||
widthInPixels?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user