Merge remote-tracking branch 'origin/exif_metadata'

# Conflicts:
#	public/mix-manifest.json
#	resources/js/App.vue
#	resources/js/components/FilesView/ImageMetaData.vue
#	resources/js/components/FilesView/InfoSidebar.vue
#	resources/js/components/FilesView/SearchBar.vue
#	resources/js/components/Spotlight/Spotlight.vue
#	resources/js/views/Shared.vue
#	src/Domain/Files/Resources/FileResource.php
This commit is contained in:
Čarodej
2022-02-25 18:23:08 +01:00
16 changed files with 358 additions and 143 deletions
@@ -25,7 +25,6 @@ class CreateFilesTable extends Migration
$table->text('filesize'); $table->text('filesize');
$table->text('type')->nullable(); $table->text('type')->nullable();
$table->longText('metadata')->nullable();
$table->enum('author', ['user', 'member', 'visitor'])->default('user'); $table->enum('author', ['user', 'member', 'visitor'])->default('user');
@@ -27,8 +27,8 @@ class CreateUserSettingsTable extends Migration
$table->text('country')->nullable(); $table->text('country')->nullable();
$table->text('phone_number')->nullable(); $table->text('phone_number')->nullable();
$table->decimal('timezone', 10, 1)->nullable(); $table->decimal('timezone', 10, 1)->nullable();
$table->text('emoji_type')->default('twemoji'); $table->text('emoji_type');
$table->text('theme_mode')->default('system'); $table->text('theme_mode');
$table->charset = 'utf8mb4'; $table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci'; $table->collation = 'utf8mb4_unicode_ci';
}); });
@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateExifsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('exifs', function (Blueprint $table) {
$table->uuid('id')->primary()->index();
$table->uuid('file_id')->index();
$table->timestamp('date_time_original')->nullable();
$table->string('artist')->nullable();
$table->integer('height')->nullable();
$table->integer('width')->nullable();
$table->string('x_resolution')->nullable();
$table->string('y_resolution')->nullable();
$table->integer('color_space')->nullable();
$table->string('camera')->nullable();
$table->string('model')->nullable();
$table->string('aperture_value')->nullable();
$table->string('exposure_time')->nullable();
$table->string('focal_length')->nullable();
$table->integer('iso')->nullable();
$table->string('aperture_f_number')->nullable();
$table->string('ccd_width')->nullable();
$table->string('longitude')->nullable();
$table->string('latitude')->nullable();
$table->string('longitude_ref')->nullable();
$table->string('latitude_ref')->nullable();
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('exifs');
}
}
+4 -3
View File
@@ -54,9 +54,10 @@ export default {
}, },
methods: { methods: {
spotlightListener(e) { spotlightListener(e) {
if (e.key === 'k' && e.metaKey) { if (e.key === 'k' && e.metaKey || e.key === 'k' && e.ctrlKey) {
events.$emit('spotlight:show') e.preventDefault()
} events.$emit('spotlight:show');
}
}, },
handleDarkMode() { handleDarkMode() {
const app = document.getElementsByTagName('html')[0] const app = document.getElementsByTagName('html')[0]
@@ -1,108 +1,93 @@
<template> <template>
<div> <div>
<ul class="meta-data-list"> <ul class="meta-data-list">
<li v-if="clipboard.metadata.DateTimeOriginal"> <li v-if="clipboard.data.attributes.date_time_original">
<span>{{ $t('file_detail_meta.time_data') }}</span> <span>{{ $t('file_detail_meta.time_data') }}</span>
<b>{{ clipboard.metadata.DateTimeOriginal }}</b> <b>{{ clipboard.data.attributes.date_time_original }}</b>
</li> </li>
<li v-if="clipboard.metadata.Artist"> <li v-if="clipboard.data.attributes.artist">
<span>{{ $t('file_detail_meta.author') }}</span> <span>{{ $t('file_detail_meta.author') }}</span>
<b>{{ clipboard.metadata.Artist }}</b> <b>{{ clipboard.data.attributes.artist }}</b>
</li> </li>
<li v-if="clipboard.metadata.ExifImageWidth && clipboard.metadata.ExifImageLength"> <li v-if="clipboard.data.attributes.width && clipboard.data.attributes.height">
<span>{{ $t('file_detail_meta.dimension') }}</span> <span>{{ $t('file_detail_meta.dimension') }}</span>
<b>{{ clipboard.metadata.ExifImageWidth }}x{{ clipboard.metadata.ExifImageLength }}</b> <b>{{ clipboard.data.attributes.width }}x{{ clipboard.data.attributes.height }}</b>
</li> </li>
<li v-if="clipboard.metadata.XResolution && clipboard.metadata.YResolution"> <li v-if="clipboard.data.attributes.x_resolution && clipboard.data.attributes.y_resolution">
<span>{{ $t('file_detail_meta.resolution') }}</span> <span>{{ $t('file_detail_meta.resolution') }}</span>
<b>{{ clipboard.metadata.XResolution }}x{{ clipboard.metadata.YResolution }}</b> <b>{{ clipboard.data.attributes.x_resolution }}x{{ clipboard.data.attributes.y_resolution }}</b>
</li> </li>
<li v-if="clipboard.metadata.ColorSpace"> <li v-if="clipboard.data.attributes.color_space">
<span> {{ $t('file_detail_meta.color_space') }}</span> <span> {{ $t('file_detail_meta.color_space') }}</span>
<b>{{ clipboard.metadata.ColorSpace }}</b> <b>{{ clipboard.data.attributes.color_space }}</b>
</li> </li>
<!--TODO: Colour profile:sRGB IEC61966-2.1--> <li v-if="clipboard.data.attributes.make">
<li v-if="clipboard.metadata.Make">
<span>{{ $t('file_detail_meta.make') }}</span> <span>{{ $t('file_detail_meta.make') }}</span>
<b>{{ clipboard.metadata.Make }}</b> <b>{{ clipboard.data.attributes.make }}</b>
</li> </li>
<li v-if="clipboard.metadata.Model"> <li v-if="clipboard.data.attributes.model">
<span>{{ $t('file_detail_meta.model') }}</span> <span>{{ $t('file_detail_meta.model') }}</span>
<b>{{ clipboard.metadata.Model }}</b> <b>{{ clipboard.data.attributes.model }}</b>
</li> </li>
<li v-if="clipboard.metadata.ApertureValue"> <li v-if="clipboard.data.attributes.aperture_value">
<span>{{ $t('file_detail_meta.aperture_value') }}</span> <span>{{ $t('file_detail_meta.aperture_value') }}</span>
<b v-html="parseInt(clipboard.metadata.ApertureValue) / 100"></b> <b> {{ clipboard.data.attributes.aperture_value }} </b>
</li> </li>
<li v-if="clipboard.metadata.ExposureTime"> <li v-if="clipboard.data.attributes.exposure_time">
<span>{{ $t('file_detail_meta.exposure') }}</span> <span>{{ $t('file_detail_meta.exposure') }}</span>
<b>{{ clipboard.metadata.ExposureTime }}</b> <b>{{ clipboard.data.attributes.exposure_time }}</b>
</li> </li>
<li v-if="clipboard.metadata.FocalLength"> <li v-if="clipboard.data.attributes.focal_length">
<span>{{ $t('file_detail_meta.focal') }}</span> <span>{{ $t('file_detail_meta.focal') }}</span>
<b>{{ clipboard.metadata.FocalLength }}</b> <b>{{ clipboard.data.attributes.focal_length }}</b>
</li> </li>
<li v-if="clipboard.metadata.ISOSpeedRatings"> <li v-if="clipboard.data.attributes.iso">
<span>{{ $t('file_detail_meta.iso') }}</span> <span>{{ $t('file_detail_meta.iso') }}</span>
<b>{{ clipboard.metadata.ISOSpeedRatings }}</b> <b>{{ clipboard.data.attributes.iso }}</b>
</li> </li>
<li v-if="clipboard.metadata.COMPUTED.ApertureFNumber"> <li v-if="clipboard.data.attributes.aperture_f_number">
<span>{{ $t('file_detail_meta.aperature') }}</span> <span>{{ $t('file_detail_meta.aperature') }}</span>
<b>{{ clipboard.metadata.COMPUTED.ApertureFNumber }}</b> <b>{{ clipboard.data.attributes.aperture_f_number }}</b>
</li> </li>
<li v-if="clipboard.metadata.COMPUTED.CCDWidth"> <li v-if="clipboard.data.attributes.ccd_width">
<span>{{ $t('file_detail_meta.camera_lens') }}</span> <span>{{ $t('file_detail_meta.camera_lens') }}</span>
<b>{{ clipboard.metadata.COMPUTED.CCDWidth }}</b> <b>{{ clipboard.data.attributes.ccd_width }}</b>
</li> </li>
<li v-if="clipboard.metadata.GPSLongitude"> <li v-if="clipboard.data.attributes.longitude">
<span>{{ $t('file_detail_meta.longitude') }}</span> <span>{{ $t('file_detail_meta.longitude') }}</span>
<b>{{ formatGps(clipboard.metadata.GPSLongitude, clipboard.metadata.GPSLongitudeRef) }}</b> <b>{{ clipboard.data.attributes.longitude }}</b>
</li> </li>
<li v-if="clipboard.metadata.GPSLatitude"> <li v-if="clipboard.data.attributes.latitude">
<span>{{ $t('file_detail_meta.latitude') }}</span> <span>{{ $t('file_detail_meta.latitude') }}</span>
<b>{{ formatGps(clipboard.metadata.GPSLatitude, clipboard.metadata.GPSLatitudeRef) }}</b> <b>{{ clipboard.data.attributes.latitude }}</b>
</li> </li>
</ul> </ul>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'
import { split } from 'lodash'
export default { export default {
name: 'ImageMetaData', name: 'ImageMetaData',
computed: { computed: {
clipboard() { clipboard() {
return this.$store.getters.clipboard[0].data.relationships return this.$store.getters.clipboard[0].data.relationships.exif
}, },
}, },
methods: {
formatGps(location, ref) {
let data = []
location.forEach((location) => {
data.push(split(location, '/', 2)[0])
})
return `${data[0]}° ${data[1]}' ${data[2].substr(0, 4) / 100}" ${ref} `
},
},
} }
</script> </script>
@@ -111,34 +96,33 @@ export default {
@import '../../../sass/vuefilemanager/mixins'; @import '../../../sass/vuefilemanager/mixins';
.meta-data-list { .meta-data-list {
list-style: none; list-style: none;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
li { li {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 9px 0; padding: 9px 0;
border-bottom: 1px solid $light_mode_border; border-bottom: 1px solid $light_mode_border;
b, b, span {
span { @include font-size(14);
@include font-size(14); color: $text;
color: $text; }
} }
}
} }
.dark { .dark {
.meta-data-list {
li {
border-color: $dark_mode_border_color;
b, .meta-data-list {
span { li {
color: $dark_mode_text_primary !important; border-color: $dark_mode_border_color;
}
} b, span {
} color: $dark_mode_text_primary !important;
}
}
}
} }
</style> </style>
@@ -113,44 +113,46 @@ import ListInfoItem from '../Others/ListInfoItem'
import MemberAvatar from './MemberAvatar' import MemberAvatar from './MemberAvatar'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
export default { export default {
name: 'InfoSidebar', name: 'InfoSidebar',
components: { components: {
TeamMembersPreview, TeamMembersPreview,
FilePreviewDetail, FilePreviewDetail,
ImageMetaData, ImageMetaData,
CopyShareLink, CopyShareLink,
MemberAvatar, MemberAvatar,
TitlePreview, TitlePreview,
ListInfoItem, ListInfoItem,
UnlockIcon, UnlockIcon,
EyeOffIcon, EyeOffIcon,
Edit2Icon, Edit2Icon,
LockIcon, LockIcon,
}, },
computed: { computed: {
...mapGetters(['permissionOptions', 'clipboard', 'user']), ...mapGetters([
isEmpty() { 'permissionOptions',
return this.clipboard.length === 0 'clipboard',
}, 'user',
isSingleFile() { ]),
return this.clipboard.length === 1 isEmpty() {
}, return this.clipboard.length === 0
singleFile() { },
return this.clipboard[0] isSingleFile() {
}, return this.clipboard.length === 1
canShowMetaData() { },
return ( singleFile() {
this.clipboard[0].data.attributes.metadata && this.clipboard[0].data.attributes.metadata.ExifImageWidth return this.clipboard[0]
) },
}, canShowMetaData() {
isLocked() { return this.clipboard[0].data.relationships.exif
return this.clipboard[0].data.relationships.shared.protected },
}, isLocked() {
sharedInfo() { return this.clipboard[0].data.relationships.shared.protected
let title = this.permissionOptions.find((option) => { },
return option.value === this.clipboard[0].data.relationships.shared.permission sharedInfo() {
}) let title = this.permissionOptions.find(option => {
return option.value === this.clipboard[0].data.relationships.shared.permission
})
return title ? this.$t(title.label) : this.$t('shared.can_download') return title ? this.$t(title.label) : this.$t('shared.can_download')
}, },
+10 -10
View File
@@ -22,15 +22,15 @@
<script> <script>
import { SearchIcon } from 'vue-feather-icons' import { SearchIcon } from 'vue-feather-icons'
export default { export default {
name: 'SearchBar', name: 'SearchBar',
components: { components: {
SearchIcon, SearchIcon,
},
computed: {
metaKeyIcon() {
return this.$isApple() ? '⌘' : '⊞'
}, },
}, computed: {
} metaKeyIcon() {
return this.$isApple() ? '⌘' : 'Ctrl'
},
},
}
</script> </script>
@@ -41,6 +41,7 @@
@keydown.delete="undoFilter" @keydown.delete="undoFilter"
@keydown.enter="showSelected" @keydown.enter="showSelected"
@keydown.meta="showByShortcut" @keydown.meta="showByShortcut"
@keydown.ctrl="showByShortcut"
@keyup.down="onPageDown" @keyup.down="onPageDown"
@keyup.up="onPageUp" @keyup.up="onPageUp"
type="text" type="text"
@@ -630,7 +631,7 @@ export default {
return this.user.data.attributes.role === 'admin' return this.user.data.attributes.role === 'admin'
}, },
metaKeyIcon() { metaKeyIcon() {
return this.$isApple() ? '⌘' : 'alt' return this.$isApple() ? '⌘' : 'Ctrl'
}, },
isNotEmptyQuery() { isNotEmptyQuery() {
return this.query !== '' return this.query !== ''
+2
View File
@@ -108,6 +108,8 @@ class UserSetting extends Model
static::creating(function ($user) { static::creating(function ($user) {
$user->id = Str::uuid(); $user->id = Str::uuid();
$user->color = config('vuefilemanager.colors')[rand(0, 5)]; $user->color = config('vuefilemanager.colors')[rand(0, 5)];
$user->emoji_type = 'twemoji';
$user->theme_mode = 'system';
}); });
} }
} }
@@ -0,0 +1,41 @@
<?php
namespace Domain\Files\Actions;
class StoreFileExifMetadataAction
{
public function __invoke($item, $file)
{
// Get exif metadata
$exif_data = get_image_meta_data($file);
if($exif_data) {
// Conver array to collection
$data = json_decode(json_encode($exif_data)) ;
$item->exif()->create([
'date_time_original' => $data->DateTimeOriginal ?? null,
'artist' => $data->OwnerName ?? null,
'width' => $data->COMPUTED->Width ?? null,
'height' => $data->COMPUTED->Height ?? null,
'x_resolution' => $data->XResolution ?? null,
'y_resolution' => $data->YResolution ?? null,
'color_space' => $data->ColorSpace ?? null,
'camera' => $data->Make ?? null,
'model' => $data->Model ?? null,
'aperture_value' => $data->ApertureValue ?? null,
'exposure_time' => $data->ExposureTime ?? null,
'focal_length' => $data->FocalLength ?? null,
'iso' => $data->ISOSpeedRatings ?? null,
'aperture_f_number' => $data->COMPUTED->ApertureFNumber ?? null,
'ccd_width' => $data->COMPUTED->CCDWidth ?? null,
'longitude' => $data->GPSLongitude ?? null,
'latitude' => $data->GPSLatitude ?? null,
'longitude_ref' => $data->GPSLongitudeRef ?? null,
'latitude_ref' => $data->GPSLatitudeRef ?? null
]);
}
}
}
+10 -5
View File
@@ -11,6 +11,7 @@ use Domain\Files\Models\File as UserFile;
use Domain\Traffic\Actions\RecordUploadAction; use Domain\Traffic\Actions\RecordUploadAction;
use App\Users\Exceptions\InvalidUserActionException; use App\Users\Exceptions\InvalidUserActionException;
use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Domain\Files\Actions\StoreFileExifMetadataAction;
class UploadFileAction class UploadFileAction
{ {
@@ -19,6 +20,7 @@ class UploadFileAction
public ProcessImageThumbnailAction $createImageThumbnail, public ProcessImageThumbnailAction $createImageThumbnail,
public GetFileParentId $getFileParentId, public GetFileParentId $getFileParentId,
public MoveFileToExternalStorageAction $moveFileToExternalStorage, public MoveFileToExternalStorageAction $moveFileToExternalStorage,
public StoreFileExifMetadataAction $storeExifMetadata,
) { ) {
} }
@@ -57,8 +59,6 @@ class UploadFileAction
// If last then process file // If last then process file
if ($request->boolean('is_last')) { if ($request->boolean('is_last')) {
$metadata = get_image_meta_data($file);
$disk_local = Storage::disk('local'); $disk_local = Storage::disk('local');
// Get user data // Get user data
@@ -90,18 +90,23 @@ class UploadFileAction
// Store user upload size // Store user upload size
($this->recordUpload)($fileSize, $user->id); ($this->recordUpload)($fileSize, $user->id);
// Return new file // Create new file
return UserFile::create([ $item = UserFile::create([
'mimetype' => get_file_type_from_mimetype($file_mimetype), 'mimetype' => get_file_type_from_mimetype($file_mimetype),
'type' => get_file_type($file_mimetype), 'type' => get_file_type($file_mimetype),
'parent_id' => ($this->getFileParentId)($request, $user->id), 'parent_id' => ($this->getFileParentId)($request, $user->id),
'metadata' => $metadata,
'name' => $request->input('filename'), 'name' => $request->input('filename'),
'basename' => $fileName, 'basename' => $fileName,
'author' => $userId ? 'visitor' : 'user', 'author' => $userId ? 'visitor' : 'user',
'filesize' => $fileSize, 'filesize' => $fileSize,
'user_id' => $user->id, 'user_id' => $user->id,
]); ]);
// Store exif metadata for files
($this->storeExifMetadata)($item, $file);
// Return new file
return $item;
} }
} }
} }
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace Domain\Files\Models;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Exif extends Model
{
use HasFactory;
protected $guarded = ['id'];
public $timestamps = false;
public $incrementing = false;
protected $keyType = 'string';
protected $casts = [
'longitude' => 'array',
'latitude' => 'array',
];
/**
* Get parent
*/
public function file(): HasOne
{
return $this->HasOne(File::class, 'id', 'file_id');
}
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->id = (string) Str::uuid();
});
}
}
+11 -5
View File
@@ -27,7 +27,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
* @property string thumbnail * @property string thumbnail
* @property string filesize * @property string filesize
* @property string type * @property string type
* @property array metadata
* @property string basename * @property string basename
* @property string name * @property string name
* @property string mimetype * @property string mimetype
@@ -55,10 +54,6 @@ class File extends Model
'file_url', 'file_url',
]; ];
protected $casts = [
'metadata' => 'array',
];
public array $sortable = [ public array $sortable = [
'name', 'name',
'created_at', 'created_at',
@@ -201,6 +196,11 @@ class File extends Model
return $this->hasOne(User::class, 'id', 'user_id'); return $this->hasOne(User::class, 'id', 'user_id');
} }
public function exif(): HasOne
{
return $this->hasOne(Exif::class);
}
public function toSearchableArray(): array public function toSearchableArray(): array
{ {
$name = mb_convert_encoding( $name = mb_convert_encoding(
@@ -225,5 +225,11 @@ class File extends Model
static::creating(function ($file) { static::creating(function ($file) {
$file->id = (string) Str::uuid(); $file->id = (string) Str::uuid();
}); });
static::deleting(function($file) {
if($file->isForceDeleting()) {
$file->exif()->forceDelete();
};
});
} }
} }
+27 -1
View File
@@ -28,7 +28,6 @@ class FileResource extends JsonResource
'mimetype' => $this->mimetype, 'mimetype' => $this->mimetype,
'file_url' => $this->file_url, 'file_url' => $this->file_url,
'thumbnail' => $this->thumbnail, 'thumbnail' => $this->thumbnail,
'metadata' => $this->metadata,
'parent_id' => $this->parent_id, 'parent_id' => $this->parent_id,
'created_at' => set_time_by_user_timezone($this->owner, $this->created_at), 'created_at' => set_time_by_user_timezone($this->owner, $this->created_at),
'updated_at' => set_time_by_user_timezone($this->owner, $this->updated_at), 'updated_at' => set_time_by_user_timezone($this->owner, $this->updated_at),
@@ -64,6 +63,33 @@ class FileResource extends JsonResource
], ],
], ],
]), ]),
$this->mergeWhen($this->exif, fn() => [
'exif' => [
'data' => [
'type' => 'exif',
'id' => $this->exif->id,
'attributes' => [
'date_time_original' => format_date($this->exif->date_time_original),
'artist' => $this->exif->artist,
'height' => $this->exif->height,
'width' => $this->exif->width,
'x_resolution' => substr($this->exif->x_resolution, 0, strrpos($this->exif->x_resolution, '/')),
'y_resolution' => substr($this->exif->y_resolution, 0, strrpos($this->exif->y_resolution, '/')),
'color_space' => $this->exif->color_space,
'camera' => $this->exif->camera,
'model' => $this->exif->model,
'aperture_value' => intval($this->exif->aperture_value) / 100,
'exposure_time' => $this->exif->exposure_time,
'focal_length' => $this->exif->focal_length,
'iso' => $this->exif->iso,
'aperture_f_number' => $this->exif->aperture_f_number,
'ccd_width' => $this->exif->ccd_width,
'longitude' => format_gps_coordinates($this->exif->longitude, $this->exif->longitude_ref),
'latitude' => format_gps_coordinates($this->exif->latitude, $this->exif->latitude_ref),
],
],
]
])
], ],
], ],
]; ];
+17
View File
@@ -1082,4 +1082,21 @@ if (! function_exists('replace_occurrence')) {
}); });
} }
} }
if(! function_exists('format_gps_coordinates')) {
/**
* Format GPS coordinates
*/
function format_gps_coordinates($coordinates, $ref)
{
if($coordinates && $ref) {
return
explode('/',$coordinates[0])[0] . '°' .
explode('/', $coordinates[1])[0] . "'" .
substr(explode(',', $coordinates[2])[0], 0, 5) / 1000 . '"' .
$ref;
}
};
}
} }
+33
View File
@@ -385,4 +385,37 @@ class FileTest extends TestCase
); );
}); });
} }
/**
* @test
*/
public function it_store_file_exif_data_after_file_upload()
{
$file = UploadedFile::fake()
->image('fake-image.jpg', 2000, 2000);
$user = User::factory()
->hasSettings()
->create();
$this
->actingAs($user)
->postJson('/api/upload', [
'filename' => $file->name,
'file' => $file,
'parent_id' => null,
'path' => '/' . $file->name,
'is_last' => 'true',
])->assertStatus(201);
$file = File::first();
$this->assertDatabaseHas('exifs', [
'file_id' => $file->id,
'height' => 2000,
'width' => 2000,
]);
}
} }