First stab at images and git-annex.
authorNick Bowler <nbowler@draconx.ca>
Wed, 24 Jun 2020 05:01:30 +0000 (01:01 -0400)
committerNick Bowler <nbowler@draconx.ca>
Fri, 26 Jun 2020 03:18:26 +0000 (23:18 -0400)
Let's add a photo and some filters to produce a handy little image
information page.

It's not so nice to store these (relatively) huge files in git, so we'll
store them in git-annex and update the deployment scripts to handle it.

Nothing links to this image yet as this is pushed mainly to validate the
git-annex deployment, but it will be accessible by direct URI.

The intention is that the site should still compile with missing annexed
components, just that those items will be missing.  In the future we could
perhaps replace them with stubs (so at least the description part of the
info page would work) but we'll save that activity for another day.

.gitattributes [new file with mode: 0644]
Rules
content/images/k8ne-capacitors.jpg [new symlink]
content/images/k8ne-capacitors.yaml [new file with mode: 0644]
content/style.scss
layouts/imginfo.xsl [new file with mode: 0644]
lib/helpers.rb
lib/imginfo.rb [new file with mode: 0644]
lib/imgresize.rb [new file with mode: 0644]

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..dc3671a
--- /dev/null
@@ -0,0 +1 @@
+* annex.backend=SHA512
diff --git a/Rules b/Rules
index 5473788e31261cfed8b0bbe865e1ba88cf83545e..f3f1f0a197c6f67c3dec7d13a593beef126cf1da 100644 (file)
--- a/Rules
+++ b/Rules
@@ -53,6 +53,44 @@ postprocess do
             end
         end
     end
+
+    # Register URLs for git-annex keys
+    unless (uribase = ENV['ANNEX_URI_BASE'].to_s.chomp("/")).empty?
+        Open3.popen2("git", "annex", "registerurl") do |stdin, stdout, result|
+            @items.each do |item|
+                l = File.readlink(item.raw_filename)
+                next unless l =~ %r{/annex/objects/}
+
+                key = File.basename(l)
+
+                # Find output reps corresponding to this key, if any
+                item.reps.each do |rep|
+                    next unless
+                        FileUtils.identical?(item.raw_filename, rep.raw_path)
+
+                    loop do
+                        STDOUT.write(stdout.read_nonblock(100))
+                    rescue EOFError, IO::WaitReadable
+                        break
+                    end
+
+                    stdin.printf("%s %s%s\n", key, uribase, rep.path)
+                end
+            rescue Errno::EINVAL
+            end
+
+            stdin.close
+            loop do
+                STDOUT.write(stdout.readpartial(100))
+            rescue EOFError
+                break
+            end
+
+            unless (rc = result.value).success?
+                printf("git annex registerurl failed: %s\n", rc.to_s)
+            end
+        end
+    end
 end
 
 compile '/license/gpl*.md' do
@@ -99,6 +137,22 @@ compile '/license/cc*.xhtml' do
     write to_xhtml
 end
 
+compile '/images/*.jpg', rep: :large do
+    filter :imgresize, width: 1200, height: 1200
+    write item.identifier.without_ext + '-t1200.' + item.identifier.ext
+end
+
+compile '/images/*.jpg', rep: :info do
+    filter :imginfo
+    layout '/imginfo.xsl'
+    layout '/default.xml'
+    layout '/default.xsl'
+    filter :relativize_paths, type: :xml
+    filter :xhtml_compat
+    filter :remove_wj
+    write to_xhtml
+end
+
 compile '/**/*.scss' do
     filter :sass, syntax: :scss
     filter :css_source, uribase: \
diff --git a/content/images/k8ne-capacitors.jpg b/content/images/k8ne-capacitors.jpg
new file mode 120000 (symlink)
index 0000000..2a9a2dc
--- /dev/null
@@ -0,0 +1 @@
+../../.git/annex/objects/JW/0W/SHA512-s2751176--43e58095d1a7a641599f648c2e3985ee166e58c0694ef53ccc64ef1027753f36f82a012f5122067b3d0230fb8570cf1167789925147ef0fadbe88b04732f9167/SHA512-s2751176--43e58095d1a7a641599f648c2e3985ee166e58c0694ef53ccc64ef1027753f36f82a012f5122067b3d0230fb8570cf1167789925147ef0fadbe88b04732f9167
\ No newline at end of file
diff --git a/content/images/k8ne-capacitors.yaml b/content/images/k8ne-capacitors.yaml
new file mode 100644 (file)
index 0000000..aa99a36
--- /dev/null
@@ -0,0 +1,51 @@
+---
+title: ASUS K8N-E Deluxe Capacitor Locations
+copyright: 2020 Nick Bowler
+license: cc-by-sa-4.0
+description: |
+        <p>Photo with overlay marking the locations and types of all
+           electrolytic capacitors on my ASUS K8N-E Deluxe motherboard.</p>
+        <table class='cc'>
+          <thead>
+            <tr>
+              <th>Label</th>
+              <th>Qty.</th>
+              <th>Value</th>
+              <th>Dimensions</th>
+              <th>Brand</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td>1</td>
+              <td>17</td>
+              <td>820µF,&#x2007; 6.3V</td>
+              <td>Pitch:&#xa0;3.5mm, Diameter:&#xa0;8mm, Height:&#xa0;13mm</td>
+              <td>OST</td>
+            </tr>
+            <tr>
+              <td>2</td>
+              <td>6</td>
+              <td>1500µF, 6.3V</td>
+              <td>Pitch:&#xa0;3.5mm, Diameter:&#xa0;8mm, Height:&#xa0;20mm</td>
+              <td>Ltec</td>
+            </tr>
+            <tr>
+              <td>3</td>
+              <td>3</td>
+              <td>1000µF, 16V</td>
+              <td>Pitch:&#xa0;3.5mm, Diameter:&#xa0;8mm, Height:&#xa0;20mm</td>
+              <td>Nippon Chemi-Con</td>
+            </tr>
+            <tr>
+              <td>4</td>
+              <td>14</td>
+              <td>100µF,&#x2007; 16V</td>
+              <td>Pitch:&#xa0;2.0mm, Diameter:&#xa0;5mm, Height:&#xa0;12mm</td>
+              <td>GSC</td>
+            </tr>
+          </tbody>
+        </table>
+        <p>Several of the OST and Ltec (the latter obscured by the heatsink in
+           the photo) capacitors have obvious signs of catastrophic failure.
+           The other types show no obvious damage.</p>
index 79d4ffb408466fbbab19b8ccac4674f380ed7d44..b0c042cdc6f5f3a44f784de6e016dda0ead212f8 100644 (file)
@@ -26,9 +26,12 @@ $linkactivecolour:  #ff0000;
 $linkvisitedcolour: #800080;
 
 $ruledefaultcolour: #d3d3d3;
+$rulestrongcolour:  #696969;
 
 $annotationcolour:  #708090;
 
+$tableshadecolour:  #f5f5f5;
+
 @mixin header_size($maxwidth, $fontsize) {
     font-size: $fontsize;
     max-width: 1em * ($maxwidth / $fontsize);
@@ -48,19 +51,21 @@ a:active { color: $linkactivecolour; }
 h1 { @include header_size(60em, 2em); }
 h2 { @include header_size(60em, 1.5em); }
 
+p>img { max-width: 40em; width: 100%; height: auto; }
+
 p, dt, dd, li {
     text-align: justify;
     padding: 0;
     margin: 0;
 }
 
-p, div, ul, ol, dl, hr {
+p, table, div, ul, ol, dl, hr {
     max-width: 50em;
     padding: 0;
     margin: 0;
 }
 
-p, body>div { margin: 1em 0; }
+p, table, body>div { margin: 1em 0; }
 
 li { margin: 0 0 0 2em; }
 dd { margin: 0 0 0 1em; }
@@ -88,6 +93,50 @@ kbd {
     .permalink { visibility: hidden; }
 }
 
+// General table styles.
+table {
+    border: 1px solid $ruledefaultcolour;
+    border-collapse: collapse;
+    width: 100%;
+}
+
+table>* { font-size: 0.9em; }
+caption {
+    caption-side: top;
+    font-weight: bold;
+    font-size: 1em;
+    text-align: left;
+    margin: 0 0 0.5em 0;
+}
+
+td, th {
+    vertical-align: middle;
+    text-align: left;
+    padding: 1ex;
+    margin: 0;
+}
+
+thead>tr, tbody>tr { border: solid $ruledefaultcolour; }
+th, thead>tr { border-bottom: 1px solid $rulestrongcolour; }
+*>table, *>th { border: none; }
+thead>tr { border-width: 1px; }
+tbody>tr { border-width: 0 1px; }
+
+td + td { box-shadow: -1px 0 $backgroundcolour; }
+
+tbody>tr {
+    &:nth-of-type(even) { background-color: $tableshadecolour; }
+    &:last-child { border-bottom: solid 1px $ruledefaultcolour; }
+}
+
+// Specific table styles
+table.cc {
+    &>tr>*:first-child, &>*>tr>*:first-child {
+        &+* { text-align: center; }
+        text-align: center;
+    }
+}
+
 // Site header rules
 #breadcrumbs>*, #sitetitle>* { font-size: 0.8em; }
 #breadcrumbs {
diff --git a/layouts/imginfo.xsl b/layouts/imginfo.xsl
new file mode 100644 (file)
index 0000000..7859d8b
--- /dev/null
@@ -0,0 +1,138 @@
+<?xml version='1.0' encoding='UTF-8' ?>
+<!--
+  Nick's web site: Image description page formatter
+
+  Copyright © 2020 Nick Bowler
+
+  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 <https://www.gnu.org/licenses/>
+-->
+<xsl:stylesheet version='1.0'
+  xmlns='http://www.w3.org/1999/xhtml'
+  xmlns:xhtml='http://www.w3.org/1999/xhtml'
+  xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
+  exclude-result-prefixes='xhtml'>
+
+<xsl:output method='xml' encoding='UTF-8' indent='yes'
+  omit-xml-declaration='yes' />
+
+<xsl:template match='node()|@*'>
+  <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
+</xsl:template>
+
+<xsl:template name='exif'>
+  <xsl:param name='node' select='.' />
+  <xsl:param name='name' select='local-name($node)' />
+
+  <tr>
+    <td><xsl:value-of select='$name' /></td>
+    <td><xsl:value-of select='$node' /></td>
+  </tr>
+</xsl:template>
+<xsl:template match='exif'>
+  <h2>Metadata</h2>
+  <table>
+    <thead>
+      <tr><th>Attribute</th><th>Value</th></tr>
+    </thead>
+    <tbody>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='make' />
+        <xsl:with-param name='name' select='"Camera make"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='model' />
+        <xsl:with-param name='name' select='"Camera model"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='date_time_original' />
+        <xsl:with-param name='name' select='"Date taken"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='concat(exposure_time, "s")' />
+        <xsl:with-param name='name' select='"Shutter speed"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='concat("f/", f_number)' />
+        <xsl:with-param name='name' select='"Aperture"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='exposure_bias_value' />
+        <xsl:with-param name='name' select='"Exposure compensation"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='concat(focal_length, "mm")' />
+        <xsl:with-param name='name' select='"Focal length"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='flash' />
+        <xsl:with-param name='name' select='"Flash"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='iso_speed_ratings' />
+        <xsl:with-param name='name' select='"ISO speed rating"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='exposure_program' />
+        <xsl:with-param name='name' select='"Exposure program"' />
+      </xsl:call-template>
+      <xsl:call-template name='exif'>
+        <xsl:with-param name='node' select='metering_mode' />
+        <xsl:with-param name='name' select='"Metering mode"' />
+      </xsl:call-template>
+    </tbody>
+  </table>
+</xsl:template>
+
+<xsl:template match='variant'>
+  <li>
+    <a href='{uri}'>
+      <xsl:value-of
+        select='concat(name, " (", width, "x", height, ", ", filesize, ")")' />
+    </a>
+  </li>
+</xsl:template>
+
+<xsl:template match='xhtml:description'>
+  <h2 id='desc'>Description</h2>
+  <xsl:choose>
+    <xsl:when test='*'>
+      <xsl:apply-templates select='node()' />
+    </xsl:when>
+    <xsl:otherwise>
+      <p><xsl:apply-templates select='node()' /></p>
+    </xsl:otherwise>
+  </xsl:choose>
+</xsl:template>
+
+<xsl:template match='/'>
+  <p>
+    <img src='{/image/variant[1]/uri}' alt=''
+      width='{/image/variant[1]/width}'
+      height='{/image/variant[1]/height}'>
+      <xsl:if test='/image/xhtml:description'>
+        <xsl:attribute name='longdesc'>#desc</xsl:attribute>
+      </xsl:if>
+    </img>
+  </p>
+  <xsl:apply-templates select='/image/xhtml:description' />
+  <h2>Files</h2>
+  <ul>
+    <xsl:apply-templates select='/image/variant'>
+      <xsl:sort select='width*height' order='ascending' data-type='number' />
+    </xsl:apply-templates>
+  </ul>
+  <xsl:apply-templates select='/image/exif' />
+</xsl:template>
+
+</xsl:stylesheet>
index d2f3a7c1fe1ab2cdffdd1850d867a56b4811e176..742507580cabdf2f1e5e59c63cd12084d0d231a7 100644 (file)
@@ -63,3 +63,17 @@ def item_longdesc(item)
     p = xml.xpath('//xhtml:p', Xmlns)
     if p.empty? then nil else p[0].xpath('string(.)') end
 end
+
+def human_filesize(size)
+    units = ["B", "KiB", "MiB", "GiB"]
+
+    for unit in units
+        if (size < 1024)
+            break
+        end
+
+        size /= 1024.0
+    end
+
+    sprintf("%.1f %s", size + 0.05, unit)
+end
diff --git a/lib/imginfo.rb b/lib/imginfo.rb
new file mode 100644 (file)
index 0000000..13d6b01
--- /dev/null
@@ -0,0 +1,111 @@
+# Nick's web site: imginfo filter.  Generate XML representation of image
+# metadata which can be further processed into a web page.
+#
+# Copyright © 2020 Nick Bowler
+#
+# 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 <https://www.gnu.org/licenses/>.
+
+class ImgInfoFilter < Nanoc::Filter
+    identifier :imginfo
+    type :binary => :text
+
+    require 'exifr/jpeg'
+    require 'fastimage'
+
+    def do_variant(xml, name, item = @item, rep: :default)
+        file = item.reps[rep].raw_path
+
+        w, h = FastImage.size(file)
+        sz = File.size(file)
+
+        xml.variant {
+            xml.name(name)
+            xml.uri(item_uri(item, rep: rep))
+            xml.width(w)
+            xml.height(h)
+            xml.filesize(human_filesize(sz))
+        }
+    end
+
+    def run(filename, params = {})
+        exif = EXIFR::JPEG.new(filename).to_hash
+
+        b = Nokogiri::XML::Builder.new do |xml|
+            xml.image {
+                do_variant(xml, "Large", rep: :large)
+                do_variant(xml, "Original")
+
+
+                if @item[:description]
+                    xml.description(:xmlns => 'http://www.w3.org/1999/xhtml') {
+                        xml << @item[:description]
+                    }
+                end
+
+                xml.exif {
+                    exif.each do |key, value|
+                        # Convert some fields to more useful forms...
+                        case key
+                        when :f_number, :exposure_bias_value
+                            value = sprintf("%.1f", value)
+                        when :focal_length
+                            value = sprintf("%d\n", value)
+                        when :gps_version_id
+                            value = value.bytes.join(".")
+                        when :exposure_program
+                            case value
+                            when 1 then value = "Manual"
+                            when 2 then value = "Normal program"
+                            when 3 then value = "Aperture priority"
+                            when 4 then value = "Shutter priority"
+                            end
+                        when :metering_mode
+                            case value
+                            when 1 then value = "Average"
+                            when 2 then value = "Center weighted average"
+                            when 3 then value = "Spot"
+                            when 4 then value = "Multi-spot"
+                            when 5 then value = "Pattern"
+                            when 6 then vlaue = "Partial"
+                            end
+                        when :flash
+                            tmp = if value & 1 == 1 then "Yes" else "No" end
+                            case (value >> 3) & 3
+                            when 1, 2 then tmp += ", compulsory"
+                            when 3 then tmp += ", auto"
+                            end
+                            case (value >> 1) & 3
+                            when 2 then tmp += ", return light not detected"
+                            when 3 then tmp += ", return light detected"
+                            end
+                            value = tmp
+                        end
+
+                        case value
+                        when String
+                            value.delete!("\x00")
+                            value.strip!
+                        when Time
+                            # EXIF does not do timezones so don't display one
+                            value = value.strftime("%Y-%m-%d %H:%M:%S")
+                        end
+
+                        xml.send((key.to_s << "_").to_sym, value.to_s)
+                    end
+                }
+            }
+        end
+        b.to_xml
+    end
+end
diff --git a/lib/imgresize.rb b/lib/imgresize.rb
new file mode 100644 (file)
index 0000000..e539f37
--- /dev/null
@@ -0,0 +1,37 @@
+# Nick's web site: imgresize filter.  Helper for resizing images during site
+# compilations for thumbnails, etc.
+#
+# Copyright © 2016, 2020 Nick Bowler
+#
+# 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 <https://www.gnu.org/licenses/>.
+
+class ImgResize < Nanoc::Filter
+    identifier :imgresize
+    type       :binary
+
+    def run(filename, params = {})
+        w = if params[:width] then params[:width].to_i end
+        h = if params[:height] then params[:height].to_i end
+        scale = [w, h && "x", h].join
+
+        args = ["-scale", scale, filename]
+        case filename
+        when /\.jpg$/
+            args << "-quality" << "85"
+        end
+        args << output_filename
+
+        system('gm', 'convert', *args)
+    end
+end