Repository: internetarchive/openlibrary

Issue:
Better Share Preview for Lists & Reading Log (e.g. Twitter Social Card)
<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->

This is a create "social card" view of a book list (from fivebooks). Would be great if Open Library lists generated this type of preview :slightly_smiling_face:

see: https://twitter.com/jabuppartyon/status/1482812981591347206

![twitter com_tommycollison_status_1482812483337277442](https://user-images.githubusercontent.com/978325/149683625-487f1e78-ab3b-4574-8bfa-cfaeec3eed08.png)

Here's the metadata fivebooks attaches:
```
 <meta name="twitter:card" content="summary_large_image" />
 <meta name="twitter:title" content="Five Books: The Best Sci Fi Books for Beginners, recommended by Nicholas Whyte" />
 <meta name="twitter:image" content="https://fivebooks.com/best-books/sci-fi-books-for-beginners-nicholas-whyte/shareimage.jpg" />
 <meta name="twitter:site" content="@five_books" />
 <meta name="twitter:label1" content="Interview by" />
 <meta name="twitter:data1" content="Cal Flyn, Deputy Editor" />
 <meta name="twitter:label2" content="Est. reading time" />
 <meta name="twitter:data2" content="16 minutes" />
```

Here's what Open Library's looks like today (for Lists):
![twitter com_tommycollison_status_1482812483337277442 (1)](https://user-images.githubusercontent.com/978325/149684431-fb5a7ed6-b5cf-4ec5-b5e7-ab445890306a.png)


### Evidence / Screenshot (if possible)

### Relevant url?
<!-- `https://openlibrary.org/...` -->

### Steps to Reproduce
<!-- What steps caused you to find the bug? -->
1. Go to ...
2. Do ...

<!-- What actually happened after these steps? What did you expect to happen? -->
* Actual:
* Expected:

### Details

- **Logged in (Y/N)?**
- **Browser type/version?**
- **Operating system?**
- **Environment (prod/dev/local)?** prod
<!-- If not sure, put prod -->

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->

In order to load the List page, we'd have already pulled the list + bookcovers from the db, so the right `/templates/type/list/view` (?) file like just needs to be updated somehow to use the e.g. 5 first book covers.

One question is how the book covers are converted into a single image. We may wish to create a simple endpoint on our coverstore service (https://github.com/internetarchive/openlibrary/blob/master/openlibrary/coverstore/code.py) to combine these images into a preview image which then gets used to the card.

This could be done with `Pillow`
https://www.tutorialspoint.com/python_pillow/Python_pillow_merging_images.htm

This isn't a super easy issue, however it is well scoped if someone wanted to give it a shot!

### Related files
<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->

### Stakeholders
<!-- @ tag stakeholders of this bug -->



Discussion:
Hi @mekarpeles, I like to work on this.
I see what you mean. I think it's a genius idea to treat lists like a tumble page where you see the book cover and browse through that than look at them one at a time. I wonder if that'll improve loading times too? At the very least it lets people view more to have to click around less.
Thank you @Udai1931! If you have any questions, please let me know!
Is this still open issue? Can I work on it?
Hi @saieswar13, @Udai1931 is working on the issue, 
How about you take a look [here ](https://github.com/internetarchive/openlibrary/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs%3A+Help%22). I am sure you will find something interesting
Happy Contributing 😄 
Thank you for your interests in Open Library 🕺🏼 

Let me know if you are stuck, I would love to help.
@Udai1931 are you still planning on working on this one? And if so, did you have any questions about getting started?
@saieswar13 it seems it's all you if you'd like to take this one on! :) 
@saieswar13 best of luck with the issue :)
Thank you for contributing to Open Library
> @saieswar13 it seems it's all you if you'd like to take this one on! :)

Thank you @mekarpeles, Sir I'm new to open source contribution. Could you please say me how to get start with this issue?
> > @saieswar13 it seems it's all you if you'd like to take this one on! :)
> 
> Thank you @mekarpeles, Sir I'm new to open source contribution. Could you please say me how to get start with this issue?

I think the best way to interact would be to ping @mekarpeles on slack
> > > @saieswar13 it seems it's all you if you'd like to take this one on! :)
> > 
> > Thank you @mekarpeles, Sir I'm new to open source contribution. Could you please say me how to get start with this issue?
> 
> I think the best way to interact would be to ping @mekarpeles on slack

Okay..! Thanks for your information 
@saieswar13 how are you doing?
Did you manage to get yourself started with the issue?
Please let me know if you have any difficulties, I would love to help. 

Adding few links which will help you to get started with Open Library
- https://github.com/internetarchive/openlibrary/blob/master/CONTRIBUTING.md
- https://github.com/internetarchive/openlibrary/wiki
Hi @saieswar13, I'm going to open up the assignee field for now and give others an opportunity. Please let us know if you've already started working on this as I want your existing work to be respected / considered! Thank you!
Me and @Vassilis-Boubis would like to work on that 
It looks like we won't be able to use svg
https://stackoverflow.com/questions/21636503/use-svg-as-ogimage

We'll probably want to...
1. [ ] create a background image which gets added to `/static/imgs`
2. [ ] Write a function (lets call it `create_list_preview` which takes a list ID (e.g .`/people/mekBot/lists/OL104041L`) fetches the first 5 **book items** from the List, uses python Pillow / PIL library to overlay these book covers over the background, and then returns the image preview (in memory).
3. [ ] creates a public API endpoint which calls create_list_preview and returns the image

re: #2, see https://moonbooks.org/Articles/How-to-overlay--superimpose-two-images-using-python-and-pillow-/
@Vassilis-Boubis + @constantinazouni, is it alright if @furugoorin helps with the 1st checkbox of creating a background design? I think that would be a cool collaboration!
Yes of course that will be helpful!
Great, I'll keep you all posted with some mockups here! Do let me know if there are any updated specifications with regards to dimensions, palette, etc., when the mockups are ready.
Please refer to this [social card designs link](https://xd.adobe.com/view/b7274a73-6173-4683-b275-7f776ca7ef9d-e274/) 

I can make more changes on my end, and sync them here.

The first two prototypes are just color gradients; I tried to find the closest color match by converting hsl to hex given the `css/less color.less` file, and from a general overview of the OpenLibrary website. The rest is a matter of preference for different parchment/paper textures (I tried to adhere to the theme of books). We can toss more ideas around, as well! 

nice work! this looks cool :)
![xd adobe com_view_b7274a73-6173-4683-b275-7f776ca7ef9d-e274_ (1)](https://user-images.githubusercontent.com/978325/165189521-42243c67-e94b-48ea-911f-3bfe14e776f9.png)

How should the card look if there are less than five items on a list that have covers?
@jimchamp How about justifying the content so that the books are centered, to begin with? [See mockup](https://xd.adobe.com/view/b585fe5e-cf8c-43eb-8cdb-24caa7613ca0-e0d6/)



Resolving patch:
diff --git a/openlibrary/coverstore/code.py b/openlibrary/coverstore/code.py
--- a/openlibrary/coverstore/code.py
+++ b/openlibrary/coverstore/code.py
@@ -1,13 +1,19 @@
 import array
 import datetime
+import io
 import json
 import logging
 import os
 
 import memcache
 import requests
+
 import web
 
+from PIL import Image, ImageDraw, ImageFont
+import textwrap
+
+
 from openlibrary.coverstore import config, db, ratelimit
 from openlibrary.coverstore.coverlib import read_file, read_image, save_image
 from openlibrary.coverstore.utils import (
@@ -512,3 +518,82 @@ def POST(self, category):
                 return 'cover has been deleted successfully.'
         else:
             return 'no such id: %s' % id
+
+
+def render_list_preview_image(lst_key):
+    """This function takes a list of five books and puts their covers in the correct
+    locations to create a new image for social-card"""
+    lst = web.ctx.site.get(lst_key)
+    five_seeds = lst.seeds[0:5]
+    background = Image.open("/openlibrary/static/images/Twitter_Social_Card_Background.png")
+
+    logo = Image.open("/openlibrary/static/images/Open_Library_logo.png")
+
+    W, H = background.size
+    image = []
+    for seed in five_seeds:
+        cover = seed.get_cover()
+
+        if  cover:
+            response = requests.get(f"https://covers.openlibrary.org/b/id/{cover.id}-M.jpg")
+            image_bytes = io.BytesIO(response.content)
+
+            img = Image.open(image_bytes)
+
+            basewidth = 162
+            wpercent = (basewidth / float(img.size[0]))
+            hsize = int(float(img.size[1]) * float(wpercent))
+            img = img.resize((basewidth, hsize), Image.ANTIALIAS)
+            image.append(img)
+    max_height=0
+    for img in image:
+        if (img.size[1] > max_height):
+            max_height = img.size[1]
+    if len(image) == 5:
+        background.paste(image[0], (63, 174 + max_height - image[0].size[1]))
+        background.paste(image[1], (247, 174 + max_height - image[1].size[1]))
+        background.paste(image[2], (431, 174 + max_height - image[2].size[1]))
+        background.paste(image[3], (615, 174 + max_height - image[3].size[1]))
+        background.paste(image[4], (799, 174 + max_height - image[4].size[1]))
+
+    elif len(image) == 4:
+        background.paste(image[0], (155, 174 + max_height - image[0].size[1]))
+        background.paste(image[1], (339, 174 + max_height - image[1].size[1]))
+        background.paste(image[2], (523, 174 + max_height - image[2].size[1]))
+        background.paste(image[3], (707, 174 + max_height - image[3].size[1]))
+
+    elif len(image) == 3:
+        background.paste(image[0], (247, 174 + max_height - image[0].size[1]))
+        background.paste(image[1], (431, 174 + max_height - image[1].size[1]))
+        background.paste(image[2], (615, 174 + max_height - image[2].size[1]))
+
+    elif len(image) == 2:
+        background.paste(image[0], (339, 174 + max_height - image[0].size[1]))
+        background.paste(image[1], (523, 174 + max_height - image[1].size[1]))
+
+    else:
+        background.paste(image[0], (431, 174 + max_height - image[0].size[1]))
+
+
+    logo = logo.resize((120,74), Image.ANTIALIAS)
+    background.paste(logo, (880, 14), logo)
+
+    draw = ImageDraw.Draw(background)
+    font_author = ImageFont.truetype("/openlibrary/static/fonts/NotoSans-LightItalic.ttf", 22)
+    font_title = ImageFont.truetype("/openlibrary/static/fonts/NotoSans-SemiBold.ttf", 28)
+
+    para = textwrap.wrap(lst.name, width=45)
+    current_h = 42
+    author_text = f"A list by {lst.get_owner().displayname}"
+    w, h = draw.textsize(author_text, font=font_author)
+    draw.text(((W - w) / 2, current_h), author_text, font=font_author, fill=(0,0,0))
+    current_h += h + 5
+
+    for line in para:
+        w, h = draw.textsize(line, font=font_title)
+        draw.text(((W - w) / 2, current_h), line, font=font_title, fill=(0,0,0))
+        current_h += h
+
+    with io.BytesIO() as buf:
+        background.save(buf, format='PNG')
+        return buf.getvalue()
diff --git a/openlibrary/plugins/openlibrary/lists.py b/openlibrary/plugins/openlibrary/lists.py
--- a/openlibrary/plugins/openlibrary/lists.py
+++ b/openlibrary/plugins/openlibrary/lists.py
@@ -2,6 +2,7 @@
 """
 import json
 import random
+import tempfile
 import web
 
 from infogami.utils import delegate
@@ -15,6 +16,8 @@
 from openlibrary.plugins.upstream import spamcheck
 from openlibrary.plugins.upstream.account import MyBooksTemplate
 from openlibrary.plugins.worksearch import subjects
+from openlibrary.coverstore.code import render_list_preview_image
+
 
 
 class lists_home(delegate.page):
@@ -639,3 +642,12 @@ def get_active_lists_in_random(limit=20, preload=True):
     lists = f(limit=limit, preload=preload)
     # convert rawdata into models.
     return [web.ctx.site.new(xlist['key'], xlist) for xlist in lists]
+
+
+class lists_preview(delegate.page):
+    path = r"(/people/[^/]+/lists/OL\d+L)/preview.png"
+
+    def GET(self, lst_key):
+        image_bytes = render_list_preview_image(lst_key)
+        web.header("Content-Type", "image/png")
+        return delegate.RawText(image_bytes)


Test patch: