Da das NanoAlbum https://github.com/oliworx/NanoAlbum seit längerem nicht mehr modifiziert wurde, habe ich mich mal hingesetzt und dieses erweitert.
Was ist NanoAlbum
Es ist ein PHP Script welches autom. alle Bilder im Verzeichnis als Gallerie einließt und anzeigt. Es ist extra simple gehalten und hat nur diese eine Datei. Ein paar Einstellungen kann man am Anfang des Scripts machen.
Es bietet jetzt zusätzlich:
- responsive Design
- Anzeige der Metadaten des Bildes
- swipeing / wischen auf Handy und Tablet
- modernerer Aufbau des PHP Scripts
- Download Funktion des Bildes
Ein paar Screenshots



Demo
folgt
Installation
Entpacke den Download und kopiere mit z.B. einem FTP Programm die index.php in einen Ordner (z.b. /images) auf deinem Webspace. Erstelle in diesem für jede Bildersammlung (Kategorie) einen Ordner /images/birthday oder images/hochzeit und kopiere deine Bilder hinein.
Das was schon. Rufe deine Url gefolgt mit deinem Ordner auf:
z.B. https://www.meineurl.de/images
Download
NanoAlbum 2
Code
<?php
/*
* NanoAlbum 2.0
* is a simple and small PHP photo album/gallery
* modified by Conny Henn - www.blog.hennweb.de
* last modified: 4/2025
Goals:
* KISS - keep it small and simple
* no database required
* zero configuration / little configuration
* whith download button, reconstruced code, swipe by tablet or mobile phone, responsive
* small footprint: basic functionality in just one single file
* no wasting of display area, use whole screen
* design for modern browsers, html5, css3
* responsive design for mobile devices
* save bandwidth, using Client cache where possible
* provide original photos for download and viewing
* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
* original https://github.com/oliworx/NanoAlbum
*
* Install:
* make a subdir and copy this php file inside
* for a category make a subfolder and copy your images inside
* thats all
*/
// Configuration with defaults
@include('config.php');
$defaults = [
'TITLE' => 'Album',
'THUMBNAIL_SIZE' => 160,
'MEDIUM_SIZE' => 600,
'MOBILE_SIZE' => 600,
'PRELOAD_IMAGES' => true,
'CSS_INLINE' => false,
'FOOTER' => 'powered by <a href="https://github.com/oliworx/NanoAlbum">NanoAlbum</a> | ',
'SHOW_METADATA' => true, // Display image metadata (EXIF)
'SHOW_GPS_DATA' => false // Display GPS location from EXIF (privacy consideration)
];
// Apply defaults if constants not defined
foreach ($defaults as $const => $value) {
if (!defined($const)) define($const, $value);
}
define('SELF', $_SERVER['SCRIPT_NAME']);
define('BATSU', base64_decode('R0lGODlhEgARALMAAMwzZvzy9eWZstVZgt9/n/XZ4v////LM2c8/b9lljAAAAAAAAAAAAAAAAAAAAAAAACH5BAAHAP8ALAAAAAASABEAAAQ30MhJq7046ynC7VYwDB5FIMUlktOZYqvnbuu8GQECHLd0jiWNKyZESYgX2xEYSrwoAWdvSqVGAAA7'));
/**
* Send content with caching headers if it has changed
*
* @param string $sContent Content to send
* @param int $iMaxAge Max age in seconds for cache
*/
function sendIfChanged($sContent, $iMaxAge = 60) {
// Set cache headers
$cacheHeaders = [
"Pragma: public",
"Cache-Control: max-age=" . $iMaxAge,
'Expires: ' . gmdate('D, d M Y H:i:s', time() + $iMaxAge) . ' GMT'
];
foreach ($cacheHeaders as $header) {
header($header);
}
// Generate Etag
$sEtag = md5($sContent);
// Check if client already has this version
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $sEtag) {
header('HTTP/1.1 304 Not Modified');
return;
}
// Send the content with etag
header('Etag: "' . $sEtag . '"');
echo $sContent;
}
/**
* Generate CSS for the gallery
*
* @param bool $detached Whether to send as separate file
* @return string CSS content if $detached is false
*/
function getCss($detached = false) {
$thumbSize = THUMBNAIL_SIZE;
$mobileSize = MOBILE_SIZE;
$sCss = '
body { margin: 0px; padding: 10px; font: 12px Arial, Helvetica, sans-serif; color: #222; }
img { border: 0px;}
a {text-decoration:none;}
h1, h2 {margin: 3px; display: inline;}
ul {margin: 0; padding: 0;}
li {list-style:none;display:block;float:left;overflow: hidden;}
ul.albums li {background-color: #ddd; margin: 2px; border: 2px solid #999; border-radius: 7px; font-weight:bold; vertical-align:middle; width:' . ($thumbSize + 20) . 'px; height: ' . $thumbSize . 'px; text-align:center;}
img.thumb {margin: 5px; border: 1px; border-radius: 5px; height:' . round($thumbSize * 0.75) . 'px;vertical-align:middle;box-shadow: 3px 2px 5px #aaa;}
ul.albums li img.thumb {width:' . $thumbSize . 'px;height:auto;}
div.details {text-align:center;}
.preload {max-width: 50px; max-height:50px; display:none;}
div.details img {vertical-align:middle;box-shadow: 3px 2px 5px #aaa;border-radius: 5px; max-height: 70vh;}
div.descr {font-weight:bold; margin:10px;}
a.prevnext {padding:5px 10px; font-size: 60px;color: #999; border: 1px solid #999;border-radius: 5px;box-shadow: 3px 2px 5px #aaa; background-size:118px; background-position:center; background-repeat:no-repeat;margin: 5px;}
#footer { padding: 20px; font-size: 10px; color: #999; }
#footer a {color: #999;}
div.clr {clear:both}
/* Download and Close Buttons */
.image-actions {
margin: 10px 0;
display: flex;
justify-content: center;
gap: 10px;
}
.btn {
display: inline-block;
padding: 8px 15px;
background-color: #f0f0f0;
color: #333;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
text-decoration: none;
}
.btn:hover {
background-color: #e0e0e0;
}
/* Lightbox */
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 1000;
text-align: center;
padding: 20px;
box-sizing: border-box;
}
.lightbox img {
max-width: 100%;
max-height: 90vh;
margin: 0 auto;
display: block;
}
.close-btn {
position: absolute;
top: 15px;
right: 25px;
color: white;
font-size: 35px;
font-weight: bold;
cursor: pointer;
}
/* Metadata styling */
.metadata {
margin: 20px auto;
padding: 10px;
background-color: #f5f5f5;
border-radius: 5px;
text-align: left;
max-width: 600px;
display: inline-block;
}
.meta-item {
margin: 5px 0;
padding: 3px 0;
border-bottom: 1px dotted #ddd;
}
.meta-label {
font-weight: bold;
display: inline-block;
min-width: 100px;
}
.meta-value {
color: #444;
}
/* Touch swipe support */
#photoview {
touch-action: pan-y;
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.image-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
position: relative;
}
#photoview.swiping .image-container {
transition: transform 0.2s ease-out;
}
@media screen and (max-width: ' . $mobileSize . 'px) {
body {padding: 0px;}
ul.albums li {margin: 1px; border: 1px solid #999; border-radius: 4px; font-weight:normal; width:' . ($thumbSize - 10) . 'px; height: ' . ($thumbSize - 20) . 'px;}
img.thumb { margin: 0 auto; border: none; border-radius: 0; box-shadow:none; height:auto; max-width:' . $thumbSize . 'px; max-height:' . $thumbSize . 'px;}
ul li {margin: 1px 0px 0px 1px; width: ' . round($thumbSize * 0.62) . 'px; height: ' . round($thumbSize * 0.62) . 'px;}
ul li img.thumb {margin: 0 -5px; }
a.album, #footer {font-size: 9px; }
div.details img {vertical-align:middle;box-shadow: 2px 1px 3px #aaa; max-width: 90%; max-height: 60vh;}
a.prevnext {padding:15px 10px; font-size: 20px; color: #999; border-radius: 4px; box-shadow: 2px 1px 3px #aaa;}
.image-container {
gap: 5px;
}
.metadata {
padding: 5px;
font-size: 11px;
margin: 10px auto;
}
.meta-label {
min-width: 80px;
}
.btn {
padding: 6px 10px;
font-size: 11px;
}
}
@media screen and (max-width: 330px) {
h1, h2 {font-size:15px}
div.details img {vertical-align:middle;box-shadow: none; max-width: 80%; max-height: 50vh;}
div.descr {font-size:10px; font-weight:bold; margin:5px;}
a.prevnext {padding:10px 5px;}
}
';
if ($detached) {
header('Content-Type: text/css; charset=UTF-8', true);
sendIfChanged($sCss, 5000);
exit();
}
return $sCss;
}
/**
* Create full HTML page with content
*
* @param string $sContent The main content HTML
* @param string $sHeadline Additional headline text
* @param string $sTitle Optional page title
*/
function getPage($sContent, $sHeadline = '', $sTitle = '') {
header('Content-Type: text/html; charset=UTF-8', true);
// Decide on CSS inclusion method
$sCssTag = CSS_INLINE
? '<style type="text/css">' . getCss() . '</style>'
: '<link type="text/css" rel="stylesheet" href="' . SELF . '?css">';
// Use passed title or default
$pageTitle = $sTitle ?: TITLE;
// JavaScript for swipe functionality on touch devices and lightbox
$script = <<<'EOT'
<script>
document.addEventListener('DOMContentLoaded', function() {
// Lightbox functionality
var lightbox = document.getElementById('lightbox');
var mainImage = document.getElementById('main-image');
var lightboxImg = document.getElementById('lightbox-img');
var closeBtn = document.getElementById('close-btn');
if (mainImage) {
mainImage.addEventListener('click', function(e) {
e.preventDefault();
var fullsizeSrc = this.parentNode.getAttribute('href');
lightboxImg.src = fullsizeSrc;
lightbox.style.display = 'block';
document.body.style.overflow = 'hidden'; // Prevent scrolling
});
}
if (closeBtn) {
closeBtn.addEventListener('click', function() {
lightbox.style.display = 'none';
document.body.style.overflow = 'auto'; // Enable scrolling
});
}
// Click outside image to close lightbox
if (lightbox) {
lightbox.addEventListener('click', function(e) {
if (e.target === lightbox) {
lightbox.style.display = 'none';
document.body.style.overflow = 'auto';
}
});
}
// Touch swipe support for photo view
var photoview = document.getElementById('photoview');
if (!photoview) return;
var imageContainer = document.querySelector('.image-container');
var startX = 0;
var currentX = 0;
var touchInProgress = false;
var threshold = 80; // Minimum distance to consider as a swipe
var prevUrl = photoview.getAttribute('data-prev');
var nextUrl = photoview.getAttribute('data-next');
// Touch event handlers
imageContainer.addEventListener('touchstart', function(e) {
startX = e.touches[0].clientX;
currentX = startX;
touchInProgress = true;
imageContainer.style.transition = '';
});
imageContainer.addEventListener('touchmove', function(e) {
if (!touchInProgress) return;
currentX = e.touches[0].clientX;
var diff = currentX - startX;
// If we have a previous or next image based on swipe direction
if ((diff > 0 && prevUrl) || (diff < 0 && nextUrl)) {
e.preventDefault(); // Prevent scrolling when swiping
// Move the container based on swipe
var translate = Math.min(Math.abs(diff), 100) * Math.sign(diff);
imageContainer.style.transform = 'translateX(' + translate + 'px)';
}
});
function handleTouchEnd(e) {
if (!touchInProgress) return;
touchInProgress = false;
var diff = currentX - startX;
// Add transition for smooth reset
imageContainer.style.transition = 'transform 0.3s ease-out';
// Reset transform
imageContainer.style.transform = '';
// Check if the swipe was long enough
if (Math.abs(diff) >= threshold) {
if (diff > 0 && prevUrl) {
window.location.href = prevUrl;
} else if (diff < 0 && nextUrl) {
window.location.href = nextUrl;
}
}
}
imageContainer.addEventListener('touchend', handleTouchEnd);
imageContainer.addEventListener('touchcancel', handleTouchEnd);
// Add keyboard navigation
document.addEventListener('keydown', function(e) {
// Don't handle keyboard events when lightbox is open
if (lightbox && lightbox.style.display === 'block') {
if (e.key === 'Escape') {
lightbox.style.display = 'none';
document.body.style.overflow = 'auto';
}
return;
}
if (e.key === 'ArrowLeft' && prevUrl) {
window.location.href = prevUrl;
} else if (e.key === 'ArrowRight' && nextUrl) {
window.location.href = nextUrl;
}
});
});
</script>
EOT;
$sHtml = '<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>' . $pageTitle . '</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
' . $sCssTag . '
</head>
<body>
<header>
<div id="header">
<h1><a accesskey="1" href="./">' . TITLE . '</a>' . $sHeadline . '</h1>
</div>
</header>
' . $sContent . '
<footer>
<div id="footer">
' . FOOTER . '
<a href="http://blog.hennweb.de">modified by Conny Henn</a> |
</div>
</footer>
' . $script . '
</body>
</html>';
sendIfChanged($sHtml);
}
/**
* Faster version of imagecopyresampled for better performance
*/
function fastimagecopyresampled(&$dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h, $quality = 3) {
if (empty($src_image) || empty($dst_image) || $quality <= 0) {
return false;
}
if ($quality < 5 && (($dst_w * $quality) < $src_w || ($dst_h * $quality) < $src_h)) {
$temp = imagecreatetruecolor($dst_w * $quality + 1, $dst_h * $quality + 1);
imagecopyresized($temp, $src_image, 0, 0, $src_x, $src_y, $dst_w * $quality + 1, $dst_h * $quality + 1, $src_w, $src_h);
imagecopyresampled($dst_image, $temp, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $dst_w * $quality, $dst_h * $quality);
imagedestroy($temp);
} else {
imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
}
return true;
}
/**
* Create and serve a thumbnail image
*
* @param string $filepath Path to the original image
* @param int $size Desired size of the thumbnail
*/
function getThumbImage($filepath, $size = THUMBNAIL_SIZE) {
$path = dirname($filepath);
$file = basename($filepath);
$thumbspath = $path . '/.thumbs/' . $size;
$thumbfilename = $thumbspath . '/' . $file;
// Create thumbnail if it doesn't exist
if (!is_file($thumbfilename)) {
// Check image type
if (stristr($file, ".jpg") || stristr($file, ".jpeg")) {
$src = @imagecreatefromjpeg($filepath);
if (!$src) {
header("Content-Type: image/gif", false);
die(BATSU);
}
} else {
die('Format not supported');
}
// Get image dimensions and calculate new dimensions
list($width, $height) = getimagesize($filepath);
if ($width > $height) {
// Landscape
$newwidth = $size;
$newheight = round(($height / $width) * $size);
} else {
// Portrait
$newheight = $size;
$newwidth = round(($width / $height) * $size);
}
// Create resized image
$tmp = imagecreatetruecolor($newwidth, $newheight);
fastimagecopyresampled($tmp, $src, 0, 0, 0, 0, $newwidth, $newheight, $width, $height);
// Ensure thumbnail directory exists
if (!is_dir($thumbspath)) {
if (!@mkdir($thumbspath, 0755, true)) {
die('Unable to create thumbnail directory, please check permissions');
}
}
// Save thumbnail with interlacing for better loading
imageinterlace($tmp, true);
if (!imagejpeg($tmp, $thumbfilename, 85)) {
header("Content-Type: image/gif", false);
die(BATSU);
}
// Clean up
imagedestroy($src);
imagedestroy($tmp);
}
// Serve the thumbnail with cache headers
$expires = 60 * 60 * 24 * 14; // 2 weeks
header("Pragma: public");
header("Cache-Control: maxage=" . $expires);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
header("Content-Type: image/jpeg", false);
$filesize = @filesize($thumbfilename);
if ($filesize) {
header("Content-Length: " . $filesize);
}
if (!@readfile($thumbfilename)) {
header("Content-Type: text/plain", true);
die('Could not read file: ' . $thumbfilename);
}
exit();
}
/**
* Serve a file for download with proper headers
*
* @param string $filepath Path to the file
*/
function serveDownload($filepath) {
if (!file_exists($filepath)) {
header('HTTP/1.0 404 Not Found');
exit('File not found');
}
$filename = basename($filepath);
// Set headers for forced download
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
// Clean output buffer
ob_clean();
flush();
// Read file and exit
readfile($filepath);
exit();
}
/**
* Read image metadata using exif functions
*
* @param string $filepath Path to the image
* @return array Metadata array
*/
function getImageMetadata($filepath) {
$metadata = [];
// Make sure exif functions are available
if (!function_exists('exif_read_data')) {
return $metadata;
}
// Try to read EXIF data
$exif = @exif_read_data($filepath, 'ANY_TAG', true);
if (!$exif) {
return $metadata;
}
// Extract useful metadata
if (isset($exif['EXIF'])) {
// Camera information
if (isset($exif['IFD0']['Make'])) {
$metadata['Camera'] = trim($exif['IFD0']['Make']);
if (isset($exif['IFD0']['Model'])) {
$metadata['Camera'] .= ' ' . trim($exif['IFD0']['Model']);
}
}
// Date taken
if (isset($exif['EXIF']['DateTimeOriginal'])) {
$metadata['Date Taken'] = $exif['EXIF']['DateTimeOriginal'];
}
// Exposure info
if (isset($exif['EXIF']['ExposureTime'])) {
$metadata['Exposure'] = $exif['EXIF']['ExposureTime'] . 's';
}
// Aperture
if (isset($exif['EXIF']['FNumber'])) {
if (is_array($exif['EXIF']['FNumber'])) {
$fNumber = $exif['EXIF']['FNumber'][0] / $exif['EXIF']['FNumber'][1];
$metadata['Aperture'] = 'f/' . round($fNumber, 1);
} else {
$metadata['Aperture'] = 'f/' . $exif['EXIF']['FNumber'];
}
}
// ISO
if (isset($exif['EXIF']['ISOSpeedRatings'])) {
$metadata['ISO'] = 'ISO ' . $exif['EXIF']['ISOSpeedRatings'];
}
// Focal Length
if (isset($exif['EXIF']['FocalLength'])) {
if (is_array($exif['EXIF']['FocalLength'])) {
$focalLength = $exif['EXIF']['FocalLength'][0] / $exif['EXIF']['FocalLength'][1];
$metadata['Focal Length'] = round($focalLength) . 'mm';
} else {
$metadata['Focal Length'] = $exif['EXIF']['FocalLength'] . 'mm';
}
}
}
// GPS data if available and permitted
if (isset($exif['GPS']) && defined('SHOW_GPS_DATA') && SHOW_GPS_DATA) {
$gps = $exif['GPS'];
if (isset($gps['GPSLatitude']) && isset($gps['GPSLongitude']) &&
isset($gps['GPSLatitudeRef']) && isset($gps['GPSLongitudeRef'])) {
// Convert GPS coordinates to decimal format
$latDec = gpsToDecimal($gps['GPSLatitude'], $gps['GPSLatitudeRef']);
$lonDec = gpsToDecimal($gps['GPSLongitude'], $gps['GPSLongitudeRef']);
if ($latDec && $lonDec) {
$metadata['GPS'] = "($latDec, $lonDec)";
$metadata['_map_link'] = "https://www.openstreetmap.org/?mlat=$latDec&mlon=$lonDec&zoom=15";
}
}
}
return $metadata;
}
/**
* Convert GPS coordinates from EXIF format to decimal
*/
function gpsToDecimal($coordParts, $hemi) {
$degrees = count($coordParts) > 0 ? convertGpsFraction($coordParts[0]) : 0;
$minutes = count($coordParts) > 1 ? convertGpsFraction($coordParts[1]) : 0;
$seconds = count($coordParts) > 2 ? convertGpsFraction($coordParts[2]) : 0;
$decimal = $degrees + $minutes / 60 + $seconds / 3600;
// If this is South or West, make the coordinate negative
$flipHemi = strtoupper($hemi) == 'S' || strtoupper($hemi) == 'W';
return $flipHemi ? -$decimal : $decimal;
}
/**
* Helper to convert GPS fractions
*/
function convertGpsFraction($fraction) {
if (is_string($fraction)) {
$parts = explode('/', $fraction);
if (count($parts) == 2 && $parts[1] != 0) {
return $parts[0] / $parts[1];
}
return floatval($fraction);
}
if (is_array($fraction) && count($fraction) == 2) {
if ($fraction[1] != 0) {
return $fraction[0] / $fraction[1];
}
}
return 0;
}
/**
* Get image detail view HTML
*
* @param string $filepath Path to the image
* @param string &$sHeadline Reference to headline variable
* @param string &$sTitle Reference to title variable
* @return string HTML for detail view
*/
function getDetails($filepath, &$sHeadline, &$sTitle = null) {
$path = dirname($filepath);
$file = basename($filepath);
$sTitle = $file;
list($aDirs, $aImages) = getDirectory($path);
// Find previous and next images
$pref = $next = false;
$currentIndex = 0;
foreach ($aImages as $i => $sFile) {
if ($sFile == $file) {
$currentIndex = $i;
if ($i > 0) $pref = $aImages[$i - 1];
if (isset($aImages[$i + 1])) $next = $aImages[$i + 1];
break;
}
}
// Get image metadata
$metadata = getImageMetadata($filepath);
// Build HTML
$sHtml = '<div class="details" id="photoview" data-current="' . $currentIndex . '"';
// Add navigation data for touch swipe
if ($pref) {
$sHtml .= ' data-prev="' . getDetailsUrl($path . '/' . $pref) . '"';
}
if ($next) {
$sHtml .= ' data-next="' . getDetailsUrl($path . '/' . $next) . '"';
}
$sHtml .= '>';
// Show breadcrumb navigation
if ($path != '.') {
$sHeadline = ' > <a href="' . getAlbumUrl($path) . '" title="Go to album ' . $path . '">' . $path . '</a>';
}
// Image container with navigation
$sHtml .= '<div class="image-container">';
// Previous image link
if ($pref) {
$background = PRELOAD_IMAGES ? ' style="background-image: url(\'' . getThumbUrl($path . '/' . $pref, MEDIUM_SIZE) . '\')"' : '';
$sHtml .= '<a class="prevnext prev"' . $background . ' href="' . getDetailsUrl($path . '/' . $pref) . '"><</a>';
}
// Current image
$sHtml .= '<a id="-img" href="' . url_encode($filepath) . '" title="' . $file . ' (click to view fullsize)">
<img id="main-image" alt="' . $file . '" src="' . getThumbUrl($filepath, MEDIUM_SIZE) . '">
</a>';
// Next image link
if ($next) {
$background = PRELOAD_IMAGES ? ' style="background-image: url(\'' . getThumbUrl($path . '/' . $next, MEDIUM_SIZE) . '\')"' : '';
$sHtml .= '<a class="prevnext next"' . $background . ' href="' . getDetailsUrl($path . '/' . $next) . '">></a>';
}
$sHtml .= '</div>'; // Close image-container
// Image description with filename
$sHtml .= '<div class="descr">' . $file . '</div>';
// Download button and fullsize view info
$sHtml .= '<div class="image-actions">
<a href="' . url_encode($filepath) . '" download="' . $file . '" class="btn">Download</a>
<span>Click image to view fullsize<br>or swipe left/right</span>
</div>';
// Add metadata section
if (!empty($metadata)) {
$sHtml .= '<div class="metadata">';
foreach ($metadata as $key => $value) {
// Skip internal keys (starting with underscore)
if ($key[0] === '_') continue;
// Handle special case for GPS with map link
if ($key === 'GPS' && isset($metadata['_map_link'])) {
$sHtml .= '<div class="meta-item"><span class="meta-label">' . $key . ':</span>
<a href="' . $metadata['_map_link'] . '" target="_blank" rel="noopener">' . $value . '</a></div>';
} else {
$sHtml .= '<div class="meta-item"><span class="meta-label">' . $key . ':</span> <span class="meta-value">' . $value . '</span></div>';
}
}
$sHtml .= '</div>';
}
// Add lightbox for fullsize view
$sHtml .= '<div id="lightbox" class="lightbox">
<span id="close-btn" class="close-btn">×</span>
<img id="lightbox-img" src="" alt="Fullsize image">
</div>';
$sHtml .= '</div>'; // Close photoview
return $sHtml;
}
/**
* Get directory entries sorted
*
* @param string $directory Directory path
* @return array Sorted directory entries
*/
function getDir($directory) {
$aDir = [];
if (empty($directory)) {
$directory = './';
}
$handle = @opendir($directory);
if (!$handle) {
return $aDir;
}
while (false !== ($sFile = readdir($handle))) {
if ($sFile[0] != '.') {
$aDir[] = $sFile;
}
}
closedir($handle);
sort($aDir);
return $aDir;
}
/**
* Get directories and images in a directory
*
* @param string $directory Directory path
* @return array Array containing arrays of directories and images
*/
function getDirectory($directory) {
$aDirs = [];
$aImages = [];
if (empty($directory)) {
$directory = '.';
}
$directory = rtrim($directory, '/') . '/';
$handle = @opendir($directory);
if (!$handle) {
return [$aDirs, $aImages];
}
while (false !== ($sFile = readdir($handle))) {
if ($sFile[0] != '.') {
$fullPath = $directory . $sFile;
if (is_dir($fullPath)) {
$aDirs[] = $sFile;
} elseif (preg_match('/\.(jpe?g)$/i', $sFile)) {
$aImages[] = $sFile;
}
}
}
closedir($handle);
sort($aDirs);
sort($aImages);
return [$aDirs, $aImages];
}
/**
* URL encode a filepath preserving slashes
*
* @param string $filepath Path to encode
* @return string Encoded path
*/
function url_encode($filepath) {
return str_replace("%2F", "/", rawurlencode($filepath));
}
/**
* Get URL for a thumbnail
*
* @param string $filepath Path to the original image
* @param int $size Thumbnail size
* @return string URL to the thumbnail
*/
function getThumbUrl($filepath, $size = THUMBNAIL_SIZE) {
$path = dirname($filepath);
$file = basename($filepath);
$thumbspath = $path . '/.thumbs/' . $size;
$thumbfilename = $thumbspath . '/' . $file;
// Return direct URL if thumbnail exists
if (is_file($thumbfilename)) {
return url_encode($thumbfilename);
}
// Return dynamic URL if thumbnail doesn't exist
$param = ($size == MEDIUM_SIZE) ? 'm' : 't';
return SELF . "?{$param}=" . urlencode($filepath);
}
/**
* Get URL for an album
*
* @param string $sPath Album path
* @return string URL to the album
*/
function getAlbumUrl($sPath) {
return SELF . '?a=' . urlencode($sPath);
}
/**
* Get URL for image details
*
* @param string $filepath Path to the image
* @return string URL to the image details
*/
function getDetailsUrl($filepath) {
return SELF . '?d=' . urlencode($filepath) . '#img';
}
/**
* Get a representative thumbnail for an album
*
* @param string $directory Album directory
* @return string HTML for the album thumbnail
*/
function getAlbumThumbnail($directory) {
list(, $aImages) = getDirectory($directory);
if (count($aImages) > 0) {
$iMiddle = floor(count($aImages) / 2);
return '<img class="thumb" alt="' . htmlspecialchars($aImages[$iMiddle]) . '" src="' .
getThumbUrl($directory . '/' . $aImages[$iMiddle]) . '">';
}
return '';
}
/**
* Get album view HTML
*
* @param string $directory Album directory
* @param string &$sHeadline Reference to headline variable
* @param string &$sTitle Reference to title variable
* @return string HTML for album view
*/
function getAlbum($directory, &$sHeadline, &$sTitle = null) {
$sAlbums = '';
$sThumbs = '';
list($aDirs, $aImages) = getDirectory($directory);
// Set title and headline
if ($directory) {
$sTitle = $directory;
$sHeadline = ' >' . $directory;
$directory .= '/';
}
// Build directory listing
foreach ($aDirs as $sFile) {
$sAlbums .= '<li><a class="album" href="' . getAlbumUrl($directory . $sFile) . '">' .
htmlspecialchars($sFile) . '<br>' . getAlbumThumbnail($directory . $sFile) . '</a></li>';
}
// Build image thumbnails
foreach ($aImages as $sFile) {
$sThumbs .= '<li><a href="' . getDetailsUrl($directory . $sFile) . '"><img class="thumb" alt="' .
htmlspecialchars($sFile) . '" title="' . htmlspecialchars($sFile) . '" src="' .
getThumbUrl($directory . $sFile) . '"></a></li>';
}
// Finalize HTML
if ($sAlbums) {
$sAlbums = '<ul class="albums">' . $sAlbums . '</ul><div class="clr"></div>';
}
if ($sThumbs) {
$sThumbs = '<ul>' . $sThumbs . '</ul><div class="clr"></div>';
}
return $sAlbums . $sThumbs;
}
// MAIN EXECUTION FLOW
$sHeadline = $sTitle = '';
// Route request to appropriate handler
if (isset($_REQUEST['t'])) {
// Generate thumbnail
getThumbImage($_REQUEST['t']);
} elseif (isset($_REQUEST['m'])) {
// Generate medium image
getThumbImage($_REQUEST['m'], MEDIUM_SIZE);
} elseif (isset($_GET['css'])) {
// Serve CSS
getCss(true);
} elseif (isset($_REQUEST['download'])) {
// Serve file for download
serveDownload($_REQUEST['download']);
} elseif (isset($_REQUEST['d'])) {
// Show image details
$sHtml = getDetails($_REQUEST['d'], $sHeadline, $sTitle);
} else {
// Show album
$sHtml = getAlbum($_REQUEST['a'] ?? '', $sHeadline, $sTitle);
}
// Render page
getPage($sHtml, $sHeadline, $sTitle);
?>