Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 36 additions & 43 deletions resources/js/form/components/fields/upload/Upload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
DialogTitle
} from "@/components/ui/dialog";
import { rotate, rotateTo } from "@/form/components/fields/upload/util/rotate";
import { createThumbnail } from "@/form/components/fields/upload/util/thumbnail";

const props = defineProps<FormFieldProps<FormUploadFieldData> & {
asEditorEmbed?: boolean,
Expand Down Expand Up @@ -93,7 +94,6 @@
'validation_rule[]': props.field.validationRule,
},
})
.use(ThumbnailGenerator, { thumbnailWidth: 300, thumbnailHeight: 300, thumbnailType: 'image/png' })
.use(XHRUpload, {
endpoint: route('code16.sharp.api.form.upload'),
fieldName: 'file',
Expand All @@ -102,42 +102,44 @@
'X-CSRF-TOKEN': getCsrfToken(),
},
})
.on('file-added', (file) => {
.on('file-added', async (file) => {
emit('clear');
uppyFile.value = uppy.getFile(file.id);
console.log('file-added', JSON.parse(JSON.stringify(uppyFile.value)));
if(file.type === 'image/svg+xml') {
transformedImg.value = URL.createObjectURL(file.data);
} else if(file.type?.startsWith('image/')) {
const preview = await createThumbnail(file, { width: 300, height: 300 });
emit('thumbnail', preview);
uppy.setFileState(file.id, { preview });
uppyFile.value = uppy.getFile(file.id);
console.log('thumbnail:generated', JSON.parse(JSON.stringify(uppyFile.value)));

if(canTransform(file.name, file.type) && props.field.imageCropRatio) {
const cropper = await new Promise<Cropper>((resolve) => {
const container = document.createElement('div');
const image = document.createElement('img');
image.src = preview;
container.appendChild(image);
return new Cropper(image, {
aspectRatio: props.field.imageCropRatio[0] / props.field.imageCropRatio[1],
autoCropArea: 1,
ready: (e) => {
resolve(e.currentTarget.cropper);
},
})
});
await onImageTransform(cropper);
cropper.destroy();
} else if(props.persistThumbnailUrl) {
const response = await fetch(preview);
const blob = await response.blob();
transformedImg.value = URL.createObjectURL(blob);
}
}
})
.on('restriction-failed', (file, error) => {
emit('error', error.message, file.data);
})
.on('thumbnail:generated', async (file, preview) => {
const { field } = props;
emit('thumbnail', preview);
uppyFile.value = uppy.getFile(file.id);
console.log('thumbnail:generated', JSON.parse(JSON.stringify(uppyFile.value)));

if(canTransform(file.name, file.type) && field.imageCropRatio) {
const cropper = await new Promise<Cropper>((resolve) => {
const container = document.createElement('div');
const image = document.createElement('img');
image.src = preview;
container.appendChild(image);
return new Cropper(image, {
aspectRatio: field.imageCropRatio[0] / field.imageCropRatio[1],
autoCropArea: 1,
ready: (e) => {
resolve(e.currentTarget.cropper);
},
})
});
await onImageTransform(cropper);
cropper.destroy();
} else if(props.persistThumbnailUrl) {
const response = await fetch(preview);
const blob = await response.blob();
transformedImg.value = URL.createObjectURL(blob);
}
})
.on('upload', () => {
emit('uploading', true);
})
Expand Down Expand Up @@ -183,7 +185,8 @@
const extension = fileName?.match(/\.[0-9a-z]+$/i)[0];
return props.field.imageTransformable
&& (!props.field.imageTransformableFileTypes || props.field.imageTransformableFileTypes?.includes(extension))
&& mimeType?.startsWith('image/');
&& mimeType?.startsWith('image/')
&& mimeType !== 'image/svg+xml';
}

const isDraggingOver = ref(false);
Expand Down Expand Up @@ -279,17 +282,7 @@
}
});
} else if(uppyFile.value) {
editModalImageUrl.value = await new Promise(resolve => {
new Uppy()
.use(ThumbnailGenerator, { thumbnailWidth: 1200, thumbnailHeight: 1000, thumbnailType: 'image/png' })
.on('thumbnail:generated', (file, preview) => {
resolve(preview);
})
.addFile({
...(uppyFile.value as MinimalRequiredUppyFile<{}, {}>),
preview: null
});
});
editModalImageUrl.value = await createThumbnail(uppyFile.value, { width: 1200, height: 1000 });
}
}

Expand Down
16 changes: 16 additions & 0 deletions resources/js/form/components/fields/upload/util/thumbnail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Uppy, { MinimalRequiredUppyFile, UppyFile } from "@uppy/core";
import ThumbnailGenerator from "@uppy/thumbnail-generator";


export function createThumbnail(file: UppyFile<any, any>, { width, height }: { width: number, height: number }) {
return new Promise<string>((resolve, reject) => {
new Uppy()
.use(ThumbnailGenerator, { thumbnailWidth: width, thumbnailHeight: height, thumbnailType: 'image/png' })
.on('thumbnail:generated', (thumbnailFile, preview) => resolve(preview))
.on('thumbnail:error', (error) => reject(error))
.addFile({
...(file as MinimalRequiredUppyFile<{}, {}>),
preview: null
});
});
}
93 changes: 59 additions & 34 deletions src/Form/Eloquent/Uploads/Thumbnails/Thumbnail.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,48 +143,55 @@ private function generateThumbnail(string $thumbnailPath, ?int $width, ?int $hei
$thumbnailDisk->makeDirectory(dirname($thumbnailPath));
}

try {
$sourceImg = $this->imageManager->read(
Storage::disk($sourceDisk)->get($sourceRelativeFilePath),
if ($this->shouldOnlyCopy()) {
$thumbnailDisk->put(
$thumbnailPath,
Storage::disk($sourceDisk)->get($sourceRelativeFilePath)
);

// Transformation filters
if ($this->transformationFilters) {
if ($rotate = Arr::get($this->transformationFilters, 'rotate.angle')) {
$sourceImg->rotate($rotate);
} else {
try {
$sourceImg = $this->imageManager->read(
Storage::disk($sourceDisk)->get($sourceRelativeFilePath),
);

// Transformation filters
if ($this->transformationFilters) {
if ($rotate = Arr::get($this->transformationFilters, 'rotate.angle')) {
$sourceImg->rotate($rotate);
}

if ($cropData = Arr::get($this->transformationFilters, 'crop')) {
$sourceImg->crop(
intval(round($sourceImg->width() * $cropData['width'])),
intval(round($sourceImg->height() * $cropData['height'])),
intval(round($sourceImg->width() * $cropData['x'])),
intval(round($sourceImg->height() * $cropData['y'])),
);
}
}

if ($cropData = Arr::get($this->transformationFilters, 'crop')) {
$sourceImg->crop(
intval(round($sourceImg->width() * $cropData['width'])),
intval(round($sourceImg->height() * $cropData['height'])),
intval(round($sourceImg->width() * $cropData['x'])),
intval(round($sourceImg->height() * $cropData['y'])),
);
// Custom modifiers
$alreadyResized = false;
foreach ($this->modifiers as $modifier) {
$modifierInstance = $this->resolveModifierClass($modifier);
if ($modifierInstance) {
$sourceImg->modify($modifierInstance);
$alreadyResized = $alreadyResized || $modifierInstance->resized();
}
}
}

// Custom modifiers
$alreadyResized = false;
foreach ($this->modifiers as $modifier) {
$modifierInstance = $this->resolveModifierClass($modifier);
if ($modifierInstance) {
$sourceImg->modify($modifierInstance);
$alreadyResized = $alreadyResized || $modifierInstance->resized();
// Resize if needed
if (! $alreadyResized) {
$sourceImg->scaleDown($width, $height);
}
}

// Resize if needed
if (! $alreadyResized) {
$sourceImg->scaleDown($width, $height);
$thumbnailDisk->put(
$thumbnailPath,
$sourceImg->encode($this->resolveEncoder())
);
} catch (FileNotFoundException|EncoderException|DecoderException) {
return null;
}

$thumbnailDisk->put(
$thumbnailPath,
$sourceImg->encode($this->resolveEncoder())
);
} catch (FileNotFoundException|EncoderException|DecoderException) {
return null;
}
}

Expand Down Expand Up @@ -239,6 +246,14 @@ private function resolveThumbnailExtension(): string

private function resolveThumbnailPath(?int $width = null, ?int $height = null): string
{
if ($this->shouldOnlyCopy()) {
return sprintf(
'%s/%s',
sharp()->config()->get('uploads.thumbnails_dir'),
$this->uploadModel->file_name
);
}

$thumbDirNameAppender = sprintf(
'%s%s_q-%s',
$this->transformationFilters ? '_'.md5(serialize($this->transformationFilters)) : '',
Expand All @@ -261,6 +276,16 @@ private function resolveThumbnailPath(?int $width = null, ?int $height = null):
return Str::replace('//', '/', $thumbnailPath);
}

private function shouldOnlyCopy(): bool
{
return in_array(
pathinfo($this->uploadModel->file_name, PATHINFO_EXTENSION),
[
'svg',
]
);
}

public function __toString()
{
// Return URL when Thumbnail is used as a string
Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/Form/Eloquent/Uploads/SharpUploadModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@
->toBeTrue();
});

it('allows to create SVG thumbnail by only copying the file', function () {
$file = createImage('local', 'test.svg');
$upload = createSharpUploadModel($file);

expect($upload->thumbnail(150, 150))
->toStartWith('/storage/thumbnails/data/test.svg')
->and(Storage::disk('public')->exists('thumbnails/data/test.svg'))
->toBeTrue();
});

function createSharpUploadModel(string $file, ?object $model = null, ?string $modelKey = 'test'): SharpUploadModel
{
return SharpUploadModel::create([
Expand Down
Loading