First batch of changes for #38 to allow photo metadata updates

This commit is contained in:
Andy Heathershaw 2017-09-16 08:26:05 +01:00
parent 24348b52a4
commit 0b64728d0a
25 changed files with 355 additions and 7 deletions

View File

@ -30,7 +30,7 @@ class DbHelper
return self::$allowedAlbumIDs; return self::$allowedAlbumIDs;
} }
public static function getAlbumsForCurrentUser($parentID = -1) public static function getAlbumsForCurrentUser($parentID = -1)
{ {
$query = self::getAlbumsForCurrentUser_NonPaged(); $query = self::getAlbumsForCurrentUser_NonPaged();
@ -43,7 +43,7 @@ class DbHelper
return $query->paginate(UserConfig::get('items_per_page')); return $query->paginate(UserConfig::get('items_per_page'));
} }
public static function getAlbumsForCurrentUser_NonPaged($permission = 'list') public static function getAlbumsForCurrentUser_NonPaged()
{ {
$albumsQuery = Album::query(); $albumsQuery = Album::query();
$user = Auth::user(); $user = Auth::user();
@ -60,7 +60,7 @@ class DbHelper
->join('permissions', 'permissions.id', '=', 'album_anonymous_permissions.permission_id') ->join('permissions', 'permissions.id', '=', 'album_anonymous_permissions.permission_id')
->where([ ->where([
['permissions.section', 'album'], ['permissions.section', 'album'],
['permissions.description', $permission] ['permissions.description', 'list']
]); ]);
} }
else else
@ -78,12 +78,12 @@ class DbHelper
->where('albums.user_id', $user->id) ->where('albums.user_id', $user->id)
->orWhere([ ->orWhere([
['group_permissions.section', 'album'], ['group_permissions.section', 'album'],
['group_permissions.description', $permission], ['group_permissions.description', 'list'],
['user_groups.user_id', $user->id] ['user_groups.user_id', $user->id]
]) ])
->orWhere([ ->orWhere([
['user_permissions.section', 'album'], ['user_permissions.section', 'album'],
['user_permissions.description', $permission], ['user_permissions.description', 'list'],
['album_user_permissions.user_id', $user->id] ['album_user_permissions.user_id', $user->id]
]); ]);
} }

View File

@ -8,6 +8,7 @@ use App\Facade\Theme;
use App\Facade\UserConfig; use App\Facade\UserConfig;
use App\Group; use App\Group;
use App\Helpers\DbHelper; use App\Helpers\DbHelper;
use App\Helpers\FileHelper;
use App\Helpers\MiscHelper; use App\Helpers\MiscHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests; use App\Http\Requests;
@ -180,6 +181,54 @@ class AlbumController extends Controller
]); ]);
} }
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function metadata(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
/** @var Album $album */
$album = $this->loadAlbum($id);
return Theme::render('admin.album_metadata', ['album' => $album, 'current_metadata' => PhotoService::METADATA_VERSION]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function metadataPost(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
/** @var Album $album */
$album = $this->loadAlbum($id);
$photosNeededToUpdate = $album->photos()->where('metadata_version', '<', PhotoService::METADATA_VERSION)->get();
$queueToken = MiscHelper::randomString();
// First download the original of each photo that needs updating and mark it as needing analysis
foreach ($photosNeededToUpdate as $photo)
{
/** @var Photo $photo */
$photo->is_analysed = false;
$photoService = new PhotoService($photo);
$photoService->downloadOriginalToFolder(FileHelper::getQueuePath($queueToken));
$photo->save();
}
// Now redirect to the analysis page
return response()->redirectToRoute('albums.analyse', ['id' => $id, 'queue_token' => $queueToken]);
}
public function setGroupPermissions(Request $request, $id) public function setGroupPermissions(Request $request, $id)
{ {
$this->authorizeAccessToAdminPanel('admin:manage-albums'); $this->authorizeAccessToAdminPanel('admin:manage-albums');

View File

@ -15,6 +15,7 @@ use App\Http\Requests\SaveSettingsRequest;
use App\Label; use App\Label;
use App\Mail\TestMailConfig; use App\Mail\TestMailConfig;
use App\Photo; use App\Photo;
use App\Services\PhotoService;
use App\Storage; use App\Storage;
use App\User; use App\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -32,6 +33,38 @@ class DefaultController extends Controller
View::share('is_admin', true); View::share('is_admin', true);
} }
public function metadataUpgrade()
{
$albums = DbHelper::getAlbumsForCurrentUser();
$albumIDs = DbHelper::getAlbumIDsForCurrentUser();
$photoMetadata = DB::table('photos')
->whereIn('album_id', $albumIDs)
->select([
'album_id',
DB::raw('MIN(metadata_version) AS min_metadata_version')
])
->groupBy('album_id')
->get();
foreach ($photoMetadata as $metadata)
{
/** @var Album $album */
foreach ($albums as $album)
{
if ($album->id == $metadata->album_id)
{
$album->min_metadata_version = $metadata->min_metadata_version;
}
}
}
return Theme::render('admin.metadata_upgrade', [
'albums' => $albums,
'current_metadata_version' => PhotoService::METADATA_VERSION
]);
}
public function index() public function index()
{ {
$this->authorizeAccessToAdminPanel(); $this->authorizeAccessToAdminPanel();
@ -42,12 +75,15 @@ class DefaultController extends Controller
$labelCount = Label::all()->count(); $labelCount = Label::all()->count();
$userCount = User::where('is_activated', true)->count(); $userCount = User::where('is_activated', true)->count();
$metadataUpgradeNeeded = Photo::min('metadata_version') < PhotoService::METADATA_VERSION;
return Theme::render('admin.index', [ return Theme::render('admin.index', [
'album_count' => $albumCount, 'album_count' => $albumCount,
'app_version' => config('app.version'), 'app_version' => config('app.version'),
'group_count' => $groupCount, 'group_count' => $groupCount,
'label_count' => $labelCount, 'label_count' => $labelCount,
'memory_limit' => ini_get('memory_limit'), 'memory_limit' => ini_get('memory_limit'),
'metadata_upgrade_needed' => $metadataUpgradeNeeded,
'photo_count' => $photoCount, 'photo_count' => $photoCount,
'php_version' => phpversion(), 'php_version' => phpversion(),
'os_version' => exec('lsb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om'), 'os_version' => exec('lsb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om'),

View File

@ -135,6 +135,27 @@ class PhotoController extends Controller
]); ]);
} }
public function showExifData(Request $request, $albumUrlAlias, $photoFilename)
{
$album = DbHelper::getAlbumByPath($albumUrlAlias);
if (is_null($album))
{
App::abort(404);
return null;
}
$this->authorizeForUser($this->getUser(), 'view', $album);
$photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename);
$this->authorizeForUser($this->getUser(), 'changeMetadata', $photo);
return Theme::render('gallery.photo_exif', [
'album' => $album,
'exif_data' => print_r(unserialize($photo->raw_exif_data), true),
'photo' => $photo
]);
}
/** /**
* @param $id * @param $id
* @return Photo * @return Photo

View File

@ -31,6 +31,7 @@ class Photo extends Model
'width', 'width',
'height', 'height',
'is_analysed', 'is_analysed',
'raw_exif_data',
'created_at', 'created_at',
'updated_at' 'updated_at'
]; ];
@ -48,6 +49,14 @@ class Photo extends Model
return $this->belongsTo(Album::class); return $this->belongsTo(Album::class);
} }
public function exifUrl()
{
return route('viewExifData', [
'albumUrlAlias' => $this->album->url_path,
'photoFilename' => $this->storage_file_name
]);
}
public function labelIDs() public function labelIDs()
{ {
$labelIDs = []; $labelIDs = [];

View File

@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\File\File;
class PhotoService class PhotoService
{ {
const METADATA_VERSION = 1; const METADATA_VERSION = 2;
/** /**
* @var Album * @var Album
@ -70,6 +70,7 @@ class PhotoService
// Read the Exif data // Read the Exif data
$exifData = @exif_read_data($photoFile); $exifData = @exif_read_data($photoFile);
$isExifDataFound = ($exifData !== false && is_array($exifData)); $isExifDataFound = ($exifData !== false && is_array($exifData));
$this->photo->raw_exif_data = $isExifDataFound ? serialize($exifData) : '';
$angleToRotate = 0; $angleToRotate = 0;
// If Exif data contains an Orientation, ensure we rotate the original image as such // If Exif data contains an Orientation, ensure we rotate the original image as such
@ -179,6 +180,24 @@ class PhotoService
$this->photo->delete(); $this->photo->delete();
} }
public function downloadOriginalToFolder($folderPath)
{
$photoPath = join(DIRECTORY_SEPARATOR, [$folderPath, $this->photo->storage_file_name]);
$photoHandle = fopen($photoPath, 'w');
$stream = $this->albumSource->fetchPhotoContent($this->photo);
$stream->rewind();
while (!$stream->feof())
{
fwrite($photoHandle, $stream->read(4096));
}
fflush($photoHandle);
fclose($photoHandle);
$stream->close();
return $photoPath;
}
public function flip($horizontal, $vertical) public function flip($horizontal, $vertical)
{ {
// First export the original photo from the storage provider // First export the original photo from the storage provider

View File

View File

0
resources/assets/selectize/css/selectize.css Executable file → Normal file
View File

0
resources/assets/selectize/css/selectize.default.css Executable file → Normal file
View File

0
resources/assets/selectize/css/selectize.legacy.css Executable file → Normal file
View File

0
resources/assets/selectize/js/selectize.js Executable file → Normal file
View File

0
resources/assets/selectize/js/selectize.min.js vendored Executable file → Normal file
View File

0
resources/assets/selectize/js/standalone/selectize.js Executable file → Normal file
View File

0
resources/assets/selectize/js/standalone/selectize.min.js vendored Executable file → Normal file
View File

View File

@ -123,6 +123,19 @@ return [
'manage_widget' => [ 'manage_widget' => [
'panel_header' => 'Manage' 'panel_header' => 'Manage'
], ],
'metadata_upgrade' => [
'can_be_upgraded' => 'Metadata can be updated (version :version_from --&gt; :version_to)',
'confirm' => 'Are you sure you want to update the metadata of the :name album?',
'intro' => 'Blue Twilight analyses your photos to capture metadata such as the camera and location used to capture the photo. Sometimes we capture more metadata than we have done previously, which requires your original photos to be re-analysed.',
'intro_2' => 'This page shows you which of your albums contain photos that need to be re-analysed to get the latest metadata information.',
'intro_3' => 'Click the "Update Metadata" button to re-analyse an album.',
'is_up_to_date' => 'Metadata is up-to-date',
'title' => 'Update Photo Metadata',
'upgrade_button' => 'Update Metadata',
'warning' => 'This will cause the original photo images to be downloaded from your storage. If this is a cloud storage provider (e.g. Amazon S3, Rackspace) you may incur charges for this action.'
],
'metadata_upgrade_link' => 'More information',
'metadata_upgrade_needed' => 'To enable new features that use metadata stored within your photos, Blue Twilight needs to re-analyse one or more of your albums.',
'move_failed_same_album' => 'The photo ":name" already belongs to the ":album" album and was not moved.', 'move_failed_same_album' => 'The photo ":name" already belongs to the ":album" album and was not moved.',
'move_successful_message' => 'The photo ":name" was moved successfully to the ":album" album.', 'move_successful_message' => 'The photo ":name" was moved successfully to the ":album" album.',
'no_albums_text' => 'You have no photo albums yet. Click the button below to create one.', 'no_albums_text' => 'You have no photo albums yet. Click the button below to create one.',

View File

@ -24,6 +24,7 @@ return [
'photos' => 'photo|photos', 'photos' => 'photo|photos',
'previous_button' => '&laquo; Previous Photo', 'previous_button' => '&laquo; Previous Photo',
'show_more_labels' => '... and :count other|... and :count others', 'show_more_labels' => '... and :count other|... and :count others',
'show_raw_exif_data' => 'Show all EXIF data',
'statistics' => [ 'statistics' => [
'album_by_photos' => 'Top 10 largest albums - number of photos', 'album_by_photos' => 'Top 10 largest albums - number of photos',
'album_by_size' => 'Top 10 largest albums - photo size (MB)', 'album_by_size' => 'Top 10 largest albums - photo size (MB)',

View File

@ -17,8 +17,9 @@ return [
'edit_storage' => 'Edit storage location', 'edit_storage' => 'Edit storage location',
'edit_user' => 'Edit user', 'edit_user' => 'Edit user',
'groups' => 'Groups', 'groups' => 'Groups',
'labels' => 'Labels',
'home' => 'Gallery', 'home' => 'Gallery',
'labels' => 'Labels',
'metadata_upgrade' => 'Update Photo Metadata',
'settings' => 'Settings', 'settings' => 'Settings',
'storage' => 'Storage', 'storage' => 'Storage',
'users' => 'Users' 'users' => 'Users'

View File

@ -0,0 +1,34 @@
@extends(Theme::viewName('layout'))
@section('title', trans('admin.metadata_upgrade.title'))
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('home') }}"><i class="fa fa-fw fa-home"></i></a></li>
<li class="breadcrumb-item"><a href="{{ route('admin') }}">@lang('navigation.breadcrumb.admin')</a></li>
<li class="breadcrumb-item"><a href="{{ route('albums.index') }}">@lang('navigation.breadcrumb.albums')</a></li>
<li class="breadcrumb-item"><a href="{{ route('albums.show', ['id' => $album->id]) }}">{{ $album->name }}</a></li>
<li class="breadcrumb-item active">@lang('navigation.breadcrumb.metadata_upgrade')</li>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 ml-md-auto mr-md-auto">
<div class="card bg-primary">
<div class="card-header text-white">@yield('title')</div>
<div class="card-body bg-light">
<p>@lang('admin.metadata_upgrade.confirm', ['name' => $album->name])</p>
<p class="text-danger"><b>@lang('admin.metadata_upgrade.warning')</b></p>
<div class="text-right">
<form action="{{ route('albums.metadataPost', [$album->id]) }}" method="POST">
{{ csrf_field() }}
<a href="{{ route('admin.metadataUpgrade', ['id' => $album->id]) }}" class="btn btn-link">@lang('forms.cancel_action')</a>
<button type="submit" class="btn btn-primary"><i class="fa fa-fw fa-check"></i> @lang('forms.continue_action')</button>
</form>
</div>
</pdiv>
</div>
</div>
</div>
</div>
@endsection

View File

@ -8,6 +8,17 @@
@section('content') @section('content')
<div class="container"> <div class="container">
@if ($metadata_upgrade_needed)
<div class="row">
<div class="col">
<div class="alert alert-warning">
@lang('admin.metadata_upgrade_needed')<br/>
<a href="{{ route('admin.metadataUpgrade') }}">@lang('admin.metadata_upgrade_link')</a>
</div>
</div>
</div>
@endif
<div class="row"> <div class="row">
<div class="col-md-4 order-1 order-md-2"> <div class="col-md-4 order-1 order-md-2">
@include (Theme::viewName('partials.admin_actions_widget')) @include (Theme::viewName('partials.admin_actions_widget'))

View File

@ -0,0 +1,81 @@
@extends(Theme::viewName('layout'))
@section('title', 'Gallery Admin')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('home') }}"><i class="fa fa-fw fa-home"></i></a></li>
<li class="breadcrumb-item"><a href="{{ route('admin') }}">@lang('navigation.breadcrumb.admin')</a></li>
<li class="breadcrumb-item active">@lang('navigation.breadcrumb.metadata_upgrade')</li>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="col">
<h1>@lang('admin.metadata_upgrade.title')</h1>
<div class="alert alert-info mb-4">
<i class="fa fa-fw fa-info"></i> @lang('admin.metadata_upgrade.intro')
</div>
<p>@lang('admin.metadata_upgrade.intro_2')</p>
<p class="mb-4">@lang('admin.metadata_upgrade.intro_3')</p>
@if (count($albums) == 0)
<div class="text-center">
<h4 class="text-danger"><b>@lang('admin.no_albums_title')</b></h4>
<p>@lang('admin.no_albums_text')</p>
<p style="margin-top: 40px;">
<a href="{{ route('albums.create') }}" class="btn btn-lg btn-success"><i class="fa fa-fw fa-plus"></i> @lang('admin.create_album')</a>
</p>
</div>
@else
<table class="table table-hover table-striped">
<tbody>
@foreach ($albums as $album)
@include (Theme::viewName('partials.metadata_single_album_admin'))
@endforeach
</tbody>
</table>
<div class="text-center">
{{ $albums->links() }}
</div>
@endif
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('.album-expand-handle').click(function() {
var parent = $(this).closest('tr');
var handle = $('.album-expand-handle', parent);
var albumID = parent.data('album-id');
$('tr[data-parent-album-id=' + albumID + ']').toggle();
if (handle.hasClass('fa-plus'))
{
handle.addClass('fa-minus');
handle.removeClass('fa-plus');
}
else
{
// Toggle all children
$('tr[data-parent-album-id=' + albumID + ']').each(function(index, element)
{
var childHandle = $('.album-expand-handle', element);
if (childHandle.hasClass('fa-minus'))
{
childHandle.click();
}
});
handle.addClass('fa-plus');
handle.removeClass('fa-minus');
}
})
})
</script>
@endpush

View File

@ -105,6 +105,12 @@
</table> </table>
</div> </div>
</div> </div>
@if (!empty($photo->raw_exif_data))
@can('changeMetadata', $photo)
<p><a href="{{ $photo->exifUrl() }}" class="btn btn-primary mt-3"><i class="fa fa-eye fa-fw"></i> @lang('gallery.show_raw_exif_data')</a></p>
@endcan
@endif
</div> </div>
</div> </div>

View File

@ -0,0 +1,31 @@
@extends(Theme::viewName('layout'))
@section('title', $photo->name)
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('home') }}"><i class="fa fa-fw fa-home"></i></a></li>
@foreach ($album->albumParentTree() as $parentAlbum)
<li class="breadcrumb-item"><a href="{{ $parentAlbum->url() }}">{{ $parentAlbum->name }}</a></li>
@endforeach
<li class="breadcrumb-item active">{{ $photo->name }}</li>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="col">
<h1>{{ $photo->name }}</h1>
@if (strlen($photo->description) > 0)
<p>{{ $photo->description }}</p>
@endif
</div>
</div>
<div class="row">
<div class="col content-body">
<p class="text-center"><img src="{{ $photo->thumbnailUrl('fullsize') }}" alt="" class="img-thumbnail mb-4"/></p>
<pre>{{ $exif_data }}</pre>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,30 @@
<tr data-album-id="{{ $album->id }}">
<td style="width: 20px;">
</td>
<td>
<span style="font-size: 1.3em;">
@can('edit', $album)
<a href="{{ route('albums.show', ['id' => $album->id]) }}">{{ $album->name }}</a>
@endcan
@cannot('edit', $album)
{{ $album->name }} <i class="fa fa-fw fa-lock"></i>
@endcannot
</span><br/>
<p>{{ $album->description }}</p>
<p style="margin-bottom: 0;">
<b>{{ $album->photos_count }}</b> {{ trans_choice('admin.stats_widget.photos', $album->photos_count) }} &middot;
@if ($album->min_metadata_version < $current_metadata_version)
<span class="text-danger">@lang('admin.metadata_upgrade.can_be_upgraded', ['version_from' => $album->min_metadata_version, 'version_to' => $current_metadata_version])</span>
@else
<span class="text-success">@lang('admin.metadata_upgrade.is_up_to_date')</span>
@endif
</p>
</td>
<td class="text-right">
<div class="btn-group">
@if ($album->min_metadata_version < $current_metadata_version)
<a href="{{ route('albums.metadata', ['id' => $album->id]) }}" class="btn btn-primary">@lang('admin.metadata_upgrade.upgrade_button')</a>
@endcan
</div>
</td>
</tr>

View File

@ -16,6 +16,7 @@ Auth::routes();
// Administration // Administration
Route::group(['prefix' => 'admin'], function () { Route::group(['prefix' => 'admin'], function () {
Route::get('/', 'Admin\DefaultController@index')->name('admin'); Route::get('/', 'Admin\DefaultController@index')->name('admin');
Route::get('/photo-metadata', 'Admin\DefaultController@metadataUpgrade')->name('admin.metadataUpgrade');
Route::post('quick-upload', 'Admin\DefaultController@quickUpload')->name('admin.quickUpload'); Route::post('quick-upload', 'Admin\DefaultController@quickUpload')->name('admin.quickUpload');
Route::post('settings/save', 'Admin\DefaultController@saveSettings')->name('admin.saveSettings'); Route::post('settings/save', 'Admin\DefaultController@saveSettings')->name('admin.saveSettings');
Route::post('settings/test-email', 'Admin\DefaultController@testMailSettings')->name('admin.testMailSettings'); Route::post('settings/test-email', 'Admin\DefaultController@testMailSettings')->name('admin.testMailSettings');
@ -25,6 +26,8 @@ Route::group(['prefix' => 'admin'], function () {
// Album management // Album management
Route::get('albums/{id}/analyse/{queue_token}', 'Admin\AlbumController@analyse')->name('albums.analyse'); Route::get('albums/{id}/analyse/{queue_token}', 'Admin\AlbumController@analyse')->name('albums.analyse');
Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete'); Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete');
Route::get('/albums/{id}/metadata', 'Admin\AlbumController@metadata')->name('albums.metadata');
Route::post('/albums/{id}/metadata', 'Admin\AlbumController@metadataPost')->name('albums.metadataPost');
Route::post('albums/{id}/set-group-permissions', 'Admin\AlbumController@setGroupPermissions')->name('albums.set_group_permissions'); Route::post('albums/{id}/set-group-permissions', 'Admin\AlbumController@setGroupPermissions')->name('albums.set_group_permissions');
Route::post('albums/{id}/set-user-permissions', 'Admin\AlbumController@setUserPermissions')->name('albums.set_user_permissions'); Route::post('albums/{id}/set-user-permissions', 'Admin\AlbumController@setUserPermissions')->name('albums.set_user_permissions');
Route::delete('albums/{id}/delete-redirect/{redirectId}', 'Admin\AlbumController@deleteRedirect')->name('albums.delete_redirect'); Route::delete('albums/{id}/delete-redirect/{redirectId}', 'Admin\AlbumController@deleteRedirect')->name('albums.delete_redirect');
@ -87,6 +90,9 @@ Route::get('/statistics/uploaded-12m', 'Gallery\StatisticsController@photosUploa
Route::get('a/{albumUrlAlias}', 'Gallery\AlbumController@index') Route::get('a/{albumUrlAlias}', 'Gallery\AlbumController@index')
->name('viewAlbum') ->name('viewAlbum')
->where('albumUrlAlias', '.*'); ->where('albumUrlAlias', '.*');
Route::get('exif/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@showExifData')
->name('viewExifData')
->where('albumUrlAlias', '.*');
Route::get('p/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@show') Route::get('p/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@show')
->name('viewPhoto') ->name('viewPhoto')
->where('albumUrlAlias', '.*'); ->where('albumUrlAlias', '.*');