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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"require": {
"xp-framework/compiler": "^9.3",
"xp-framework/imaging": "^11.0",
"xp-framework/imaging": "^11.1",
"xp-framework/command": "^12.0",
"xp-framework/networking": "^10.4",
"xp-forge/marshalling": "^2.4",
Expand Down
12 changes: 9 additions & 3 deletions src/main/handlebars/layout.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@
left: 0;
font-size: .9rem;
display: grid;
grid-template-columns: repeat(6, max-content);
grid-template-columns: repeat(8, max-content);
gap: 1px;
justify-content: center;
pointer-events: none;
Expand Down Expand Up @@ -761,8 +761,12 @@
border-radius: 0;
}

.meta div:nth-child(2) {
grid-column: span 3;
.meta div:nth-child(1) {
grid-column: span 2;
}

.meta div:nth-child(4) {
grid-column: span 4;
}

.meta div {
Expand Down Expand Up @@ -811,8 +815,10 @@
<div class="image">
<img alt="Lightbox" width="100%">
<div class="meta">
<div><output name="datetime"></output></div>
<div><output name="make"></output></div>
<div><output name="model"></output></div>
<div><output name="lensmodel"></output></div>
<div>ISO <output name="isospeedratings"></output></div>
<div><output name="focallength"></output> mm</div>
<div><output name="aperturefnumber"></output></div>
Expand Down
31 changes: 19 additions & 12 deletions src/main/js/lightbox.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
class Lightbox {

#meta($meta, dataset) {
if ('' !== (dataset.make ?? '')) {
$meta.querySelectorAll('output').forEach($o => $o.value = dataset[$o.name]);
$meta.style.visibility = 'visible';
} else {
$meta.style.visibility = 'hidden';
}
}

/** Opens the given lightbox, loading the image and filling in meta data */
#open($target, $link, offset) {
const $full = $target.querySelector('img');
const $img = $link.querySelector('img');

// Use opening image...
$full.src = $img.src;
$target.dataset.offset = offset;
$target.showModal();

// Overlay meta data if present
const $meta = $target.querySelector('.meta');
if ('' !== ($img.dataset.make ?? '')) {
$meta.querySelectorAll('output').forEach($o => $o.value = $img.dataset[$o.name]);
$meta.style.visibility = 'visible';
} else {
$meta.style.visibility = 'hidden';
}

// ...then replace by larger version
this.#meta($target.querySelector('.meta'), $img.dataset);
$target.dataset.offset = offset;
$full.src = $link.href;
}

#navigate($target, $link, offset) {
this.#meta($target.querySelector('.meta'), $link.querySelector('img').dataset);
$target.dataset.offset = offset;
$target.querySelector('img').src = $link.href;
}

/** Attach all of the given elements to open the lightbox specified by the given DOM element */
attach(selector, $target) {
$target.addEventListener('click', e => {
Expand All @@ -42,7 +49,7 @@ class Lightbox {

e.stopPropagation();
if (offset >= 0 && offset < selector.length) {
this.#open($target, selector.item(offset), offset);
this.#navigate($target, selector.item(offset), offset);
}
});

Expand Down Expand Up @@ -72,7 +79,7 @@ class Lightbox {

e.stopPropagation();
if (offset >= 0 && offset < selector.length) {
this.#open($target, selector.item(offset), offset);
this.#navigate($target, selector.item(offset), offset);
}
});

Expand Down
5 changes: 4 additions & 1 deletion src/main/php/de/thekid/dialog/import/Cover.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
/** Imports the cover image */
class Cover extends Source {

/** Returns this source's name */
public function name(): string { return '@cover'; }

public function entryFrom(Description $description): array<string, mixed> {
$date= $description->meta['date'];
return [
Expand All @@ -15,7 +18,7 @@ public function entryFrom(Description $description): array<string, mixed> {
'title' => $description->meta['title'],
'keywords' => $description->meta['keywords'] ?? [],
'content' => $description->content,
'locations' => [...$description->locations(($date instanceof Date ? $date : new Date($date)->getTimeZone())],
'locations' => [...$description->locations(($date instanceof Date ? $date : new Date($date))->getTimeZone())],
'is' => ['cover' => true],
];
}
Expand Down
22 changes: 2 additions & 20 deletions src/main/php/de/thekid/dialog/import/LocalDirectory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

use Generator;
use de\thekid\dialog\processing\{Files, Images, Videos, ResizeTo};
use io\{File, Folder};
use lang\{Throwable, IllegalArgumentException};
use lang\Throwable;
use util\cmd\{Command, Arg};
use util\log\Logging;
use webservices\rest\{Endpoint, RestUpload};
Expand All @@ -19,29 +18,12 @@
* - cover.md: The image to use for the cover page
*/
class LocalDirectory extends Command {
private static $implementations= [
'content.md' => new Content(...),
'journey.md' => new Journey(...),
'cover.md' => new Cover(...),
];
private $source, $api;

/** Sets origin folder, e.g. `./imports/album` */
#[Arg(position: 0)]
public function from(string $origin): void {
foreach (self::$implementations as $source => $implementation) {
$file= new File($origin, $source);
if (!$file->exists()) continue;

$this->source= $implementation(new Folder($origin), $file);
return;
}

throw new IllegalArgumentException(sprintf(
'Cannot locate any of [%s] in %s',
implode(', ', array_keys(self::$implementations)),
$origin
));
$this->source= Source::in($origin);
}

/** Sets API url, e.g. `http://user:pass@localhost:8080/api` */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/de/thekid/dialog/import/LookupWeather.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function execute(Endpoint $api) {
// Infer date range from first and last images
$dates= [];
foreach ($this->images as $image) {
$dates[]= new Date(strtr($image['meta']['dateTime'], ['+00:00' => '']), $tz);
$dates[]= new Date($image['meta']['dateTime'], $tz);
}
usort($dates, fn($a, $b) => $b->compareTo($a));
$first= current($dates);
Expand Down
40 changes: 28 additions & 12 deletions src/main/php/de/thekid/dialog/import/Source.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ public function __construct(
$this->name= $this->origin->dirname;
}

/** Creates a source from a given origin folder */
public static function in(string|Folder $origin): self {
static $implementations= [
'content.md' => new Content(...),
'journey.md' => new Journey(...),
'cover.md' => new Cover(...),
];

foreach ($implementations as $source => $new) {
$file= new File($origin, $source);
if ($file->exists()) return $new($origin instanceof Folder ? $origin : new Folder($origin), $file);
}

throw new IllegalArgumentException(sprintf(
'Cannot locate any of [%s] in %s',
implode(', ', array_keys($implementations)),
$origin
));
}

/** Returns this source's name */
public function name(): string { return $this->name; }

Expand All @@ -29,26 +49,22 @@ public function parent(): ?string { return strstr($this->name(), '/', true) ?: n
/** Sets a parent for this source */
public function nestedIn(string $parent): self { $this->name= $parent.'/'.$this->name; return $this; }

/** Returns this source's origin */
public function origin(): Folder { return $this->origin; }

/** Yields all the media files in this source */
protected function mediaIn(Files $files): iterable {
static $processed= '/^(thumb|preview|full|video|screen)-/';

$images= [];
foreach ($this->entry['images'] ?? [] as $image) {
$images[$image['name']]= $image;
}

foreach ($this->origin->entries() as $path) {
$name= $path->name();
if ($path->isFile() && !preg_match($processed, $name) && ($processing= $files->processing($name))) {
$file= $path->asFile();
$name= $file->filename;

if (!isset($images[$name]) || $file->lastModified() > $images[$name]['modified']) {
yield new UploadMedia($this->entry['slug'], $file, $processing);
}
unset($images[$name]);
foreach ($files->in($this->origin) as $file => $processing) {
$name= $file->filename;
if (!isset($images[$name]) || $file->lastModified() > $images[$name]['modified']) {
yield new UploadMedia($this->entry['slug'], $file, $processing);
}
unset($images[$name]);
}

foreach ($images as $rest) {
Expand Down
31 changes: 29 additions & 2 deletions src/main/php/de/thekid/dialog/processing/Files.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
<?php namespace de\thekid\dialog\processing;

use io\Folder;

/** @test de.thekid.dialog.unittest.FilesTest */
class Files {
private $patterns= [];
private $processed= null;

/** Maps file extensions to a processing instance */
public function matching(array<string> $extensions, Processing $processing): self {
$this->patterns['/('.implode('|', array_map(preg_quote(...), $extensions)).')$/i']= $processing;
$this->processed= null;
return $this;
}

/** Returns a (cached) pattern to match all processed files */
public function processed(): string {
if (null !== $this->processed) return $this->processed;
$prefixes= [];
foreach ($this->patterns as $processing) {
foreach ($processing->prefixes() as $prefix) {
$prefixes[$prefix]= true;
}
}
return $this->processed= '/^('.implode('|', array_keys($prefixes)).')-/';
}

/** Returns processing instance based on filename, or NULL */
public function processing(string $filename): ?Processing {
foreach ($this->patterns as $pattern => $processing) {
if (preg_match($pattern, $filename)) return $processing;
if (!preg_match($this->processed(), $filename)) {
foreach ($this->patterns as $pattern => $processing) {
if (preg_match($pattern, $filename)) return $processing;
}
}
return null;
}

/** Yields files and their associated processing in a given folder */
public function in(Folder $origin): iterable {
foreach ($origin->entries() as $path) {
if ($path->isFile() && ($processing= $this->processing($path->name()))) {
yield $path->asFile() => $processing;
}
}
}
}
14 changes: 12 additions & 2 deletions src/main/php/de/thekid/dialog/processing/Images.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?php namespace de\thekid\dialog\processing;

use img\io\MetaDataReader;
use img\io\{MetaDataReader, XMPSegment};
use io\File;

class Images extends Processing {
private const RDF= 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
private $meta= new MetaDataReader();

public function kind(): string { return 'image'; }
Expand All @@ -24,16 +25,25 @@ public function meta(File $source): array<string, mixed> {
$r+= [
'width' => $exif->width,
'height' => $exif->height,
'dateTime' => $exif->dateTime?->toString('c', self::$UTC) ?? gmdate('c'),
'dateTime' => $exif->dateTime?->toString(self::DATEFORMAT),
'make' => $exif->make,
'model' => $exif->model,
'lensModel' => $exif->lensModel,
'apertureFNumber' => $exif->apertureFNumber,
'exposureTime' => $exif->exposureTime,
'isoSpeedRatings' => $exif->isoSpeedRatings,
'focalLength' => $exif->focalLength ? $this->toRounded($exif->focalLength, precision: 1) : null,
'flashUsed' => $exif->flashUsed(),
];
}

// Merge in XMP segment
if ($xmp= $meta?->segmentsOf(XMPSegment::class)) {
foreach ($xmp[0]->document()->getElementsByTagNameNS(self::RDF, 'Description')[0]->attributes as $attr) {
$r[lcfirst($attr->name)]= $attr->value;
}
}
$r['lensModel']??= $r['lens'] ?? '(Unknown Lens)';
return $r;
} finally {
$source->close();
Expand Down
5 changes: 4 additions & 1 deletion src/main/php/de/thekid/dialog/processing/Processing.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
use util\TimeZone;

abstract class Processing {
protected static $UTC= new TimeZone('UTC');
protected const DATEFORMAT= 'd.m.Y H:i';
protected $targets= [];

/** Returns processing kind */
public abstract function kind(): string;

/** Returns prefixes used by the targets */
public function prefixes(): array<string> { return array_keys($this->targets); }

/**
* Adds a conversion target with a given prefix and conversion target.
* Fluent interface.
Expand Down
12 changes: 8 additions & 4 deletions src/main/php/de/thekid/dialog/processing/Videos.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ class Videos extends Processing {

public function __construct(private string $executable= 'ffmpeg') { }

/** Returns processing kind */
public function kind(): string { return 'video'; }

/** Returns prefixes used by the targets */
public function prefixes(): array<string> { return [...parent::prefixes(), 'video', 'screen']; }

/** Executes a given external command and returns its exit code */
private function execute(string $command, array<string> $args): void {
$p= new Process($command, $args, null, null, [STDIN, STDOUT, STDERR]);
Expand All @@ -27,7 +31,7 @@ private function execute(string $command, array<string> $args): void {
}

public function meta(File $source): array<string, mixed> {
static $MAP= [
static $mdta= [
'mdta:com.apple.quicktime.make' => 'make',
'mdta:com.apple.quicktime.model' => 'model',
'mdta:com.android.manufacturer' => 'make',
Expand All @@ -49,21 +53,21 @@ public function meta(File $source): array<string, mixed> {
// Normalize meta data from iOS and Android devices
$r= [];
foreach ($meta as $key => $value) {
if ($mapped= $MAP[$key] ?? null) {
if ($mapped= $mdta[$key] ?? null) {
$r[$mapped]= $value[0];
}
}

// Prefer original creation date from iOS, converting it to local time
if ($date= $meta['mdta:com.apple.quicktime.creationdate'][0] ?? null) {
$r['dateTime']= new Date(preg_replace('/[+-][0-9]{4}$/', '', $date))->toString('c', self::$UTC);
$r['dateTime']= new Date(preg_replace('/[+-][0-9]{4}$/', '', $date))->toString(self::DATEFORMAT);
}

// Aggregate information from movie header: Duration and creation time
// Time info is the number of seconds since 1904-01-01 00:00:00 UTC
if (isset($meta['mvhd'])) {
$r['duration']= round($meta['mvhd']['duration'] / $meta['mvhd']['scale'], 3);
$r['dateTime']??= new Date($meta['mvhd']['created'] - 2082844800)->toString('c', self::$UTC);
$r['dateTime']??= new Date($meta['mvhd']['created'] - 2082844800)->toString(self::DATEFORMAT);
}

return $r;
Expand Down
Loading
Loading