M .gitignore => .gitignore +2 -0
@@ 1,1 1,3 @@
+src/
+_build/
assets/lightbox2/
D Gemfile => Gemfile +0 -4
@@ 1,4 0,0 @@
-source "https://rubygems.org"
-
-gem 'sqlite3'
-# gem 'ratom' # https://github.com/seangeo/ratom
D Gemfile.lock => Gemfile.lock +0 -13
@@ 1,13 0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- sqlite3 (1.3.11)
-
-PLATFORMS
- ruby
-
-DEPENDENCIES
- sqlite3
-
-BUNDLED WITH
- 1.10.6
A Makefile => Makefile +36 -0
@@ 0,0 1,36 @@
+#
+# Example
+#
+# $ make title='My Photos' base='https://example.com/sub/my-photos'
+#
+# See http://purl.mro.name/Photos2Atom
+#
+
+.PHONY: all build clean large thumb
+
+_build/200/%: src/%
+ @-mkdir -p _build/200
+ @# https://www.smashingmagazine.com/2015/06/efficient-image-resizing-with-imagemagick/
+ @# https://www.linuxquestions.org/questions/linux-software-2/question-about-thumbnail-orientation-dimensions-with-imagemagick-493034/
+ convert $< -thumbnail 200x200 -auto-orient -quality 30% -strip -define png:exclude-chunk=all $@
+ @touch -r "$<" "$@"
+
+_build/1200/%: src/%
+ @-mkdir -p _build/1200
+ convert $< -resize 1200x1200\> -auto-orient -quality 82% -strip -define png:exclude-chunk=all $@
+ @touch -r "$<" "$@"
+
+build: thumb large _build/index.xml
+
+all: build
+
+clean:
+ rm -rf _build
+
+SRC := $(wildcard src/*)
+
+thumb: $(patsubst src/%,_build/200/%,${SRC})
+large: $(patsubst src/%,_build/1200/%,${SRC})
+
+_build/index.xml: ${SRC}
+ sh ./atom.sh '$(title)' '$(base)' $^ > $@
D Photos2Atom.rb => Photos2Atom.rb +0 -466
@@ 1,466 0,0 @@
-#!/usr/bin/env ruby
-# encoding: UTF-8
-#
-# iPhoto2Atom, extract images from Apple™ Photos™ libraries
-# Copyright (C) 2015-2016 Marcus Rohrmoser, http://purl.mro.name/iPhoto2Atom
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-
-if ! ARGV[4].nil? || ARGV[1].nil? || '-h' == ARGV[0] || '-?' == ARGV[0]
- s = <<EOF
-NAME
- #{__FILE__} -- turn an Apple™ Photos™ Album into an Atom image feed.
-
-SYNOPSIS
- #{__FILE__} album atom_url [atom2html.xslt] [Fotos-Mediathek.photoslibrary]
-
-DESCRIPTION
- album Fotos album name to extract as a Atom photo feed
- atom_url final Atom file url (after deployment)
- atom2html.xslt xslt transformation URL to use in <?xml-stylesheet type="text/xsl" href="assets/atom2html.xslt"?> or '-'
- Fotos-Mediathek.photoslibrary complete filesystem path, usually inside $HOME/Pictures/
-
-EXAMPLES
- #{__FILE__} 'Public Images' https://example.com/images/demo/photos.atom
- simple usage, include default assets/atom2html.xslt
-
- #{__FILE__} 'Public Images' https://example.com/images/demo/photos.atom '-'
- simple usage, no atom2html.xslt
-
- #{__FILE__} 'Public Images' https://example.com/images/demo/photos.atom foo/atom2html.xslt $HOME/Pictures/*-Mediathek.photoslibrary
- explicit atom2html.xslt, explicit Photos™ media location
-
-EOF
- $stderr.puts s
- exit
-end
-
-########################################################################
-## First comes a very simple Atom generator (API inspired by https://github.com/seangeo/ratom
-########################################################################
-
-class Object
- def to_xml
- self.to_s.gsub(/[<>&"']/, {'<'=>'<', '>'=>'>', '&'=>'&', '"'=>'"', "'"=>'''})
- end
-end
-
-class Time
- def to_xml
- self.strftime('%FT%T%z').gsub(/(\d{2})$/, ":\\1")
- end
-end
-
-module MRO
- module Atom
- class Basic
- attr_reader :authors
- def initialize
- @authors = []
- @xml = ''
- end
- def id= v
- @xml <<= "<id>#{v.to_xml}</id>"
- end
- def title= v
- @xml <<= "<title>#{v.to_xml}</title>"
- end
- def updated= v
- @xml <<= "<updated>#{v.to_xml}</updated>"
- end
- end
-
- class Feed < Basic
- attr_writer :xslt
- attr_reader :links, :entries
-
- def initialize
- super
- @links = []
- @entries = []
- yield self
- self
- end
-
- def generator= v
- @xml <<= v
- end
-
- def to_xml
- s = ''
- if @xslt
- s <<= "<?xml-stylesheet type='text/xsl' href='#{@xslt.to_xml}'?>"
- s <<= <<END_OF_XSLT
-<!--
- https://developer.mozilla.org/en/docs/XSL_Transformations_in_Mozilla_FAQ#Why_isn.27t_my_stylesheet_applied.3F
-
- Note that Firefox will override your XSLT stylesheet if your XML is
- detected as an RSS or Atom feed. A known workaround is to add a
- sufficiently long XML comment to the beginning of your XML file in
- order to "push" the <.feed> or <.rss> tag out of the first 512 bytes,
- which is analyzed by Firefox to determine if it's a feed or not. See
- the discussion on bug
- https://bugzilla.mozilla.org/show_bug.cgi?id=338621#c72 for more
- information.
-
-
- For best results serve both atom feed and xslt as 'text/xml' or
- 'application/xml' without charset specified.
--->
-END_OF_XSLT
- end
- s <<= '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:georss="http://www.georss.org/georss">'
- s <<= @xml
- s <<= self.links.join('')
- s <<= "<author>"
- s <<= self.authors.join('')
- s <<= "</author>"
- s <<= self.entries.collect{|c| c.to_xml}.join('')
- s <<= '</feed>'
- s
- end
- end
-
- class Entry < Basic
- attr_reader :title, :summary, :links, :categories
-
- def initialize
- super
- @links = []
- @categories = []
- yield self
- self
- end
-
- def title= v
- super(@title = v)
- end
- def content= v
- @xml <<= v
- end
- def summary= v
- @summary = v
- @xml <<= "<summary>#{v.to_xml}</summary>" unless v.nil?
- end
- def georss_point= v
- @xml <<= "<georss:point>#{v.to_xml}</georss:point>"
- end
- def media_thumbnail= v
- @xml <<= v
- end
-
- def to_xml
- s = '<entry>'
- s <<= @xml
- s <<= '<author>'
- s <<= self.authors.join('')
- s <<= '</author>'
- s <<= self.links.join('')
- s <<= self.categories.join('')
- s <<= '</entry>'
- s
- end
- end
-
- class Link < String
- def initialize h
- super "<link #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
- end
- end
- class Category < String
- def initialize h
- super "<category #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
- end
- end
- module Content
- class External < String
- def initialize h
- super "<content #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
- end
- end
- end
- class Person < String
- def initialize h
- super h.map{|k,v| "<#{k}>#{v.to_xml}</#{k}>"}.join(' ')
- end
- end
- class Generator < String
- def initialize h
- name = h[:name]
- h.delete :name
- super "<generator #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}>#{name.to_xml}</generator>"
- end
- end
- end
-
- module Media
- class Thumbnail < String
- def initialize h
- super "<media:thumbnail #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
- end
- end
- end
-end
-
-########################################################################
-## Then access Apple™ Photos™
-########################################################################
-
-require 'sqlite3' # http://www.rubydoc.info/gems/sqlite3/1.3.11
-
-def sh_escape str
- '"' + "#{str}".gsub(/[\\\n"]/){|s| {'\\'=>'\\\\', "\n"=>'\\n', '"'=>'\\"'}[s]} + '"'
-end
-
-module MRO
- class Photos
-
- def self.open fotosMediathekFilePath
- self.new(fotosMediathekFilePath)
- end
-
- def initialize fotosMediathekFilePath
- Dir.glob( File.join(ENV['HOME'], 'Pictures', '*.photoslibrary') ) do |d|
- fotosMediathekFilePath = d
- break
- end if fotosMediathekFilePath.nil?
- $stderr.puts "Photos Library: #{fotosMediathekFilePath}"
- t0 = Time.now
- @DB = SQLite3::Database.new(File.join(fotosMediathekFilePath, 'Database', 'Library.apdb'), :readonly => true)
- @DB.execute("ATTACH DATABASE ? AS ImageProxies", File.join(fotosMediathekFilePath, 'Database', 'ImageProxies.apdb'))
- # get & check DB version?
- @LibraryPath = fotosMediathekFilePath
- end
-
- def libraryPath
- @LibraryPath
- end
-
- def cache_atom_file atom_url
- host = atom_url.host
- path = atom_url.path
- path = File.join(path, 'photos.atom') if path.end_with?('/')
- File.join(ENV['HOME'], 'Library', 'Caches', 'name.mro.iPhoto2Atom', host, path.split('/'))
- end
-
- def find_images_for_album album_name, &block
- sql = <<END_OF_SQL
-SELECT
- version.name AS imageName
- , master.name AS masterName
- , version.latitude AS latitude
- , version.longitude AS longitude
- , datetime(version.imageDate + 978307200, 'unixepoch', 'localtime') AS imageDate -- NSDate, Jan 1st 2001
- , master.imagePath
- , master.uuid
- , master.modelId
- , version.modelId
- , version.adjustmentUuid
- , note_str.value AS description
- , img_prx_res.resourceUuid AS adjusted_resourceUuid
- , img_prx_res.fileName AS adjusted_fileName
- , version.rotation
- -- , keyword.name
-FROM RKMaster AS master
-INNER JOIN RKVersion as version
-ON version.masterId = master.modelId
-INNER JOIN RKAlbumVersion as album4version
-ON album4version.versionId = version.modelId
-INNER JOIN RKAlbum as album
-ON album.modelId = album4version.albumId
-LEFT OUTER JOIN RKVersion_stringNote AS note_str
-ON version.modelId = note_str.attachedToId
-AND 669 = note_str.keyPath -- where is this constant defined?
-LEFT OUTER JOIN ImageProxies.RKModelResource AS img_prx_res
-ON 'UNADJUSTEDNONRAW' != version.adjustmentUuid
-AND version.adjustmentUuid = img_prx_res.resourceTag
-WHERE album.name = ? -- version.name LIKE '%7665%'
-ORDER BY
- imageDate DESC
- , adjusted_resourceUuid DESC
-END_OF_SQL
-
- @DB.execute(sql, album_name) {|row| yield row}
- end
-
- def find_keywords_for_image_version_id image_version_id, &block
- sql = <<END_OF_SQL
-SELECT DISTINCT keyword.name AS keywrd
-FROM RKKeyword as keyword
-INNER JOIN RKKeywordForVersion as keyword4version
-ON keyword4version.keywordId = keyword.modelId
-AND keyword4version.versionId = ?
-ORDER BY keyword.name ASC
-END_OF_SQL
-
- @DB.execute(sql, image_version_id) {|row| yield row[0]}
- end
- end
-end
-
-########################################################################
-## Then bring them together…
-########################################################################
-
-require 'uri'
-require 'fileutils'
-require 'digest/sha1'
-
-module MRO
- class CFG
- def initialize iphoto, album_name, dst_atom_url
- @IPHOTO = iphoto
- @ALBUM_NAME = album_name
- @ATOM_URL = dst_atom_url
- # do sanity checks in case
-
- @BASE_URL = @ATOM_URL + '.'
- @ATOM_FILE = iphoto.cache_atom_file(@ATOM_URL)
- @ATOM_DIR = File.dirname @ATOM_FILE
- $stderr.puts "Atom file : #{@ATOM_FILE}"
- $stderr.puts "Atom URL : #{@ATOM_URL}"
- end
-
- def produce_atom_feed xslt_url
- # $stderr.puts "convert album '#{@ALBUM['AlbumName']}' to #{@ATOM_URL}"
-
- dir2pxls = {
- # 'original' => {:rel => :image_src},
- '1600p' => {:pxls => 1600, :quality => '50%', :rel => :enclosure},
-# '1024p' => {:pxls => 1024, :quality => '50%', :rel => :enclosure},
- '200p' => {:pxls => 200, :quality => '30%', :rel => :previewimage},
- }
- dir2pxls.keys.each {|dir| FileUtils.mkpath File.join(@ATOM_DIR, dir)}
-
- maxDate = Time.at 0
- feed = Atom::Feed.new do |f|
- f.xslt = xslt_url
- f.title = @ALBUM_NAME
- f.links << Atom::Link.new(:href => @ATOM_URL, :rel => :self, :type => 'application/atom+xml')
- f.links << Atom::Link.new(:href => @ATOM_URL, :rel => :alternate, :type => 'text/html')
- f.generator = Atom::Generator.new(:uri => 'http://purl.mro.name/Photos2Atom', :name => 'iPhoto2Atom')
- artist = {:name => 'John Doe', :uri => 'http://example.com/~jd'}
- f.authors << Atom::Person.new(artist)
- f.id = @BASE_URL # "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
- prev = nil
- @IPHOTO.find_images_for_album(@ALBUM_NAME) do |row|
- # prevent duplicates:
- next if prev == row[8]
- prev = row[8]
- # $stderr.puts "#{img[:created]} #{img['Caption']}"
- # unless 2 == row[MediaType]
- # $stderr.write "-(#{img['MediaType']})"
- # next
- # end
- # $stderr.puts "img: #{row[5]}"
- src = nil
- # use the largest adjusted image binary (if there is an adjustmend)
- src = Dir.glob(File.join(@IPHOTO.libraryPath,'resources','modelresources','*','*',row[11],row[12])).max_by {|fn| File.size(fn)} unless row[11].nil?
- src ||= File.join(@IPHOTO.libraryPath, 'Masters', row[5]) # fallback to unmodified master image
- raise src unless src.start_with?( @IPHOTO.libraryPath )
- dst_fmt = case File.extname(src)
- when /png/i then :png
- when /jpe?g/i then :jpeg
- when /jp2/i then :jpeg
- when /mov/i then :mov
- else raise "odd extension: '#{File.extname(src)}'"
- end
- if :mov == dst_fmt
- $stderr.write 'o'
- next
- end
- sha = Digest::SHA1.hexdigest(row[5]) # (destination) file extension not part of sha.
- begin
- f.entries << Atom::Entry.new do |e|
- e.id = @BASE_URL + ('#_' + sha)
- # use GUID?
- # e.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
- create_date = Time.parse row[4]
- e.updated = create_date
- maxDate = create_date if create_date > maxDate
- e.title = row[0] || row[1] # fallback to master name
- e.summary = (row[10] || '').strip # is mandatory if content/@src
- e.authors << Atom::Person.new(artist)
- # use Rating ?
- dir2pxls.each do |dir,h|
- dst = File.join(@ATOM_DIR, dir, "#{sha}.#{dst_fmt}") # (destination) file extension not part of sha.
- raise "Wrong type '#{create_date.class}'" unless create_date.kind_of? Time
- # unless FileUtils.uptodate?(dst, [src]) # wrong, compare timestamp to img[:created]
- if !File.exist?(dst) || File.mtime(dst) < create_date
- $stderr.write '+'
- FileUtils.cp(src, dst)
- # bake exif rotation into image and reset exif to normal http://www.leancrew.com/all-this/2009/04/derotating-jpegs-with-exiftool/
- rotation_exif = IO.popen("exiftool -Orientation -n '#{src}'") do |f|
- m = /^Orientation\s*:\s*(\d+)\s*$/m.match(f.read) || [nil,nil]
- m[1]
- end
-# rotation = row[11].nil? || '' == row[11] ? row[13].to_i : 0 # rotate only if unmodified
- # fallback to Photos-DB rotation if exiftool fails (usually jpeg2000)
- rotation = rotation_exif.nil? ? row[13].to_i : [0,90,180,270][(rotation_exif.to_i - 1) % 4]
- unless h[:pxls].nil?
- # @todo - keep original pngs in high-res
- # https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/sips.1.html
- cyear = [create_date.year, Time.now.year].uniq.join('-')
- properties = {
- :copyright => "© #{cyear}, #{artist[:name]}, #{artist[:uri]}",
- :artist => artist[:name],
- :description => [e.title, e.summary].collect{|s| '' == s ? nil : s}.compact.collect{|s| s.strip}.join(' / '),
- :format => dst_fmt,
- :formatOptions => h[:quality],
- }
- cmd = "sips --rotate #{rotation} #{properties.map{|k,v| "--setProperty #{k} #{sh_escape(v)}"}.join(' ')} -Z #{h[:pxls]} '#{dst}' 1>/dev/null 2>&1"
- unless system(cmd)
- $stderr.puts " failed: #{cmd}"
- FileUtils.rm dst
- end
- cmd = "exiftool -Orientation=1 -n -overwrite_original_in_place '#{dst}' 1>/dev/null 2>&1"
- $stderr.puts " failed: #{cmd}" unless system(cmd)
- # @todo optional run pngquant / optipng
- end
- FileUtils.touch dst, :mtime => create_date+1
- else
- $stderr.write '.'
- end
- e.links << Atom::Link.new(:href => (@ATOM_URL + (dir + '/') + "#{sha}.#{dst_fmt}"), :rel => h[:rel], :length => File.size(dst), :type => "image/#{dst_fmt}" )
- end
- e.links << Atom::Link.new(:href => (@ATOM_URL + ('1600p' + '/') + "#{sha}.#{dst_fmt}"), :type => "image/#{dst_fmt}" )
- e.content = Atom::Content::External.new :src => (@ATOM_URL + ('1600p' + '/') + "#{sha}.#{dst_fmt}"), :type => "image/#{dst_fmt}"
- e.media_thumbnail = Media::Thumbnail.new :url => (@ATOM_URL + ('200p' + '/') + "#{sha}.#{dst_fmt}")
- @IPHOTO.find_keywords_for_image_version_id(row[8]) do |keywor|
- e.categories << Atom::Category.new( :scheme => @BASE_URL, :term => keywor, :label => keywor )
- end
- e.georss_point = "#{row[2]} #{row[3]}" if row[2] or row[3]
- end
- rescue Exception => e
- $stderr.puts row
- $stderr.puts e
- end
- end
- f.updated = maxDate
- $stderr.write "\n"
- end
-
- File.open(@ATOM_FILE, 'w') {|dst| dst.puts feed.to_xml}
- FileUtils.touch @ATOM_FILE, :mtime => maxDate+1
- end
- end
-end
-
-photos = MRO::Photos.open ARGV[3]
-xslt_url = ('-' == ARGV[2]) ? nil : URI::parse( ARGV[2].nil? ? 'assets/atom2html.xslt' : ARGV[2] )
-
-atom_url = URI::parse ARGV[1]
-album = ARGV[0]
-MRO::CFG.new(photos, album, atom_url).produce_atom_feed(xslt_url)
M README.md => README.md +10 -15
@@ 1,28 1,23 @@
-Extract Apple™ [Photos™](http://www.apple.com/osx/photos/) (formerly
-[iPhoto™](http://www.apple.com/mac/iphoto/)) albums to
-[Atom](http://atomenabled.org/developers/syndication/) photo feeds and simple
-HTML image galleries.
+Prepare a static photo feed for publication on a webserver.
# Usage
- $ gem install bundler && bundle install
- $ [brew](http://brew.sh/) install exiftool
+ $ # put the photos or softlinks into ./src/, then
+ $ make title='My Photos' base='https://example.org/sub/my-photos'
+
$ # optional:
$ sh ./install-lightbox2.sh
- $ vim run.sh # enter album names, dst URL, deployment destination
- $ sh ./run.sh
-
-## Prerequisites
-- [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/)
+ $ rsync -avPz _build/ example.org:/var/www/.../
+ $ rsync -avPz assets/ example.org:/var/www/.../assets/
-plus the already present (on OS X):
+## Prerequisites
-- [ruby](http://www.ruby-lang.org/de/)
-- [xmllint](http://xmlsoft.org/xmllint.html)
-- [sips](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/sips.1.html)
+- [make](https://www.gnu.org/software/make/)
+- [imagemagick](https://imagemagick.org/)
- [rsync](http://rsync.samba.org/)
+- [curl](https://curl.haxx.se/)
## Appendix
M assets/atom2html.xslt => assets/atom2html.xslt +33 -19
@@ 83,30 83,30 @@
<!-- http://www.quirksmode.org/blog/archives/2013/10/initialscale1_m.html -->
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<!-- meta name="viewport" content="width=400"/ -->
- <link href="assets/style.css" rel="stylesheet" type="text/css"/>
<!-- http://lokeshdhakar.com/projects/lightbox2/#how-to-use -->
<link href="assets/lightbox2/css/lightbox.min.css" rel="stylesheet" />
+ <link href="assets/style.css" rel="stylesheet" type="text/css"/>
<title>
<xsl:value-of select="a:title"/>
</title>
<link href="photos.atom" rel="alternate" type="application/atom+xml"/>
</head>
<body id="photofeed">
+ <h1><xsl:value-of select="a:title"/></h1>
+
<ul id="photos">
<xsl:for-each select="a:entry">
+ <xsl:sort select="a:updated" order="descending"/>
<li id="{substring-after(a:id, '#')}">
<xsl:attribute name="class">
<xsl:for-each select="a:category">cat_<xsl:value-of select="generate-id(key('CategorY',@term))"/><xsl:text> </xsl:text></xsl:for-each>
</xsl:attribute>
<xsl:variable name="time" select="substring(translate(a:updated,'T',' '), 1, 16)"/>
- <a href="{a:link[@rel='enclosure']/@href}" title="{normalize-space(a:title)}" data-lightbox="foo" data-title="{normalize-space(a:title)} | {$time}">
- <span style="display:block;width:200px;height:200px;text-align:center">
- <img alt="{normalize-space(a:summary)}" src="{a:link[@rel='previewimage']/@href}"/>
- </span>
+ <a href="{a:link[@rel='enclosure']/@href}" title="{a:updated} {normalize-space(a:title)}" data-lightbox="foo" data-title="{normalize-space(a:title)} | {$time}">
+ <img alt="{normalize-space(a:summary)}" src="{a:link[@rel='previewimage']/@href}"/>
</a>
- <br class='br'/>
- <span>
+ <span style="display:none">
<span class="title"><xsl:value-of select="a:title"/></span><br class='br'/>
<span class="time"><xsl:value-of select="$time"/></span><br class='br'/>
<xsl:variable name="lat" select="substring-before(georss:point, ' ')"/>
@@ 128,6 128,20 @@
</xsl:for-each>
</ul>
+ <p style="clear:both">
+ <xsl:for-each select="a:entry/a:updated">
+ <xsl:sort select="." order="ascending"/>
+ <xsl:if test="position() = 1"><xsl:value-of select="substring(., 1, 10)"/></xsl:if>
+ </xsl:for-each>
+ -
+ <xsl:for-each select="a:entry/a:updated">
+ <xsl:sort select="." order="descending"/>
+ <xsl:if test="position() = 1"><xsl:value-of select="substring(., 1, 10)"/></xsl:if>
+ </xsl:for-each>,
+ #<xsl:value-of select="count(a:entry)"/>
+ </p>
+
+ <div style="display:none">
<h2 style="clear:left">Schlagworte</h2>
<ul id="categories" class="categories">
@@ 140,13 154,22 @@
</li>
</xsl:for-each>
</ul>
+ </div>
<!-- http://lokeshdhakar.com/projects/lightbox2/#how-to-use -->
<script src="assets/lightbox2/js/lightbox-plus-jquery.min.js" type="text/javascript"></script>
<script src="assets/script.js" type="text/javascript"></script>
- <hr style="clear:left;"/>
- <p id="footer">
+ <p id="footer" style="clear:both">
+ Powered by <a href="http://purl.mro.name/Photos2Atom">purl.mro.name/Photos2Atom</a>
+ and <a rel="license" href="https://creativecommons.org/licenses/by/2.5/"><img alt=
+ "Creative Commons License" src=
+ "assets/by.svg" style="border:0;width:88px;height:31px"/></a>
+ <span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">lightbox2</span> by <a xmlns:cc=
+ "http://creativecommons.org/ns#" href="http://lokeshdhakar.com/projects/lightbox2/" property=
+ "cc:attributionName" rel="cc:attributionURL">Lokesh Dhakar</a> is licensed under a <a rel=
+ "license" href="https://creativecommons.org/licenses/by/2.5/">Creative Commons Attribution 2.5 License</a>.
+ <br class="br"/>
<a title="Validate my Atom 1.0 feed" href="https://validator.w3.org/feed/check.cgi?url={a:link[@rel='self']/@href}">
<img alt="Valid Atom 1.0" src="assets/valid-atom.png" style="border:0;width:88px;height:31px"/>
</a><xsl:text> </xsl:text>
@@ 156,17 179,8 @@
<a href="https://jigsaw.w3.org/css-validator/check/referer?profile=css3&usermedium=screen&warning=2&vextwarning=false&lang=de">
<img alt="CSS ist valide!" src="assets/valid-css-blue-v.svg" style="border:0;width:88px;height:31px"/>
</a>
- <br class='br'/>
<!-- https://i.creativecommons.org/l/by/2.5/88x31.png -->
- Powered by <a href="http://purl.mro.name/Photos2Atom">purl.mro.name/Photos2Atom</a><br class="br"/>
- and <a rel="license" href="https://creativecommons.org/licenses/by/2.5/"><img alt=
- "Creative Commons License" src=
- "assets/by.svg" style="border:0;width:88px;height:31px"/></a>
- <span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">lightbox2</span> by <a xmlns:cc=
- "http://creativecommons.org/ns#" href="http://lokeshdhakar.com/projects/lightbox2/" property=
- "cc:attributionName" rel="cc:attributionURL">Lokesh Dhakar</a> is licensed under a <a rel=
- "license" href="https://creativecommons.org/licenses/by/2.5/">Creative Commons Attribution 2.5 License</a>.
- </p>
+ </p>
</body>
</html>
</xsl:template>
M assets/style.css => assets/style.css +22 -33
@@ 16,51 16,36 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
html {
- background-color: #222;
- color: #DDD;
+ --dx: 200px;
+ background-color: hsl(0,0%,23%);
+ color: hsl(30, 60%, 60%);
+ font-family: sans-serif;
}
ul#photos {
list-style-type: none;
- padding: 0;
margin: 0;
+ padding: 0;
}
ul#photos > li {
+ /* border: solid lightgrey 2px; */
+ display: block;
float: left;
- margin: 2.5ex;
+ height: var(--dx);
+ margin: .5ex;
padding: 0.25ex;
- border: solid lightgrey 2px;
- font-family: sans-serif;
-}
-ul#photos > li a:link { color: white; text-decoration:none; }
-ul#photos > li a:hover { color: white; text-decoration:none; }
-ul#photos > li a:active { color: white; text-decoration:none; }
-ul#photos > li a:visited { color: white; text-decoration:none; }
-
-ul#photos li a span img {
- max-width: 200px;
- max-height: 200px;
-}
-
-.title {
- font-weight: bold;
-}
-.time {
- font-size: 10pt;
- color: lightgrey;
-}
-.geo {
- font-size: 10pt;
- color: lightgrey;
+ width: var(--dx);
+ text-align: center;
}
-
-.thumb {
- width: 200px;
- height: 200px;
+ul#photos > li img {
+ max-height: var(--dx);
+ max-width: var(--dx);
}
#footer {
- background-color: #DDD;
- color: black;
+ opacity: 40%;
+}
+a:any-link {
+ color: hsl(115, 50%, 45%);
}
/* This is a workaround for Browsers that insert additional <br> tags.
@@ 85,3 70,7 @@ ul#photos.active li {
ul#photos.active li.active {
display: block;
}
+
+.lb-outerContainer {
+ background-color: hsl(30, 60%, 40%);
+}
A atom.sh => atom.sh +103 -0
@@ 0,0 1,103 @@
+#!/bin/sh
+# see http://purl.mro.name/Photos2Atom
+#
+cd "$(dirname "${0}")" || exit 1
+
+if [ "" = "${2}" ] ; then
+ # Should we enforce that $${2} is a url?
+cat <<Endofmessage
+
+Extract meta data from a set of images and write the photo feed atom xml to stdout.
+
+SYNOPSIS
+
+ $0 <Feed Name> <baseurl> [src/image.jpg]...
+
+Endofmessage
+ exit 1
+fi
+
+title="${1}"
+shift
+base="${1}"
+shift
+
+if [ "$(date -d '@123' --iso-8601=seconds 2>/dev/null)" = "1970-01-01T01:02:03+01:00" ] ; then
+ file_date_iso8601 () {
+ date -r "${1}" --iso-8601=seconds
+ }
+elif [ "$(date -r 123 +'%FT%T%z' | sed 's/..$/:&/g' 2>/dev/null)" = "1970-01-01T01:02:03+01:00" ] ; then
+ file_date_iso8601 () {
+ date -r "${1}" +'%FT%T%z' | sed 's/..$/:&/g'
+ }
+else
+ file_date_iso8601 () {
+ '1970-01-01T00:00:00+00:00'
+ }
+fi
+
+file_size () {
+ wc -c < "${0}" | tr -d ' '
+}
+
+youngest=$(ls -tr "$@" | tail -n 1)
+
+cat <<Endofmessage
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet type='text/xsl' href='assets/atom2html.xslt'?>
+<!--
+ https://developer.mozilla.org/en/docs/XSL_Transformations_in_Mozilla_FAQ#Why_isn.27t_my_stylesheet_applied.3F
+
+ Note that Firefox will override your XSLT stylesheet if your XML is
+ detected as an RSS or Atom feed. A known workaround is to add a
+ sufficiently long XML comment to the beginning of your XML file in
+ order to "push" the <.feed> or <.rss> tag out of the first 512 bytes,
+ which is analyzed by Firefox to determine if it's a feed or not. See
+ the discussion on bug
+ https://bugzilla.mozilla.org/show_bug.cgi?id=338621#c72 for more
+ information.
+
+
+ For best results serve both atom feed and xslt as 'text/xml' or
+ 'application/xml' without charset specified.
+-->
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:georss="http://www.georss.org/georss">
+ <title>${title}</title>
+ <generator uri="http://purl.mro.name/Photos2Atom">Photos2Atom</generator>
+ <id>${base}/</id>
+ <updated>$(file_date_iso8601 "${youngest}")</updated>
+ <link href="${base}/" rel="self" type="application/atom+xml"/>
+ <link href="${base}/" rel="alternate" type="text/html"/>
+ <author>
+ <name>John Doe</name>
+ <uri>http://example.com/~jd</uri>
+ </author>
+Endofmessage
+
+while [ "" != "${1}" ]
+do
+ src="${1}"
+ file="$(basename "${src}")"
+ mime="$(file --brief --mime-type "${src}")"
+ size="$(file_size "_build/200/${file}"y%)"
+
+ cat <<Endofmessage
+ <entry>
+ <id>${base}/#${file}</id>
+ <updated>$(file_date_iso8601 "${src}")</updated>
+ <title></title>
+ <summary/>
+ <content src="${base}/1200/${file}" type="${mime}"/>
+ <media:thumbnail url="${base}/200/${file}"/>
+ <!-- georss:point>47.874091670000006 12.639475000000001</georss:point -->
+ <link href="${base}/200/${file}" rel="previewimage" length="${size}" type="${mime}"/>
+ <link href="${base}/1200/${file}" rel="enclosure" length="$(file_size "_build/1200/${file}")" type="${mime}"/>
+ <link href="${base}/1200/${file}" type="${mime}"/>
+ </entry>
+Endofmessage
+ shift
+done
+
+cat <<Endofmessage
+</feed>
+Endofmessage
M doap.rdf => doap.rdf +9 -9
@@ 2,24 2,24 @@
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://usefulinc.com/ns/doap#">
<Project>
- <bug-database rdf:resource="https://gogs.mro.name/mro/iPhoto2Atom/issues"/>
- <category rdf:resource="https://l.mro.name/=/t/Atom/"/>
- <category rdf:resource="https://l.mro.name/=/t/iphoto/"/>
+ <bug-database rdf:resource="https://gogs.mro.name/mro/Photos2Atom/issues"/>
+ <category rdf:resource="https://l.mro.name/o/t/Atom/"/>
+ <category rdf:resource="https://l.mro.name/o/t/iphoto/"/>
<homepage rdf:resource="http://purl.mro.name/Photos2Atom"/>
<license rdf:resource="https://www.gnu.org/licenses/gpl-3.0.txt"/>
<maintainer rdf:resource="http://mro.name/~me"/>
- <name>iPhoto2Atom</name>
+ <name>Photos2Atom</name>
<programming-language>css</programming-language>
<programming-language>js</programming-language>
- <programming-language>ruby</programming-language>
+ <programming-language>dash</programming-language>
<programming-language>xslt</programming-language>
<repository>
<GitRepository>
- <browse rdf:resource="https://gogs.mro.name/mro/iPhoto2Atom"/>
- <location rdf:resource="https://gogs.mro.name/mro/iPhoto2Atom.git"/>
+ <browse rdf:resource="https://gogs.mro.name/mro/Photos2Atom"/>
+ <location rdf:resource="https://gogs.mro.name/mro/Photos2Atom.git"/>
</GitRepository>
</repository>
- <short-description>Extract Apple™ Photos™ (formerly iPhoto™) albums to Atom photo feeds and simple HTML image galleries.</short-description>
- <wiki rdf:resource="https://gogs.mro.name/mro/iPhoto2Atom/wiki"/>
+ <short-description>Prepare a static photo feed for publication on a webserver.</short-description>
+ <wiki rdf:resource="https://gogs.mro.name/mro/Photos2Atom/wiki"/>
</Project>
</rdf:RDF>
M run.sh => run.sh +5 -50
@@ 1,53 1,8 @@
#!/bin/sh
-#
-# iPhoto2Atom, extract images from iPhoto™ libraries
-# Copyright (C) 2015-2016 Marcus Rohrmoser, http://purl.mro.name/iPhoto2Atom
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
+cd "$(dirname "${0}")" || exit 1
-ruby --version >/dev/null || { echo "Install ruby" && exit 1 ; }
-bundle --version >/dev/null || { echo "$ gem install bundler" && exit 1 ; }
-xmllint --version 2>/dev/null || { echo "Install libxml2" && exit 1 ; }
-rsync --version >/dev/null || { echo "Install rsync" && exit 1 ; }
-exiftool -ver >/dev/null || { echo "Install exiftool, e.g. $ brew install exiftool" && exit 1 ; }
+make title='My Photos' base='https://example.org/sub/my-photos'
-cwd="$(pwd)"
-cd "$(dirname "$0")"
-script_dir="$(pwd)"
-# cd "$cwd"
-
-########################################################
-## Adjust below as appropriate
-
-bundle exec ruby Photos2Atom.rb 'Foo' "http://example.com/wherever/foo/" &
-bundle exec ruby Photos2Atom.rb 'Bar' "http://example.com/somewhereelse/bar/" &
-
-wait
-
-# put html assets in place for deployment:
-find "${HOME}/Library/Caches/name.mro.iPhoto2Atom" -type d -name "200p" -exec rsync --delete -a assets "{}/.." \;
-# run atom xml through xmllint:
-find "${HOME}/Library/Caches/name.mro.iPhoto2Atom" -type f -name "photos.atom" | while read atom
-do
- {
- xmllint --relaxng rfc4287.rng --encode utf-8 --nonet --format --output "$atom"~ "$atom" >/dev/null \
- && touch -r "$atom" "$atom"~ \
- && mv "$atom"~ "$atom"
- }
-done
-
-wait
-
-# deploy to production
-rsynv -avPz --delete "${HOME}/Library/Caches/name.mro.iPhoto2Atom/example.com/" "example.com:/var/www/.../example.com/public_html/"
+dst="example.org:/var/www/vhosts/example.org/pages/sub/my-photos"
+rsync -avP _build/ "${dst}/"
+rsync -avP assets/ "${dst}/assets/"
D sample.atom => sample.atom +0 -60
@@ 1,60 0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<?xml-stylesheet type="text/xsl" href="assets/atom2html.xslt"?>
-<!--
- https://developer.mozilla.org/en/docs/XSL_Transformations_in_Mozilla_FAQ#Why_isn.27t_my_stylesheet_applied.3F
-
- Note that Firefox will override your XSLT stylesheet if your XML is
- detected as an RSS or Atom feed. A known workaround is to add a
- sufficiently long XML comment to the beginning of your XML file in
- order to "push" the <.feed> or <.rss> tag out of the first 512 bytes,
- which is analyzed by Firefox to determine if it's a feed or not. See
- the discussion on bug
- https://bugzilla.mozilla.org/show_bug.cgi?id=338621#c72 for more
- information.
-
-
- For best results serve both atom feed and xslt as 'text/xml' or
- 'application/xml' without charset specified.
--->
-<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:georss="http://www.georss.org/georss">
- <id>https://drop.mro.name/galleries/demo/</id>
- <generator uri="http://purl.mro.name/iPhoto2Atom">iPhoto2Atom</generator>
- <title>Demo Album</title>
- <updated>2015-04-16T17:57:46+02:00</updated>
- <link rel="self" href="https://drop.mro.name/galleries/demo/photos.atom"/>
- <author>
- <name>John Doe</name>
- </author>
- <entry>
- <title>Osterglocken</title>
- <id>https://drop.mro.name/galleries/demo/#_022e7cb3e8b9d44155e7ce17866f629cd1418708</id>
- <summary> </summary>
- <updated>2015-04-16T17:57:46+02:00</updated>
- <link rel="enclosure" length="243347" href="https://drop.mro.name/galleries/demo/1600p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
- <link rel="previewimage" length="11621" href="https://drop.mro.name/galleries/demo/200p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
- <content type="image/jpeg" src="https://drop.mro.name/galleries/demo/1600p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
- <media:thumbnail url="https://drop.mro.name/galleries/demo/200p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
- </entry>
- <entry>
- <title>IMG_7639</title>
- <id>https://drop.mro.name/galleries/demo/#_e141de41c8e8e426b1bf3e72c18999987b04e22a</id>
- <summary> </summary>
- <updated>2015-03-24T17:20:44+01:00</updated>
- <link rel="enclosure" length="191218" href="https://drop.mro.name/galleries/demo/1600p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
- <link rel="previewimage" length="13416" href="https://drop.mro.name/galleries/demo/200p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
- <category label="Screen" scheme="https://drop.mro.name/galleries/demo/" term="Screen"/>
- <content type="image/png" src="https://drop.mro.name/galleries/demo/1600p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
- <media:thumbnail url="https://drop.mro.name/galleries/demo/200p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
- </entry>
- <entry>
- <title>IMG_6735</title>
- <id>https://drop.mro.name/galleries/demo/#_fcc1302401173e66ad6ab86d42deee2486f1ad9f</id>
- <summary> </summary>
- <updated>2014-11-14T17:09:13+01:00</updated>
- <link rel="enclosure" length="215330" href="https://drop.mro.name/galleries/demo/1600p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
- <link rel="previewimage" length="8682" href="https://drop.mro.name/galleries/demo/200p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
- <content type="image/jpeg" src="https://drop.mro.name/galleries/demo/1600p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
- <media:thumbnail url="https://drop.mro.name/galleries/demo/200p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
- <georss:point>47.86138833 12.64393667</georss:point>
- </entry>
-</feed>
A src/.gitkeep => src/.gitkeep +0 -0