diff --git a/.gitignore b/.gitignore index fb5facb..4f028ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ /_build/target -/_layouts/private-* /_site /asset /private /.jekyll-metadata /_python /Gemfile.lock -/.sass-cache +/vendor +/composer.lock +/composer.phar +/asset +/gallery \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..689797d --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Installation... + + $ php composer.phar install + +You might also need PHP 5.5. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7057684 --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "imagine/imagine": "0.5.0", + "symfony/console": "2.4.3", + "symfony/yaml": "2.4.3", + "phpexiftool/phpexiftool": "0.4.1" + } +} diff --git a/convert.php b/convert.php new file mode 100644 index 0000000..9bc31f4 --- /dev/null +++ b/convert.php @@ -0,0 +1,290 @@ + '', 'comment' => '']; + } + } + $sYaml = str_replace("''", null, Yaml::dump($aMeta, 4, 2)); + file_put_contents($sDir . '/meta.yaml', $sYaml); +} +// writeMetaYaml('C:\Users\Rik\Downloads\Blog\jekyll-gallery\in'); + +$oConsole = new Application(); + +$oConsole + ->register('run') + ->setDefinition([ + new InputOption('export', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Target image export sizes'), + new InputOption('layout', null, InputOption::VALUE_REQUIRED, 'Rendering layout for individual images', 'gallery-photo'), + new InputOption('importdir', null, InputOption::VALUE_REQUIRED, 'Directory to scan for images'), + new InputArgument('name', null, InputArgument::REQUIRED, 'Gallery name'), + new InputArgument('assetdir', InputArgument::OPTIONAL, 'Asset directory for exported images', 'asset/gallery'), + new InputArgument('mdowndir', InputArgument::OPTIONAL, 'Markdown directory for dumping individual photo details', 'gallery'), + ]) + ->setDescription('Parse a YAML-like gallery configuration and export it.') + ->setHelp(' + The export option will accept values like: + 200x110 - photo will outset the boundary with the dimensions being 200x110 + 1280 - photo will inset with the largest dimension being 1280 + ') + ->setCode( + function (InputInterface $oInput, OutputInterface $oOutput) { + // Get input arguments and options + $sGallery = $oInput->getArgument('name'); + $sAssetPath = $oInput->getArgument('assetdir') . '/' . $sGallery; + $sRenderPath = $oInput->getArgument('mdowndir') . '/' . $sGallery; + $sImportDir = $oInput->getOption('importdir'); + $sLayout = $oInput->getOption('layout'); + $sExports = $oInput->getOption('export'); + + $oImagine = new Imagine\Gd\Imagine(); + + // Initialize directories + if (!is_dir($sAssetPath)) { + mkdir($sAssetPath, 0700, true); + } + if (!is_dir($sRenderPath)) { + mkdir($sRenderPath, 0700, true); + } + + if (isset($sImportDir)) { + // Use provided directory + $sImportDir = rtrim($oInput->getOption('importdir'), '/\\'); + } else { + // Get directory and metadata from yaml + $sStdin = stream_get_contents(STDIN); + $aYaml = Yaml::parse($sStdin); + $sImportDir = rtrim($aYaml['dir'], '"\'/\\'); + $aMeta = $aYaml['files']; + if (!isset($aYaml['all'])) { + $aFiles = array_keys($aYaml['files']); + } + } + + // Scan import directory for images + if (!isset($aFiles)) { + $aFiles = array_map('basename', glob($sImportDir . '/*.jpg')); + } + + // Loop over files + $aPhotos = []; + foreach ($aFiles as $i => $sFile) { + // Build photo information + $aPhoto = [ + 'path' => $sImportDir . '/' . $sFile, + 'ordering' => $i, + 'name' => isset($aMeta[$sFile]['name']) ? $aMeta[$sFile]['name'] : null, + 'comment' => isset($aMeta[$sFile]['comment']) ? $aMeta[$sFile]['comment'] : null + ]; + + // Generate id from file contents + $aPhoto['id'] = substr(sha1_file($aPhoto['path']), 0, 7); + if (isset($aPhoto['title'])) { + $aPhoto['id'] .= '-' . preg_replace('/(-| )+/', '-', preg_replace('/[^a-z0-9 ]/i', '-', preg_replace('/\'/', '', strtolower(preg_replace('/\p{Mn}/u', '', Normalizer::normalize($aPhoto['title'], Normalizer::FORM_KD)))))); + } + + // Parse selected EXIF data + $aPhoto['exif'] = exif_read_data($aPhoto['path']); + if (isset($aPhoto['exif']['GPSLongitude'])) { + $aPhoto = array_merge($aPhoto, [ + 'longitude' => coordinateToDegrees($aPhoto['exif']['GPSLongitude'], $aPhoto['exif']['GPSLongitudeRef']), + 'latitude' => coordinateToDegrees($aPhoto['exif']['GPSLatitude'], $aPhoto['exif']['GPSLatitudeRef']), + 'altitude' => fractionToFloat($aPhoto['exif']['GPSAltitude']), + 'direction' => fractionToFloat($aPhoto['exif']['GPSImgDirection'])]); + } + $aPhoto['date'] = new DateTime($aPhoto['exif']['DateTimeOriginal']); + $aPhotos[] = $aPhoto; + } + + // Manipulate + foreach ($aPhotos as $i => $aPhoto) { + $oOutput->write('' . $aPhoto['id'] . ''); + $aPhoto['sizes'] = []; + + // Image exports + if (0 < count($sExports)) { + $oSourceJpg = $oImagine->open($aPhoto['path']); + if (isset($aPhoto['exif']['Orientation'])) { + switch ($aPhoto['exif']['Orientation']) { + case 2: + $oSourceJpg->mirror(); + break; + case 3: + $oSourceJpg->rotate(180); + break; + case 4: + $oSourceJpg->rotate(180)->mirror(); + break; + case 5: + $oSourceJpg->rotate(90)->mirror(); + break; + case 6: + $oSourceJpg->rotate(90); + break; + case 7: + $oSourceJpg->rotate(-90)->mirror(); + break; + case 8: + $oSourceJpg->rotate(-90); + break; + } + } + + $oSourceSize = $oSourceJpg->getSize(); + $oOutput->writeln(' [' . $oSourceSize->getWidth() . 'x' . $oSourceSize->getHeight() . ']...'); + + foreach ($sExports as $sExport) { + $oOutput->write(' ' . $sExport . ''); + + if (false !== strpos($sExport, 'x')) { + list($iW, $iH) = explode('x', $sExport); + $sExportImage = $oSourceJpg->thumbnail( + new \Imagine\Image\Box($iW, $iH), + \Imagine\Image\ImageInterface::THUMBNAIL_OUTBOUND + ); + } else { + if ('w' == substr($sExport, -1)) { + $iX = (int) $sExport; + $iY = ($iX * $oSourceSize->getHeight()) / $oSourceSize->getWidth(); + } elseif ('h' == substr($sExport, -1)) { + $iY = (int) $sExport; + $iX = ($iY * $oSourceSize->getWidth()) / $oSourceSize->getHeight(); + } elseif ($oSourceSize->getWidth() == max($oSourceSize->getWidth(), $oSourceSize->getHeight())) { + $iX = (int) $sExport; + $iY = ($iX * $oSourceSize->getHeight()) / $oSourceSize->getWidth(); + } elseif ($oSourceSize->getHeight() == max($oSourceSize->getWidth(), $oSourceSize->getHeight())) { + $iY = (int) $sExport; + $iX = ($iY * $oSourceSize->getWidth()) / $oSourceSize->getHeight(); + } + $sExportImage = $oSourceJpg->thumbnail( + new \Imagine\Image\Box(ceil($iX), ceil($iY)), + \Imagine\Image\ImageInterface::THUMBNAIL_INSET + ); + } + + $sExportsize = $sExportImage->getSize(); + + $aPhoto['sizes'][$sExport] = [ + 'width' => $sExportsize->getWidth(), + 'height' => $sExportsize->getHeight(), + ]; + + $oOutput->writeln(' [' . $sExportsize->getWidth() . 'x' . $sExportsize->getHeight() . ']'); + $sExportPath = $sAssetPath . '/' . $aPhoto['id'] . '~' . $sExport . '.jpg'; + + // Write converted image + file_put_contents( + $sExportPath, + $sExportImage->get('jpeg', ['quality' => 90]) + ); + + touch($sExportPath, $aPhoto['date']->getTimestamp()); + $sExportImage = null; + } + $oSourceJpg = null; + } + + $oOutput->write(' markdown'); + $aMatter = [ + 'layout' => $sLayout, + 'title' => isset($aPhoto['title']) ? $aPhoto['title'] : null, + 'date' => $aPhoto['date']->format('Y-m-d H:i:s'), + 'ordering' => $aPhoto['ordering'] + ]; + + if (isset($aPhoto['exif']['Make'])) { + $aMatter['exif'] = [ + 'make' => $aPhoto['exif']['Make'], + 'model' => $aPhoto['exif']['Model'], + 'aperture' => $aPhoto['exif']['COMPUTED']['ApertureFNumber'], + 'exposure' => $aPhoto['exif']['ExposureTime'], + ]; + } + + if (isset($aPhotos[$i - 1])) { + $aMatter['previous'] = '/gallery/' . $sGallery . '/' . $aPhotos[$i - 1]['id']; + } + + if (isset($aPhotos[$i + 1])) { + $aMatter['next'] = '/gallery/' . $sGallery . '/' . $aPhotos[$i + 1]['id']; + } + + if (isset($aPhoto['latitude'])) { + $aMatter['location'] = [ + 'latitude' => $aPhoto['latitude'], + 'longitude' => $aPhoto['longitude'], + ]; + } + + if ($aPhoto['sizes']) { + $aMatter['sizes'] = $aPhoto['sizes']; + } + + ksort_recursive($aMatter); + uasort( + $aMatter['sizes'], + function ($aA, $aB) { + $iSurfaceA = $aA['width'] * $aA['height']; + $iSurfaceB = $aB['width'] * $aB['height']; + return $iSurfaceA == $iSurfaceB + ? 0 + : (($iSurfaceA > $iSurfaceB)? -1 : 1); + } + ); + + // Write Markdown file + file_put_contents( + $sRenderPath . '/' . $aPhoto['id'] . '.md', + '---' . "\n" . Yaml::dump($aMatter, 4, 2) . '---' . "\n" . ((!empty($aPhoto['comment'])) ? ($aPhoto['comment'] . "\n") : '') + ); + + $oOutput->writeln(' done'); + } + } +); + +$oConsole->run(new ArgvInput(array_merge([$_SERVER['argv'][0], 'run' ], array_slice($_SERVER['argv'], 1)))); + +function ksort_recursive(&$aArray, $mSortFlags = SORT_REGULAR) { + if (!is_array($aArray)) { + return false; + } + foreach ($aArray as &$aSubarray) { + ksort_recursive($aSubarray, $mSortFlags); + } + ksort($aArray, $mSortFlags); + return true; +} + +function coordinateToDegrees($aCoordinate, $sHemisphere) { + $aCoordinate = array_map('fractionToFloat', $aCoordinate); + $aDegrees = array_map(function ($a, $b) { + return $a / $b; + }, $aCoordinate, array(1, 60, 3600)); + $iFlip = ($sHemisphere == 'W' or $sHemisphere == 'S') ? -1 : 1; + return $iFlip * array_sum($aDegrees); +} + +function fractionToFloat($sFraction) { + $aParts = explode('/', $sFraction); + $iParts = count($aParts); + return $iParts + ? ($iParts > 1 + ? floatval($aParts[0]) / floatval($aParts[1]) + : $aParts[0]) + : 0; +} \ No newline at end of file diff --git a/gps-jpg2raw.php b/gps-jpg2raw.php new file mode 100644 index 0000000..ee5f79d --- /dev/null +++ b/gps-jpg2raw.php @@ -0,0 +1,101 @@ + $sFile) { + printf("Processing %s (%d/%d)...\n", $sFile, $i, $iFiles); + + // Regenerate JPG and DNG file paths + $sJpgPath = sprintf('%s%s%s.jpg', $sDir, DIRECTORY_SEPARATOR, $sFile); + $sDngPath = sprintf('%s%s%s.dng', $sDir, DIRECTORY_SEPARATOR, $sFile); + + // Read from JPG file + printf(" > Reading from JPG file.\n"); + $oFileEntity = $oReader->files($sJpgPath)->first(); + $oReader->reset(); + + // Extract desired GPS tags + $aData = $aMetadataBag = array(); + $aKeys = array( + 'GPS:GPSAltitude' => 'altitude', + 'GPS:GPSLongitude' => 'longitude', + 'GPS:GPSLatitude' => 'latitude', + 'GPS:GPSImgDirection' => 'direction'); + + // Loop over all metadata + printf(" > Parsing metadata.\n"); + foreach ($oFileEntity as $oMetaData) { + $oTag = $oMetaData->getTag(); + $sTag = $oTag->getTagname(); + $oValue = $oMetaData->getValue(); + + // Store desired tags + if (array_key_exists($sTag, $aKeys)) { + $aData[$aKeys[$sTag]] = $oValue; + } + + // Store all GPS tags in metadata bag + if (strpos($sTag, 'GPS:') !== false) { + $aMetadataBag[] = $oMetaData; + } + } + + // Check presence of GPS data + if (empty($aData)) { + printf(" > No GPS data present!\n"); + continue; + } + + // Convert values to floats + $aData = array_map(function ($sValue) { + return floatval($sValue->asString()); + }, $aData); + + // Write to DNG file + printf(" > Writing to DNG file.\n"); + $oWriter->write($sDngPath, new MetadataBag($aMetadataBag)); +} \ No newline at end of file