添加相册功能内容比较多,单独成章。
前一段时间,整理手机的时候,发现手机内的照片太多了,而那个时候也在写一些书摘,就想着,要不在博客上建立一个相册吧?在搭建和配置这个博客(Hexo & Next)的时候,已经知道不支持相册功能,但实在是很想弄一个,于是上网搜搜看,看也没有可以借鉴的。
在看来了多个文章之后,选择了其中几篇比较详尽,功能比较符合需求的文章为借鉴,根据自己的实际情况,以及自己的需求,做出一些修改和调整,并在此记录下来。
1.Hexo NexT 添加多级相册功能
2.搭建Hexo博客相册
3.Next -23- 添加相册系列 -3- 获取图像信息、保存为json文件并上传图像
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 页面。
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
了。
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 | | |
以上这些图片以及脚本均可以在 /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 ) { el = el.parentNode .parentNode ; var thumbElements = el.getElementsByClassName ('thumb' ); } childNodes = document .getElementsByClassName ('thumb' );
放大图片的规则:当前点击照片全局索引 a,当前“相册” 照片数 x,放大的图片在该“相册”内的索引是 a;自然会有 a > x
的情况,那结果就是取“相册”的第一张照片。
5.自动化 JSON
上面说到相册的数据是从 data.json
读取的,如果照片或者相册比较多,那么写 JSON 会很无聊的,所以我们使用 Python 来完成这个工作。
因为我使用了 Gitee 的仓库来作为图床,所以便也把 python 脚本也写在了图床仓库里。
这一部分参考 Hexo NexT 添加多级相册功能
就可以了,实际也可以根据自己的需求,进行部分修改和调整,比如我就增加了批量添加图片,解析 GPS
获取地址以及拍摄日期(个人觉得从图片名称获取日期不确定性比较大,甚至可能得修改图片名称)。
为了获取 Exif 信息,肯定得放上原图,但原图如果上传到图库,也不合适(😏那你还不是主动提供了地址。。。😂😂😂
),所以我多增加了一个文件,用来存放原图,且不上传的。
@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] if orientation == 1 : pass elif orientation == 2 : img = img.transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 3 : img = img.rotate(180 ) elif orientation == 4 : img = img.rotate(180 ).transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 5 : img = img.rotate(-90 , expand=True ).transpose(Image.FLIP_LEFT_RIGHT) w, h = h, w elif orientation == 6 : img = img.rotate(-90 , expand=True ) w, h = h, w elif orientation == 7 : img = img.rotate(90 , expand=True ).transpose(Image.FLIP_LEFT_RIGHT) w, h = h, w elif orientation == 8 : 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) 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