richardmyu / blog

个人(issues)博客
https://github.com/richardmyu/blog/issues
MIT License
0 stars 0 forks source link

Hexo 博客增加相册功能 #20

Open richardmyu opened 3 years ago

richardmyu commented 3 years ago

前言

前一段时间,整理手机的时候,发现手机内的照片太多了,而那个时候也在写一些书摘,就想着,要不在博客上建立一个相册吧?在搭建和配置这个博客(Hexo & Next)的时候,已经知道不支持相册功能,但实在是很想弄一个,于是上网搜搜看,看也没有可以借鉴的。

在看来了多个文章之后,选择了其中几篇比较详尽,功能比较符合需求的文章为借鉴,根据自己的实际情况,以及自己的需求,做出一些修改和调整,并在此记录下来。


1.Hexo NexT 添加多级相册功能 2.搭建Hexo博客相册 3.Next -23- 添加相册系列 -3- 获取图像信息、保存为json文件并上传图像

richardmyu commented 3 years ago

1.增加相册 tab

控制台执行 hexo new page "album",然后会在 source 文件看到 album 以及 index.md 文件。

然后在主题下的配置文件(themes/next/_config.yml)增加 tab:

menu:
  ...
  # 新增
  album: /album/ || fa fa-camera

图标可以自己更换,如果有语言切换,记得去主题的 languages 文件增加相册的对应语言词汇。

2.一级相册

单纯只要一级相册是挺简单的。但这里的一级相册,是要为二级相册做准备的。

这里,我们使用模板来生成 album 页面,具体是在 themes/next/layout 下新建 album.swig 文件:

{% extends '_layout.swig' %}
{% import '_macro/sidebar.swig' as sidebar_template with context %}

{# 添加相册 title #}
{% block title %}{{ page.title }} | {{ title }}{% endblock %}

{% block content %}
    <div class="posts-expand">
        <div class="post-block" lang="{{ page.lang or page.language or config.language }}">
            {# 不想在相册页面显示 “相册”,则注释下一条代码 #}
            {# {% include '_partials/page/page-header.swig' %} #}
            <div class="post-body{%- if page.direction and page.direction.toLowerCase() === 'rtl' %} rtl{%- endif %}">
                {% if config.album %}
                <div class="album-wrapper row">
                    {% for gallery in config.album.gallery %}
                    <div class="album-box">
                        <a href="./{{ gallery.name }}" class="album-item">
                            <div class="album-cover-box" style="background-image: url({{ config.album.image_bed }}/{{ gallery.name }}/thumbnail/{{ gallery.cover }});">
                            </div>
                            <p class="album-name">
                                {{ gallery.name }}
                            </p>
                        </a>
                        <p class="album-description">
                            {{ gallery.description }}
                        <p>
                    </div>
                    {% endfor %}
                </div>
                {% endif %}
            </div>
        </div>
    </div>
{% endblock %}

{% block sidebar %}
  {{ sidebar_template.render(true) }}
{% endblock %}

细节上,可以根据自己的实际需求进行修改调整。然后要在站点配置文件(_config.yml)中,添加相册的信息:

album:
  imageBed: https://xxx.xx.com/album
  gallery:
    - name: 'xxx'
      cover: 'xxx.jpg'
      description: 'xx...xx'
      created: 'xxxx-xx-xx'

对于相册而已,这里只是生成了 html 页面,如有需要你可以根据 css 来定制对应的样式。这里 css 数据放在了souce/_data/styles.styl 中。

最后修改 source/album/index.md 文件:

---
date: xxxx-xx-xx xx:xx:xx
layout: album
title: '相册'
---

layout: album 用来指定使用 album.swig 来渲染出相册的 HTML 页面。

richardmyu commented 3 years ago

3.二级相册

source/album 下,创建 gallery 目录,具体是指,根据站点配置信息,建立对应的图册目录。

album:
  imageBed: https://xxx.xx.com/album
  gallery:
    - name: 'flowers'
      cover: 'xxx.jpg'
      description: 'xx...xx'
      created: 'xxxx-xx-xx'
    - name: 'sky'
      cover: 'xxx.jpg'
      description: 'xx...xx'
      created: 'xxxx-xx-xx'

对应 gallery :

|-- album
|    |
|    |-- index.md
|    |
|    |-- flowers
|    |    |
|    |    |-- data.json
|    |    |
|    |    |-- index.md
|    |    |
|    |
|    |-- sky
|    |    |
|    |    |-- data.json
|    |    |
|    |    |-- index.md
|    |    |
|    |
|    |-- ...
|

下面是描述相册的 JSON 文件,可以通过 Python 脚本自动生成和更新(后面有讲述)。

{
    "name": "flowers",
    "cover": "xx.jpg",
    "description": "xx...xx",
    "created": "xxxx-xx-xx",
    "imageBed": "https://xxx.xxcom/album",
    "items": [
        {
            "date": "2019-11",
            "images": [
                {
                    "name": "xxx.jpg",
                    "caption": "xx....xxx",
                    "type": "image",
                    "date": "xxxx-xx-xx",
                    "address": "xx,xxx,xxx",
                    "width": 6400,
                    "height": 4000
                }
            ]
        }
    ]
}

下面是 gallery 的 index.md 文件:

---
date: xxxx-xx-xx xx:xx:xx
fancybox: false
layout: gallery
title: 'flowers'
galleryName: 'flowers'
---

接下来,就要编写 gallery 的模板了。跟一级相册一样,还是在在 themes/next/layout 下新建 gallery.swig 文件:

{% extends '_layout.swig' %}
{% import '_macro/sidebar.swig' as sidebar_template with context %}

{% block title %}{{ page.title }} | {{ title }}{% endblock %}

{% block content %}

    <div class="posts-expand">
        <div class="post-block" lang="{{ page.lang or page.language or config.language }}">
            {# {% include '_partials/page/page-header.swig' %} #}
            <div class="post-body{%- if page.direction and page.direction.toLowerCase() === 'rtl' %} rtl{%- endif %}">
                <link rel="stylesheet" href="/lib/album/photoswipe.css">
                <link rel="stylesheet" href="/lib/album/default-skin/default-skin.css">

                {# gallery body #}

                <div class="gallery">
                    <div class="gallery-description">
                        <p id="gallery-description">这里是相册描述</p>
                    </div>

                    <div class="instagram itemscope">
                        <a href="https://richyu.gitee.io/" target="_blank" class="open-ins">图片正在加载中…</a>
                    </div>
                </div>
                {# gallery body end #}

                {# <!-- Core JS file --> #}
                <script src="/lib/album/photoswipe.min.js"></script>

                {# <!-- UI JS file --> #}
                <script src="/lib/album/photoswipe-ui-default.min.js"></script>

                {# <!-- gallery.js --> #}
                <script>
                    (function() {
                        var loadScript = function(path) {
                        var $script = document.createElement('script')
                        document.getElementsByTagName('body')[0].appendChild($script)
                        $script.setAttribute('src', path)
                        }
                        setTimeout(function() {
                            loadScript('/lib/album/gallery.min.js')
                        }, 0)
                    })()
                </script>
            </div>
            {# {% include '_partials/page/breadcrumb.swig' %} #}
        </div>
    </div>

    {# <!-- Root element of PhotoSwipe. Must have class pswp. --> #}
    <div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    {# <!-- Background of PhotoSwipe.
         It's a separate element, as animating opacity is faster than rgba(). --> #}
        <div class="pswp__bg"></div>

        {# <!-- Slides wrapper with overflow:hidden. --> #}
        <div class="pswp__scroll-wrap">

            {# <!-- Container that holds slides. PhotoSwipe keeps only 3 slides in DOM to save memory. --> #}
            {# <!-- don't modify these 3 pswp__item elements, data is added later on. --> #}
            <div class="pswp__container">
                <div class="pswp__item"></div>
                <div class="pswp__item"></div>
                <div class="pswp__item"></div>
            </div>

            {# <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. --> #}
            <div class="pswp__ui pswp__ui--hidden">

                <div class="pswp__top-bar">

                    {# <!--  Controls are self-explanatory. Order can be changed. --> #}
                    <div class="pswp__counter"></div>
                    <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
                    {# <button class="pswp__button pswp__button--share" title="Share"></button> #}
                    <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
                    <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                    {# <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR --> #}
                    {# <!-- element will get class pswp__preloader--active when preloader is running --> #}
                    <div class="pswp__preloader">
                        <div class="pswp__preloader__icn">
                            <div class="pswp__preloader__cut">
                                <div class="pswp__preloader__donut"></div>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                    <div class="pswp__share-tooltip"></div>
                </div>

                <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>

                <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>

                <div class="pswp__caption">
                    <div class="pswp__caption__center"></div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

{% block sidebar %}
  {{ sidebar_template.render(true) }}
{% endblock %}

到此为止,页面基本完成,剩下的就是编写 gallery.js 了。

richardmyu commented 3 years ago

4.编写脚本

themes/next/source/lib 下,新建 album 目录,创建一下文件:

|
|-- album
|    |
|    |-- assets
|    |    |
|    |    |-- empty,png
|    |    |
|    |
|    |-- default-skin
|    |    |
|    |    |-- default-skin.css
|    |    |
|    |    |-- default-skin.png
|    |    |
|    |    |-- default-skin.svg
|    |    |
|    |    |-- preloader.gif
|    |    |
|    |
|    |-- gallery.js
|    |
|    |-- handler_photoswipe.js
|    |
|    |-- photoswipe-ui-default.min.js
|    |
|    |-- photoswipe.css
|    |
|    |-- photoswipe.min.js
|    |
|

以上这些图片以及脚本均可以在 https://github.com/richardmyu/richardmyu.github.io/tree/main/lib/album 中找到。

其中一个重点在 gallery.js 文件中。

这是参考文章中的脚本:

"use strict";

require('lazyloadjs');
var photoswipe = require("./handler_photoswipe.js");

var view = _interopRequireDefault(photoswipe.viewer);
var galleryPath = window.location.pathname;
var galleryName = galleryPath.split("/")[2];
var dataUrl = '/album/' + galleryName + '/data.json';
var dataJSON;

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {
    default: obj
  };
}

var render = function render(response) {
  var imageBed = response["image_bed"];
  var description = response["description"];
  var items = response["items"];
  var ulTmpl = "";
  for (var item of items) {
    var liTmpl = "";
    var date = item['date']
    var year = date.split('-')[0];
    var month = date.split('-')[1];
    for (var img of item["images"]) {
      var thumbnail = imageBed + '/' + galleryName + "/thumbnail/" + img.name;
      var artwork = imageBed + '/' + galleryName + "/artwork/" + img.name;
      var caption = img.caption;
      var address = img.address
      var width = img.width;
      var height = img.height;

      liTmpl += `
      <figure class="thumb" itemprop="associatedMedia" itemscope="" itemtype="http://schema.org/ImageObject">
        <a href="${artwork}" itemprop="contentUrl" data-size="${width}x${height}">
            <img class="reward-img" data-src="${thumbnail}" src="/lib/album/assets/empty.png" itemprop="thumbnail" onload="lzld(this)">
        </a>
        <figcaption style="display:none;" itemprop="caption description">${address || caption}</figcaption>
      </figure>`;
    }

    ulTmpl += `
    <section class="archives album">
      <h3 class="timeline">${year} 年 ${month} 月</h3>
      <div class="img-box">${liTmpl}</div>
    </section>`;
  }

  document.querySelector('.instagram').innerHTML = `<div class=${photoswipe.galleryClass} itemscope="" itemtype="http://schema.org/ImageGallery">${ulTmpl}</div>`;
  document.querySelector('#gallery-description').innerHTML = description;

  view.default.init();
};

function loadData(render) {
  if (!dataJSON) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', dataUrl, true);
    xhr.onload = function () {
      if (this.status >= 200 && this.status < 300) {
        dataJSON = JSON.parse(this.response);
        render(dataJSON);
      } else {
        console.error(this.statusText);
      }
    };
    xhr.onerror = function () {
      console.error(this.statusText);
    };
    xhr.send();
  } else {
    render(dataJSON);
  }
}

var Gallery = {
  init: function init() {
    loadData(function (data) {
      render(data);
    });
  }
};

Gallery.init();

需要注意的一点是:

ulTmpl += `
    <section class="archives album">
      <h3 class="timeline">${year} 年 ${month} 月</h3>
      <div class="img-box">${liTmpl}</div>
    </section>`;

<section class="archives album">${liTmpl},中间只能嵌套一层;若想要嵌套多层,要修改 handler_photoswipe.js

var parseThumbnailElements = function parseThumbnailElements(el) {
    // 两次获取父节点,限定了最多只能嵌套一层,
    // 超过一层(多层情况也一致),会因为祖父节点定位在单个照片组之内,
    // 使得 photoswipe 将照片组甚至单个照片视为一个相册,
    // 从而造成点击的照片在“相册”中的索引与当前与全局获取图片的索引不一致
    // 结果就是点击图片,放大的是另一张图片
    el = el.parentNode.parentNode;
    var thumbElements = el.getElementsByClassName('thumb')

// ...

// 获取该页面全部图片 `thumb` 
childNodes = document.getElementsByClassName('thumb')

放大图片的规则:当前点击照片全局索引 a,当前“相册” 照片数 x,放大的图片在该“相册”内的索引是 a;自然会有 a > x 的情况,那结果就是取“相册”的第一张照片。

richardmyu commented 3 years ago

5.自动化 JSON

上面说到相册的数据是从 data.json 读取的,如果照片或者相册比较多,那么写 JSON 会很无聊的,所以我们使用 Python 来完成这个工作。

因为我使用了 Gitee 的仓库来作为图床,所以便也把 python 脚本也写在了图床仓库里。

这一部分参考 Hexo NexT 添加多级相册功能 就可以了,实际也可以根据自己的需求,进行部分修改和调整,比如我就增加了批量添加图片,解析 GPS 获取地址以及拍摄日期(个人觉得从图片名称获取日期不确定性比较大,甚至可能得修改图片名称)。

为了获取 Exif 信息,肯定得放上原图,但原图如果上传到图库,也不合适(:smirk:那你还不是主动提供了地址。。。:joy::joy::joy:),所以我多增加了一个文件,用来存放原图,且不上传的。

    @staticmethod
    def reset_orientation(img):
        """
        处理图片的自动(PIL处理回发生)旋转
        增加对 width / height 的调换处理
        https://cloud.tencent.com/developer/article/1523050
        """
        exif_orientation_tag = 274
        w, h = img.size
        if hasattr(img, "_getexif") and isinstance(img._getexif(), dict) and exif_orientation_tag in img._getexif():
            exif_data = img._getexif()
            orientation = exif_data[exif_orientation_tag]

            # Handle EXIF Orientation
            if orientation == 1:
                # Normal image - nothing to do!
                pass
            elif orientation == 2:
                # Mirrored left to right
                # transpose 翻转
                img = img.transpose(Image.FLIP_LEFT_RIGHT)
            elif orientation == 3:
                # Rotated 180 degrees
                img = img.rotate(180)
            elif orientation == 4:
                # Mirrored top to bottom
                img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
            elif orientation == 5:
                # Mirrored along top-left diagonal
                img = img.rotate(-90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
                w, h = h, w
            elif orientation == 6:
                # Rotated 90 degrees
                img = img.rotate(-90, expand=True)
                w, h = h, w
            elif orientation == 7:
                # Mirrored along top-right diagonal
                img = img.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
                w, h = h, w
            elif orientation == 8:
                # Rotated 270 degrees
                img = img.rotate(90, expand=True)
                w, h = h, w
        return {
            "image": img,
            "size": (w, h)
        }

    @staticmethod
    def reverse_geocoder(geolocator, lat_lon, sleep_sec=5):
        """
        根据经纬度,计算区域
        https://www.pythonheidong.com/blog/article/680556/d9bf76d691415de282f1/
        """
        try:
            # 反向地理编码
            return geolocator.reverse(lat_lon)
        except GeocoderTimedOut:
            print('Timeout: GeocoderTimedOut Retrying...')
            time.sleep(randint(2 * 100, sleep_sec * 100) / 50)
            return AlbumTool.reverse_geocoder(geolocator, lat_lon, sleep_sec)
        except GeocoderServiceError as e:
            print('CONNECTION REFUSED: GeocoderServiceError encountered.')
            print(e)
            return None
        except Exception as e:
            print('ERROR: Terminating due to exception {}'.format(e))
            return None

    @staticmethod
    def get_exif(img):
        """
        获取图片的经纬度以及拍摄时间等信息
        https://zhuanlan.zhihu.com/p/98460548
        """
        print("Loading and Resolving: " + img)
        f = open(img, 'rb')
        image_map = exifread.process_file(f)

        try:
            # 图片的经度
            img_longitude_ref = image_map["GPS GPSLongitudeRef"].printable
            img_longitude = image_map["GPS GPSLongitude"].printable[1:-1].replace(" ", "").replace("/", ",").split(
                ",")
            img_longitude = float(img_longitude[0]) + float(img_longitude[1]) / 60 + float(
                img_longitude[2]) / float(img_longitude[3]) / 3600
            if img_longitude_ref != "E":
                img_longitude = img_longitude * (-1)

            # 图片的纬度
            img_latitude_ref = image_map["GPS GPSLatitudeRef"].printable
            img_latitude = image_map["GPS GPSLatitude"].printable[1:-1].replace(" ", "").replace("/", ",").split(
                ",")
            img_latitude = float(img_latitude[0]) + float(img_latitude[1]) / 60 + float(img_latitude[2]) / float(
                img_latitude[3]) / 3600
            if img_latitude_ref != "N":
                img_latitude = img_latitude * (-1)
        except Exception as e:
            print('ERROR: 图片中不包含 Gps 信息')
            img_latitude = ''
            img_longitude = ''

        # 照片拍摄时间
        img_create_date = image_map["Image DateTime"].printable[:10].replace(":", "-")

        f.close()

        location = None
        if img_latitude != '' and img_longitude != '':
            reverse_value = str(img_latitude) + ', ' + str(img_longitude)

            # 初始化 Nominatim() 时传入新的 user-agent 值,避开样例值
            user_agent = 'my_blog_agent_{}'.format(randint(10000, 99999))
            geolocator = Nominatim(user_agent=user_agent)
            location = AlbumTool.reverse_geocoder(geolocator, reverse_value)

        address = location and location.address or ''
        info = {
            'address': address,  # 地址
            'date': img_create_date  # 日期
        }

        return info

批量添加很简单就不说了。目前为止,相册部分算是基本完工了。效果见 album

补充
    with open(self.data_json) as json_file:
        data = json.load(json_file)
        items =  data["items"]

这行代码可能会二次读取 JSON 时,造成乱码,解决方法就是指定读取格式:

with open(self.data_json, 'r', encoding='utf-8') as json_file