merge from master

This commit is contained in:
Adrien Beudin 2016-02-09 21:12:34 +01:00
commit cfd2c4b751
12 changed files with 174 additions and 91 deletions

View File

@ -62,7 +62,7 @@ sub_title: it's a scary place, don't go there
This settings.yaml will describe: This settings.yaml will describe:
* the title, subtitle and cover picture of your gallery that will be used on the homepage * the title, subtitle and cover picture of your gallery that will be used on the homepage
* if your gallery is public * if your gallery is public (if not, it will still be built but won't appear on the homepage)
* the date of your gallery: this will be used on the homepage since **galleries are sorted anti chronologically** on it * the date of your gallery: this will be used on the homepage since **galleries are sorted anti chronologically** on it
* the list of sections that will contains your gallery. A section will represent either one picture, a group of pictures or text. The different kind of sections will be explained in the next README section. * the list of sections that will contains your gallery. A section will represent either one picture, a group of pictures or text. The different kind of sections will be explained in the next README section.

View File

@ -3,18 +3,20 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8"> <meta charset="UTF-8">
<link type="text/css" rel="stylesheet" href="../static/css/fonts.css" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="../static/css/style-page.css" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="../static/css/style-page.css" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="../static/css/baguetteBox.min.css" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="../static/css/baguetteBox.min.css" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="../static/css/panorama_viewer.css" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="../static/css/panorama_viewer.css" media="screen,projection"/>
<!--Let browser know website is optimized for mobile--> <!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>my first gallery | Example gallery</title> <title>my first gallery · Example gallery</title>
</head> </head>
<body> <body>
<section class="full-picture" style="background: transparent url('stuff.png') no-repeat scroll center top / cover;"> <section class="full-picture" style="background: transparent url('stuff.png') no-repeat scroll center top / cover;">
<div class="picture-text"> <div class="picture-text">
@ -28,6 +30,7 @@
</section> </section>
<section class="bordered-picture baguette"> <section class="bordered-picture baguette">
<a href="stuff.png"> <a href="stuff.png">
<img src="stuff.png"> <img src="stuff.png">
@ -43,6 +46,7 @@
<div class="pictures-line"> <div class="pictures-line">
<div class="picture"> <div class="picture">
<a href="stuff.png"> <a href="stuff.png">
<img src="stuff-small.png"> <img src="stuff-small.png">
@ -53,6 +57,7 @@
<div class="picture"> <div class="picture">
<a href="stuff.png"> <a href="stuff.png">
<img src="stuff-small.png"> <img src="stuff-small.png">
@ -63,6 +68,7 @@
<div class="picture"> <div class="picture">
<a href="stuff.png"> <a href="stuff.png">
<img src="stuff-small.png"> <img src="stuff-small.png">
@ -75,6 +81,7 @@
<div class="pictures-line"> <div class="pictures-line">
<div class="picture"> <div class="picture">
<a href="stuff.png"> <a href="stuff.png">
<img src="stuff-small.png"> <img src="stuff-small.png">
@ -85,6 +92,7 @@
<div class="picture"> <div class="picture">
<a href="stuff.png"> <a href="stuff.png">
<img src="stuff-small.png"> <img src="stuff-small.png">
@ -97,6 +105,7 @@
</section> </section>
<section class="full-picture" style="background: transparent url('stuff.png') no-repeat scroll center top / cover;"> <section class="full-picture" style="background: transparent url('stuff.png') no-repeat scroll center top / cover;">
</section> </section>
@ -115,17 +124,19 @@
<script type="text/javascript" src="../static/js/baguetteBox.min.js" charset="utf-8"></script> <script type="text/javascript" src="../static/js/baguetteBox.min.js" charset="utf-8"></script>
<script type="text/javascript" src="../static/js/jquery.panorama_viewer.min.js" charset="utf-8"></script> <script type="text/javascript" src="../static/js/jquery.panorama_viewer.min.js" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
$(function() {
baguetteBox.run(".baguette", {}); baguetteBox.run(".baguette", {});
$(".panorama").panorama_viewer({ $(".panorama").panorama_viewer({
repeat: true, repeat: true,
direction: "horizontal", direction: "horizontal",
animationTime: 250, animationTime: 150,
easing: "ease-out", easing: "linear",
overlay: true overlay: true
}); });
});
</script> </script>
<footer> <footer>
<p>Generate using <a href="https://github.com/psycojoker/prosopopee">Prosopopée</a> · content under <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a> · atom logo by <a href="https://thenounproject.com/jjjon/">Jonathan Li</a> under <a href="https://creativecommons.org/licenses/by/3.0/">CC-BY</a></p> <p>Generated using <a href="https://github.com/psycojoker/prosopopee">Prosopopée</a> · content under <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a> · atom logo by <a href="https://thenounproject.com/jjjon/">Jonathan Li</a> under <a href="https://creativecommons.org/licenses/by/3.0/">CC-BY</a></p>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link type="text/css" rel="stylesheet" href="static/css/fonts.css" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="static/css/style.css" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="static/css/style.css" media="screen,projection"/>
<!--Let browser know website is optimized for mobile--> <!--Let browser know website is optimized for mobile-->
@ -30,6 +31,8 @@
<div class="gallery-datetime">08 December 2015</div> <div class="gallery-datetime">08 December 2015</div>
</div> </div>
</a> </a>
<div class="gallery-cover" style="background-image: url('first_gallery/stuff-small.png');"></div> <div class="gallery-cover" style="background-image: url('first_gallery/stuff-small.png');"></div>
</div><!-- comment tricks against space between inline-block </div><!-- comment tricks against space between inline-block
--> -->
@ -40,7 +43,7 @@
<p style="visibility: hidden">.</p> <p style="visibility: hidden">.</p>
<footer> <footer>
<p>Generate using <a href="https://github.com/psycojoker/prosopopee">Prosopopée</a> · content under <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a> · atom logo by <a href="https://thenounproject.com/jjjon/">Jonathan Li</a> under <a href="https://creativecommons.org/licenses/by/3.0/">CC-BY</a></p> <p>Generated using <a href="https://github.com/psycojoker/prosopopee">Prosopopée</a> · content under <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a> · atom logo by <a href="https://thenounproject.com/jjjon/">Jonathan Li</a> under <a href="https://creativecommons.org/licenses/by/3.0/">CC-BY</a></p>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -12,7 +12,7 @@ body {
} }
section { section {
margin-bottom: 64px; margin-bottom: 80px;
} }
a { a {
@ -51,7 +51,7 @@ a {
text-transform: uppercase; text-transform: uppercase;
font-size: 5.5vw; font-size: 5.5vw;
letter-spacing: 4px; letter-spacing: 4px;
font-family: sans-serif; font-family: 'montserrat', sans-serif;
margin-left: 10%; margin-left: 10%;
margin-right: 10%; margin-right: 10%;
margin-bottom: 1px; margin-bottom: 1px;
@ -61,30 +61,30 @@ a {
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
font-size: 2.2vw; font-size: 2.2vw;
font-family: serif; font-family: 'crimson', serif;
margin-top: 1px; margin-top: 1px;
} }
.full-picture .datetime { .full-picture .datetime {
text-transform: uppercase; text-transform: uppercase;
font-family: serif; font-family: 'crimson', serif;
letter-spacing: 2px; letter-spacing: 2px;
} }
.bordered-picture img { .bordered-picture img {
height: 80%; height: 77%;
width: 80%; width: 77%;
margin-left: 10%; margin-left: 11.5%;
margin-right: 10%; margin-right: 11.5%;
} }
.pictures-line { .pictures-line {
min-width: 80%; min-width: 77%;
width: 80%; width: 77%;
margin-left: 10%; margin-left: 11.5%;
margin-right: 10%; margin-right: 11.5%;
display: flex; display: flex;
margin-bottom: 15px; margin-bottom: 0.5em;
} }
.pictures-line .picture img { .pictures-line .picture img {
@ -93,22 +93,47 @@ a {
} }
.pictures-line .separator { .pictures-line .separator {
min-width: 15px; min-width: 0.5em;
} }
.text { .text {
text-align: center; text-align: center;
font-size: 25px; font-family: 'crimson', serif;
margin-left: 15%; font-size: 1.6em;
margin-right: 15%; line-height: 1.8em;
margin-left: 21%;
margin-right: 21%;
color: black;
}
.paragraph {
text-align: left;
font-family: 'crimson', serif;
font-size: 1em;
margin-left: 21%;
margin-right: 21%;
color: #333; color: #333;
} }
.paragraph h2 {
font-family: 'montserrat', sans-serif;
font-weight: normal;
font-size: 2.5em;
text-transform: uppercase;
color: black;
line-height: 1.4em;
}
.paragraph p {
line-height: 2em;
}
footer { footer {
margin-top: 7em; margin-top: 6em;
text-align: center; text-align: center;
position: relative; position: relative;
font-family: serif; font-family: 'crimson', serif;
font-size: 11px; font-size: 11px;
color: #555; color: #555;
background-color: #EEE; background-color: #EEE;
@ -154,7 +179,8 @@ footer {
justify-content: center; justify-content: center;
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
font-family: sans-serif; font-family: 'montserrat', sans-serif;
font-weight: bold;
} }
footer p { footer p {
@ -164,6 +190,6 @@ footer p {
footer a { footer a {
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
font-family: sans-serif; font-family: 'montserrat', sans-serif;
color: #111; color: #111;
} }

View File

@ -1,6 +1,6 @@
body { body {
color: #222; color: #222;
font-family: sans-serif; font-family: 'montserrat', sans-serif;
background-color: #FBFBFB; background-color: #FBFBFB;
margin: 0; margin: 0;
} }
@ -17,10 +17,11 @@ body {
.galleries-line { .galleries-line {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin-bottom: -4px; /* YOLO */
} }
.covers-1 .gallery-square { .covers-1 .gallery-square {
width: 47%; width: 100%;
height: 100%; height: 100%;
margin: auto; margin: auto;
padding-bottom: 47%; padding-bottom: 47%;
@ -28,12 +29,12 @@ body {
} }
.covers-2 .gallery-square { .covers-2 .gallery-square {
width: 47%; width: 50%;
height: 100%; height: 100%;
float: left; margin: 0 0 0;
margin: 0 1.5% 3%;
padding-bottom: 47%; padding-bottom: 47%;
position: relative; position: relative;
display: inline-block;
} }
.covers-3 .gallery-square { .covers-3 .gallery-square {
@ -110,7 +111,7 @@ body {
color: #444; color: #444;
font-style: italic; font-style: italic;
font-weight: normal; font-weight: normal;
font-family: serif; font-family: 'crimson', serif;
margin-top: .5em; margin-top: .5em;
} }
@ -131,13 +132,13 @@ body {
font-style: italic; font-style: italic;
margin-top: 0; margin-top: 0;
margin-bottom: .7em; margin-bottom: .7em;
font-family: serif; font-family: 'crimson', serif;
font-weight: normal; font-weight: normal;
} }
.gallery-datetime { .gallery-datetime {
margin-bottom: 1em; margin-bottom: 1em;
font-family: serif; font-family: 'crimson', serif;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 2px; letter-spacing: 2px;
font-size: 11px; font-size: 11px;
@ -147,7 +148,7 @@ footer {
margin-top: 7em; margin-top: 7em;
text-align: center; text-align: center;
position: relative; position: relative;
font-family: serif; font-family: 'crimson', serif;
font-size: 11px; font-size: 11px;
color: #555; color: #555;
background-color: #EEE; background-color: #EEE;
@ -163,6 +164,6 @@ footer p {
footer a { footer a {
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
font-family: sans-serif; font-family: 'montserrat', sans-serif;
color: #111; color: #111;
} }

View File

@ -13,71 +13,100 @@ index_template = templates.get_template("index.html")
gallery_index_template = templates.get_template("gallery-index.html") gallery_index_template = templates.get_template("gallery-index.html")
page_template = templates.get_template("page.html") page_template = templates.get_template("page.html")
DEFAULT_GM_QUALITY = 75
CACHE_VERSION = 1
class Cache(object): class Cache(object):
cache_file_path = os.path.join(os.getcwd(), ".prosopopee_cache") cache_file_path = os.path.join(os.getcwd(), ".prosopopee_cache")
def __init__(self): def __init__(self, json):
# fix: I need to keep a reference to json because for whatever reason
# modules are set to None during python shutdown thus totally breaking
# the __del__ call to save the cache
# This wonderfully stupid behavior has been fixed in 3.4 (which nobody uses)
self.json = json
if os.path.exists(os.path.join(os.getcwd(), ".prosopopee_cache")): if os.path.exists(os.path.join(os.getcwd(), ".prosopopee_cache")):
self.cache = json.load(open(self.cache_file_path, "r")) self.cache = json.load(open(self.cache_file_path, "r"))
else: else:
self.cache = {} self.cache = {"version": CACHE_VERSION}
def thumbnail_needs_to_be_generated(self, source, target): if "version" not in self.cache or self.cache["version"] != CACHE_VERSION:
print "info: cache format as changed, prune cache"
self.cache = {"version": CACHE_VERSION}
def thumbnail_needs_to_be_generated(self, source, target, image):
if not os.path.exists(target): if not os.path.exists(target):
return True return True
if target not in self.cache: if target not in self.cache:
return True return True
if self.cache[target] != os.path.getsize(source): cached_thumbnail = self.cache[target]
if cached_thumbnail["size"] != os.path.getsize(source) or cached_thumbnail["options"] != image.options:
return True return True
return False return False
def cache_thumbnail(self, source, target): def cache_thumbnail(self, source, target, image):
self.cache[target] = os.path.getsize(source) self.cache[target] = {"size": os.path.getsize(source), "options": image.options}
def __del__(self): def __del__(self):
json.dump(self.cache, open(self.cache_file_path, "w")) self.json.dump(self.cache, open(self.cache_file_path, "w"))
CACHE = Cache() CACHE = Cache(json=json)
class TemplateFunctions():
def __init__(self, base_dir, target_dir, has_gm):
self.base_dir = base_dir
self.target_dir = target_dir
def copy_image(self, image): class Image(object):
source, target = os.path.join(self.base_dir, image), os.path.join(self.target_dir, image) base_dir = ""
target_dir = ""
def __init__(self, options):
# assuming string
if not isinstance(options, dict):
name = options
options = {"name": options}
self.name = name
self.quality = options.get("quality", DEFAULT_GM_QUALITY)
self.options = options.copy() # used for caching, if it's modified -> regenerate
del self.options["name"]
def copy(self):
source, target = os.path.join(self.base_dir, self.name), os.path.join(self.target_dir, self.name)
# XXX doing this DOESN'T improve perf at all (or something like 0.1%) # XXX doing this DOESN'T improve perf at all (or something like 0.1%)
# if os.path.exists(target) and os.path.getsize(source) == os.path.getsize(target): # if os.path.exists(target) and os.path.getsize(source) == os.path.getsize(target):
# print "Skiped %s since the file hasn't been modified based on file size" % source # print "Skiped %s since the file hasn't been modified based on file size" % source
# return "" # return ""
shutil.copyfile(source, target) shutil.copyfile(source, target)
print source, "->", target print source, "->", target
return "" return ""
def generate_thumbnail(self, image, gm_geometry): def generate_thumbnail(self, gm_geometry):
thumbnail_name = image.split(".") thumbnail_name = self.name.split(".")
thumbnail_name[-2] += "-small" thumbnail_name[-2] += "-small"
thumbnail_name = ".".join(thumbnail_name) thumbnail_name = ".".join(thumbnail_name)
source, target = os.path.join(self.base_dir, image), os.path.join(self.target_dir, thumbnail_name) source, target = os.path.join(self.base_dir, self.name), os.path.join(self.target_dir, thumbnail_name)
if CACHE.thumbnail_needs_to_be_generated(source, target): if CACHE.thumbnail_needs_to_be_generated(source, target, self):
command = "gm convert %s -resize %s %s" % (source, gm_geometry, target) command = "gm convert %s -resize %s -quality %s %s" % (source, gm_geometry, self.quality, target)
print command print command
os.system(command) os.system(command)
CACHE.cache_thumbnail(source, target, self)
CACHE.cache_thumbnail(source, target)
else: else:
print "skiped %s since it's already generated (based on source unchanged size)" % target print "skiped %s since it's already generated (based on source unchanged size and images options set in your gallery's settings.yaml)" % target
return thumbnail_name return thumbnail_name
def __repr__(self):
return self.name
def error(test, error_message): def error(test, error_message):
if test: if test:
@ -90,10 +119,9 @@ def error(test, error_message):
def main(): def main():
has_gm = True
if os.system("which gm > /dev/null") != 0: if os.system("which gm > /dev/null") != 0:
has_gm = False sys.stderr.write("ERROR: I can't locate the 'gm' binary, I won't be able to resize images, please install the 'graphicsmagick' package.\n")
sys.stderr.write("WARNING: I can't locate the 'gm' binary, I won't be able to resize images.\n") sys.exit(1)
error(os.path.exists(os.path.join(os.getcwd(), "settings.yaml")), "I can't find a settings.yaml in the current working directory") error(os.path.exists(os.path.join(os.getcwd(), "settings.yaml")), "I can't find a settings.yaml in the current working directory")
@ -143,7 +171,11 @@ def main():
if not os.path.exists(os.path.join("build", gallery)): if not os.path.exists(os.path.join("build", gallery)):
os.makedirs(os.path.join("build", gallery)) os.makedirs(os.path.join("build", gallery))
open(os.path.join("build", gallery, "index.html"), "w").write(gallery_index_template.render(settings=settings, gallery=gallery_settings, helpers=TemplateFunctions(os.path.join(os.getcwd(), gallery), os.path.join(os.getcwd(), "build", gallery), has_gm=has_gm)).encode("Utf-8")) # this should probably be a factory
Image.base_dir = os.path.join(os.getcwd(), gallery)
Image.target_dir = os.path.join(os.getcwd(), "build", gallery)
open(os.path.join("build", gallery, "index.html"), "w").write(gallery_index_template.render(settings=settings, gallery=gallery_settings, Image=Image).encode("Utf-8"))
front_page_galleries_cover = reversed(sorted(front_page_galleries_cover, key=lambda x: x["date"])) front_page_galleries_cover = reversed(sorted(front_page_galleries_cover, key=lambda x: x["date"]))
@ -154,7 +186,11 @@ def main():
error(os.path.exists(os.path.join(os.getcwd(), item_file+".yaml")), "I can't find a "+item_file+".yaml in the current working directory") error(os.path.exists(os.path.join(os.getcwd(), item_file+".yaml")), "I can't find a "+item_file+".yaml in the current working directory")
open(os.path.join("build", item_file+".html"), "w").write(page_template.render(settings=settings, pages=yaml.safe_load(open(item_file+".yaml", "r")), galleries=front_page_galleries_cover, helpers=TemplateFunctions(os.getcwd(), os.path.join(os.getcwd(), "build"), has_gm=has_gm)).encode("Utf-8")) open(os.path.join("build", item_file+".html"), "w").write(page_template.render(settings=settings, pages=yaml.safe_load(open(item_file+".yaml", "r")), galleries=front_page_galleries_cover, helpers=TemplateFunctions(os.getcwd(), os.path.join(os.getcwd(), "build"), has_gm=has_gm)).encode("Utf-8"))
open(os.path.join("build", "index.html"), "w").write(index_template.render(settings=settings, galleries=front_page_galleries_cover, helpers=TemplateFunctions(os.getcwd(), os.path.join(os.getcwd(), "build"), has_gm=has_gm)).encode("Utf-8")) Image.base_dir = os.getcwd()
Image.target_dir = os.path.join(os.getcwd(), "build")
open(os.path.join("build", "index.html"), "w").write(index_template.render(settings=settings, galleries=front_page_galleries_cover, Image=Image).encode("Utf-8"))
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -43,7 +43,7 @@
}); });
</script> </script>
<footer> <footer>
<p>Generate using <a href="https://github.com/psycojoker/prosopopee">Prosopopée</a> · content under <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a> · atom logo by <a href="https://thenounproject.com/jjjon/">Jonathan Li</a> under <a href="https://creativecommons.org/licenses/by/3.0/">CC-BY</a></p> <p>Generated using <a href="https://github.com/psycojoker/prosopopee">Prosopopée</a> · content under <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a> · atom logo by <a href="https://thenounproject.com/jjjon/">Jonathan Li</a> under <a href="https://creativecommons.org/licenses/by/3.0/">CC-BY</a></p>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -35,7 +35,9 @@
{% if gallery.date %}<div class="gallery-datetime">{{ gallery.date.strftime("%d %B %Y") }}</div>{% endif %} {% if gallery.date %}<div class="gallery-datetime">{{ gallery.date.strftime("%d %B %Y") }}</div>{% endif %}
</div> </div>
</a> </a>
<div class="gallery-cover" style="background-image: url('{{ helpers.generate_thumbnail(gallery.cover, "x900") }}');"></div> {% set cover = Image(gallery.cover) %}
{{ cover.copy() }}
<div class="gallery-cover" style="background-image: url('{{ cover.generate_thumbnail("x900") }}');"></div>
</div><!-- comment tricks against space between inline-block </div><!-- comment tricks against space between inline-block
-->{% endfor %} -->{% endfor %}
</div> </div>

View File

@ -1,6 +1,7 @@
{{ helpers.copy_image(section.image) }} {% set image = Image(section.image) %}
{{ image.copy()}}
<section class="bordered-picture baguette"> <section class="bordered-picture baguette">
<a href="{{ section.image }}"> <a href="{{ image }}">
<img src="{{ section.image }}"> <img src="{{ image }}">
</a> </a>
</section> </section>

View File

@ -1,5 +1,6 @@
{{ helpers.copy_image(section.image) }} {% set image = Image(section.image) %}
<section class="full-picture" style="background: transparent url('{{ section.image }}') no-repeat scroll center top / cover;"> {{ image.copy() }}
<section class="full-picture" style="background: transparent url('{{ image }}') no-repeat scroll center top / cover;">
{% if section.text %} {% if section.text %}
<div class="picture-text"> <div class="picture-text">
<div class="picture-text-column"> <div class="picture-text-column">

View File

@ -1,4 +1,5 @@
{{ helpers.copy_image(section.image) }} {% set image = Image(section.image) %}
{{ image.copy() }}
<section class="panorama"> <section class="panorama">
<img src="{{ helpers.generate_thumbnail(section.image, "x800") }}"> <img src="{{ image.generate_thumbnail("x800") }}">
</section> </section>

View File

@ -2,10 +2,11 @@
{% for line in section.images %} {% for line in section.images %}
<div class="pictures-line"> <div class="pictures-line">
{% for image in line %} {% for image in line %}
{{ helpers.copy_image(image) }} {% set image = Image(image) %}
{{ image.copy() }}
<div class="picture"> <div class="picture">
<a href="{{ image }}"> <a href="{{ image }}">
<img src="{{ helpers.generate_thumbnail(image, "x600") }}"> <img src="{{ image.generate_thumbnail("x600") }}">
</a> </a>
</div> </div>
{% if not loop.last %} {% if not loop.last %}