├── __init__.py ├── requirements.txt ├── run_all_jobs.sh ├── LICENSE ├── README.md └── insta_reddit └── code ├── sheets_db.py ├── upload_to_instagram.py ├── download_from_reddit.py ├── draw_text_on_image.py └── image_utils.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gspread 2 | instabot 3 | nltk 4 | pandas 5 | python-crontab 6 | Pillow 7 | praw 8 | print-schema 9 | textblob -------------------------------------------------------------------------------- /run_all_jobs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /Users/$USER/Documents/insta_reddit/venv/bin/python3 insta_reddit/code/download_from_reddit.py --post_count 15 --subreddit_name LifeProTips 4 | /Users/$USER/Documents/insta_reddit/venv/bin/python3 insta_reddit/code/draw_text_on_image.py 5 | /Users/$USER/Documents/insta_reddit/venv/bin/python3 insta_reddit/code/upload_to_instagram.py --post_count 1 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2020 Surya Shekhar Chakraborty 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # insta_reddit 2 | 3 | ![insta_reddit flow](https://i.imgur.com/yZyakml.jpg) 4 | 5 | 6 | 1. Download top K posts from a subreddit 7 | (chosen [r/unethicallifeprotips](https://www.reddit.com/r/UnethicalLifeProTips/) for this). 8 | 2. Generate an image out of those posts. 9 | 3. Generate a caption for those posts. 10 | 4. Upload them to [an Instagram account](https://www.instagram.com/unethical.lifeprotips/). 11 | ## Installation 12 | #### Setting up credentials 13 | 1. Get [PRAW credentials](https://praw.readthedocs.io/en/latest/getting_started/quick_start.html) from Reddit. 14 | 2. Setup a [service account](https://gspread.readthedocs.io/en/latest/oauth2.html#for-bots-using-service-account) 15 | to use Google Sheets as a DB. 16 | 3. Download the service account JSON from the step above and add it to your repo. 17 | Use the screenshot below for reference. 18 | 19 | At the end of all the setup, the credentials file should look like this: 20 | ![credentials](https://i.imgur.com/mx7yeHX.jpg) 21 | 22 | #### Support modules 23 | Install requirements by running: 24 | ```bash 25 | pip install -r requirements.txt 26 | ``` 27 | [Link](https://github.com/JotJunior/PHP-Boleto-ZF2/blob/master/public/assets/fonts/arial.ttf) to install the "arial.ttf" font if you need it. 28 | You'll also need `nltk`, used to generate captions. 29 | Run the following (one-time effort) from a Python3 shell. 30 | 31 | ```python 32 | import nltk 33 | nltk.download("punkt") 34 | nltk.download('averaged_perceptron_tagger') 35 | nltk.download('wordnet') 36 | ``` 37 | For SSL issues at this stage, run `bash /Applications/Python 3.6/Install Certificates.command` 38 | #### How to run the entire thing: 39 | ```bash 40 | cd insta_reddit/code 41 | python download_from_reddit.py 42 | python draw_text_on_image.py 43 | python upload_to_instagram.py 44 | 45 | ``` 46 | Or run the modifiable Cron job (remember to change the venv path): 47 | ```bash 48 | sh run_all_jobs.sh 49 | ``` 50 | Or add the above to your crontab to run at your set frequency: 51 | ```bash 52 | crontab -e 53 | ``` 54 | Add this line your crontab: `@daily /path/to/run_all_jobs.sh` 55 | 56 | 57 | #### End result on [Instagram](https://www.instagram.com/unethical.lifeprotips/): 58 | ![feed](https://i.imgur.com/9MmYy81.jpg) 59 | 60 | Sample post: 61 | ![sample_post](https://i.imgur.com/1czZFFK.jpg) 62 | 63 | 64 | ## Contributing 65 | Pull requests are welcome! 66 | For major changes, please open an issue first to discuss what you would like to change. 67 | 68 | ## Author 69 | * **Surya Shekhar Chakraborty** 70 | 71 | Much thanks to Avishek Rakshit for help with the graphic design, Puneet Jindal for brainstorming, and to you for coming here. :) 72 | 73 | ## License 74 | This project is licensed under the MIT License - 75 | see the [LICENSE](https://github.com/suryashekharc/insta_reddit/blob/master/LICENSE) file for details 76 | -------------------------------------------------------------------------------- /insta_reddit/code/sheets_db.py: -------------------------------------------------------------------------------- 1 | # https://gspread.readthedocs.io/en/latest/oauth2.html#for-bots-using-service-account 2 | # NOTE: Google Sheets cells are 1-base indexed 3 | import warnings 4 | import gspread 5 | 6 | 7 | class SheetsDb: 8 | def __init__(self, sheet_id, credentials_path=None): 9 | """ 10 | Initialize gspread handler with credentials 11 | :param sheet_id: The long-ass alphanumeric code in the URL of the Google Sheet 12 | :param credentials_path: If None, will look at ~/.config/gspread/service_account.json 13 | """ 14 | self.gc = gspread.service_account(filename=credentials_path) 15 | self.sheet_id = sheet_id 16 | self.sheet = self.gc.open_by_key(sheet_id).sheet1 17 | 18 | def get_row_for_id(self, post_id: str) -> int: 19 | """ 20 | Finds the row given the ID, returns -1 if not found 21 | :param str post_id: Reddit post ID 22 | :return int: The index of the row the ID is on 23 | """ 24 | id_col = self.get_index_for_column("id") 25 | id_list = self.sheet.col_values(id_col) 26 | try: 27 | return id_list.index(post_id) + 1 28 | except ValueError as _: 29 | return -1 30 | 31 | def get_index_for_column(self, colname: str) -> int: 32 | """ 33 | Returns the column index for the column name 34 | :param str colname: Name of the column, e.g. image_uploaded 35 | :return int: The index of the column the colname is on 36 | """ 37 | colnames = self.sheet.row_values(1) 38 | return colnames.index(colname) + 1 39 | 40 | def update_image_uploaded(self, post_id: str): 41 | """ 42 | Update the Google sheet to reflect that an image has been already uploaded 43 | :param str post_id: The "id" of the Reddit post to be uploaded 44 | :return: None 45 | """ 46 | col_idx = self.get_index_for_column("image_uploaded") 47 | row_idx = self.get_row_for_id(post_id) 48 | if self.sheet.cell(row_idx, col_idx).value == "TRUE": 49 | warnings.warn("Cell at {}, {} already updated to True.".format(row_idx, col_idx)) 50 | else: 51 | self.sheet.update_cell(row_idx, col_idx, "TRUE") 52 | 53 | def append_row(self, row: list, col_idx: int = 1): 54 | """ 55 | Append a list after the last cell of the given column 56 | :param list row: Row to append 57 | :param int col_idx: Which column to use to decide the last cell beyond which to append 58 | :return: None 59 | """ 60 | last_row_idx = len(self.sheet.col_values(col_idx)) 61 | for idx, elem in enumerate(row): 62 | self.sheet.update_cell(last_row_idx + 1, idx + 1, elem) 63 | print("Row {} appended.".format(last_row_idx + 1)) 64 | 65 | def get_unuploaded_rows(self): 66 | """ 67 | Get all the records that are yet to be uploaded 68 | :rtype: list(dict) 69 | """ 70 | all_rows = self.sheet.get_all_records() 71 | return [row for row in all_rows if row['image_uploaded'] != "TRUE"] 72 | -------------------------------------------------------------------------------- /insta_reddit/code/upload_to_instagram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Upload to instagram 3 | Check GSheets for images not uploaded yet 4 | Check if those IDs are available in images/generated 5 | If found, check if it also contains a self_text 6 | Upload them together, move them to uploaded, and update GSheets with post ID 7 | """ 8 | 9 | # TODO: See if multiple photo uploads is supported 10 | import os 11 | import shutil 12 | import glob 13 | import argparse 14 | import sys 15 | from pathlib import Path 16 | git_root = str(Path(__file__).parent.parent.parent.resolve()) 17 | sys.path.append(git_root) 18 | import html 19 | 20 | import nltk 21 | from nltk.stem import WordNetLemmatizer 22 | 23 | from instabot import Bot 24 | from insta_reddit import credentials 25 | from insta_reddit.code.sheets_db import SheetsDb 26 | 27 | DEFAULT_CAPTION_PREFIX = "Unethical life pro tips be like... " 28 | DEFAULT_HASHTAGS = " #lifeprotips #lpt" 29 | MAX_NUM_HASHTAGS = 18 30 | 31 | 32 | def get_hashtags(text): 33 | """ 34 | Returns a string of hashtags generated from the text 35 | :param str text: The text of the title 36 | :return: Space separated hashtags generated 37 | :rtype: str 38 | """ 39 | """ 40 | # NOTE: Run the following lines from a Python console to enable hashtag generation: 41 | import nltk 42 | nltk.download("punkt") 43 | nltk.download('averaged_perceptron_tagger') 44 | nltk.download('wordnet') 45 | """ 46 | # TODO: Remove stopwords 47 | lemmatizer = WordNetLemmatizer() 48 | hashtags = list(set([lemmatizer.lemmatize(word) 49 | for (word, pos) in nltk.pos_tag(nltk.word_tokenize(text)) 50 | if pos[0] == 'N'])) # use nouns to get hashtags 51 | hashtag_string = "#" + " #".join(hashtags[:min(len(hashtags), (MAX_NUM_HASHTAGS - 4))]) \ 52 | if len(hashtags) > 0 \ 53 | else "" # total upto 15+4 = 19 hashtags (any more and Instagram starts lowering SEO) 54 | hashtag_string += DEFAULT_HASHTAGS 55 | print("Hashtags generated: {}".format(hashtag_string)) 56 | return hashtag_string 57 | 58 | 59 | def get_caption(record): 60 | """ 61 | Return the complete caption for a post 62 | :param dict record: dict containing 'title', 'author', 'url' in its keys 63 | :return: caption for the post to be uploaded 64 | :rtype: str 65 | """ 66 | hashtags = get_hashtags(record['title']) 67 | author, url = record['author'], record['url'] 68 | prefix_text = DEFAULT_CAPTION_PREFIX 69 | if record['selftext']: 70 | st = html.unescape(record['selftext']) 71 | prefix_text = st 72 | return prefix_text + hashtags + \ 73 | " Author: u/{} URL: {}".format(author, url) 74 | 75 | 76 | def move_to_uploaded(file_path): 77 | shutil.move(file_path, "uploaded".join(file_path.rsplit("generated", 1))) 78 | # This syntax is to replace the last occurrence of generated in the file path with uploaded 79 | # to avoid a potential "generated" string in some other part of the filepath getting replaced 80 | pass 81 | 82 | 83 | def get_image_location(post_id): 84 | """ 85 | Returns a list of image file paths for a given post ID 86 | :param str post_id: To be found in file name 87 | :return: List of image file paths in /content/images/generated/title with the post ID 88 | :rtype: list(str) 89 | """ 90 | cur_folder_path = "/".join(os.path.dirname(os.path.realpath(__file__)).split('/')[:-1]) 91 | title_path = "".join([cur_folder_path, "/content/images/generated/title"]) 92 | all_images_paths = glob.glob(title_path + "*" + post_id + ".jpg") 93 | return all_images_paths 94 | 95 | 96 | def upload_posts(record): 97 | """ 98 | 99 | :param record: 100 | :return: 101 | """ 102 | bot = Bot() 103 | bot.login(username=credentials.instabot_username, 104 | password=credentials.instabot_password) 105 | 106 | images = get_image_location(record['id']) 107 | if not images: 108 | print("Image file not found for ID: {}".format(record['id'])) 109 | return False 110 | 111 | if len(images) == 1: # contains only title 112 | bot.upload_photo(images[0], caption=get_caption(record)) 113 | return True 114 | elif len(images) == 2: # contains title + selftext 115 | print("No support yet for multiple image uploads.") 116 | return False 117 | 118 | 119 | def main(args): 120 | credentials_path = "/".join(os.path.dirname(os.path.realpath(__file__)).split('/')[:-1]) + \ 121 | "/service_account.json" 122 | sdb = SheetsDb(sheet_id=credentials.sheets_url, 123 | credentials_path=credentials_path) 124 | unuploaded_posts = sdb.get_unuploaded_rows() 125 | for unuploaded_post in unuploaded_posts[:int(args.post_count)]: 126 | post_id = unuploaded_post['id'] 127 | upload_posts(unuploaded_post) 128 | sdb.update_image_uploaded(post_id) 129 | 130 | 131 | if __name__ == "__main__": 132 | parser = argparse.ArgumentParser() 133 | parser.add_argument('--post_count', dest='post_count', default=1, 134 | help="""Number of posts to post at one call""") 135 | main(args=parser.parse_args()) 136 | sys.exit(0) 137 | -------------------------------------------------------------------------------- /insta_reddit/code/download_from_reddit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Download top K posts from Reddit and save them to GSheets DB if not already saved. 3 | """ 4 | import argparse 5 | import sys 6 | import os 7 | from pathlib import Path 8 | git_root = str(Path(__file__).parent.parent.parent.resolve()) 9 | sys.path.append(git_root) 10 | import html 11 | 12 | import pandas as pd 13 | import praw 14 | from insta_reddit import credentials 15 | from insta_reddit.code.sheets_db import SheetsDb # To append records to sheets 16 | 17 | 18 | def initialize(): 19 | # NOTE: Ensure the credentials.py file is present in the current directory 20 | reddit_obj = praw.Reddit(client_id=credentials.client_id, 21 | client_secret=credentials.client_secret, 22 | user_agent=credentials.user_agent, 23 | username=credentials.username) 24 | if not reddit_obj.read_only: # Flag to ensure this object has been correctly configured 25 | raise Exception("Reddit object not configured correctly to be read_only.") 26 | return reddit_obj 27 | 28 | 29 | def get_posts(reddit_obj, 30 | subreddit_name="unethicallifeprotips", 31 | post_count=20, 32 | time_filter="month", 33 | fields=None): 34 | """ 35 | Get all the posts as a pandas dataframe 36 | :param reddit_obj: Instance of PRAW API 37 | :param subreddit_name: Name of the subreddit 38 | :param post_count: Number of posts 39 | :param time_filter: day/month/week etc since we are sorting by top 40 | :param fields: List of fields to return (Refer: sample_object_json.txt) 41 | :return: Pandas DF with "count" rows and "len(kwargs)" columns 42 | """ 43 | # TODO: Add support for hot along with top 44 | 45 | if fields is None: # To avoid passing mutable default args 46 | fields = ["title", "selftext", "author", "url", "id"] 47 | if "id" not in fields: # To ensure the unique ID of a post is always captured 48 | fields.append("id") 49 | content_dict = {field: [] for field in fields} # Initializing dict 50 | 51 | # Metadata of the fields of a submission are available here: 52 | # https://praw.readthedocs.io/en/latest/code_overview/models/submission.html 53 | # PRAW uses lazy objects so that network requests to ... 54 | # ... Reddit's API are only issued when information is needed. 55 | for submission in reddit_obj.subreddit(subreddit_name).top(limit=post_count, 56 | time_filter=time_filter): 57 | for field in fields: 58 | content_dict[field].append(getattr(submission, field)) 59 | return pd.DataFrame(content_dict) 60 | 61 | 62 | def cleanup_content(content_df, colnames=None): 63 | """ 64 | Read the dumped posts and clean up the text 65 | Also convert the HTML entities to characters, e.g. '>' to '>' 66 | :param content_df: Dataframe containing columns in colnames to clean up 67 | :param colnames: Which columns to clean up 68 | :return: 69 | """ 70 | # TODO: Filter out selftext containing 'edit', 'thanks', 'upvote' 71 | # NOTE: Selftext containing the above keywords are not uploaded as caption 72 | if colnames is None: 73 | colnames = ['title'] 74 | 75 | def cleanup_ulpt_text(text): 76 | if "request" not in text.lower(): 77 | text = text[text.find(' '):] 78 | return None if len(text) < 20 or len(text) > 500 else html.unescape(text) 79 | return None # TODO: Add support for ULPT request 80 | 81 | for colname in colnames: 82 | content_df[colname] = content_df.apply(lambda row: cleanup_ulpt_text(row[colname]), axis=1) 83 | return content_df.dropna() 84 | 85 | 86 | def save_posts_to_gsheets(content_df): 87 | # Iterate through rows of content_df. If id not in GSheet, append row. 88 | credentials_path = "/".join(os.path.dirname(os.path.realpath(__file__)).split('/')[:-1]) + \ 89 | "/service_account.json" 90 | sdb = SheetsDb(sheet_id=credentials.sheets_url, 91 | credentials_path=credentials_path) 92 | for index, row in content_df.iterrows(): 93 | my_list = [row.title, row.selftext, row.author.name, row.url, row.id] 94 | print("Trying {}".format(row.id)) 95 | if sdb.get_row_for_id(row.id) == -1: # i.e. ID not found 96 | sdb.append_row(my_list) 97 | else: 98 | print("Found at {}".format(sdb.get_row_for_id(row.id))) 99 | 100 | 101 | def main(args): 102 | reddit_obj = initialize() 103 | content_df = get_posts(reddit_obj, 104 | str(args.subreddit_name), 105 | int(args.post_count), 106 | str(args.time_filter), 107 | args.fields.replace(" ", "").split(",")) 108 | cleaned_content_df = cleanup_content(content_df) 109 | save_posts_to_gsheets(cleaned_content_df) 110 | 111 | 112 | if __name__ == "__main__": 113 | parser = argparse.ArgumentParser() 114 | parser.add_argument('--subreddit_name', dest='subreddit_name', default="unethicallifeprotips", 115 | help="""Name of the subreddit""") 116 | parser.add_argument('--post_count', dest='post_count', default=15, 117 | help="""Number of posts to fetch""") 118 | parser.add_argument('--time_filter', dest='time_filter', default="month", 119 | help="""day/month/week etc since we are sorting by top""") 120 | parser.add_argument('--fields', dest='fields', default="title,selftext,author,url,id", 121 | help="""Comma-separated list of fields to save""") 122 | 123 | main(args=parser.parse_args()) 124 | sys.exit(0) 125 | -------------------------------------------------------------------------------- /insta_reddit/code/draw_text_on_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read CSV of posts and generate images for them 3 | """ 4 | # Download and install fonts from: https://www.cufonfonts.com/font/helvetica-neue-9 5 | # https://www.cufonfonts.com/font/pragmatica-extralight 6 | 7 | import json 8 | import os 9 | from pathlib import Path 10 | import sys 11 | git_root = str(Path(__file__).parent.parent.parent.resolve()) 12 | sys.path.append(git_root) 13 | 14 | from insta_reddit import credentials 15 | from insta_reddit.code.image_utils import ImageText 16 | from insta_reddit.code.sheets_db import SheetsDb # To read records from GSheets 17 | 18 | MAX_TITLE_LEN = 400 19 | MAX_SELFTEXT_LEN = 830 20 | 21 | 22 | def get_title_and_self_text(record): 23 | """ 24 | Returns title and self text if they fall within the character limits 25 | :param record: Row of content containing title and selftext 26 | :rtype: tuple(str, str) 27 | """ 28 | title, self_text = record['title'], record['selftext'] 29 | 30 | if self_text is None or self_text == "" or len(self_text) < 5: 31 | if title is None or len(title) >= MAX_TITLE_LEN or len(self_text) >= MAX_SELFTEXT_LEN: 32 | return None, None 33 | else: 34 | return title, None 35 | return title, self_text 36 | 37 | 38 | def get_bg_img(): 39 | """Returns a white background ImageText object 40 | """ 41 | return ImageText((1500, 1500), background=(255, 255, 255)) 42 | 43 | 44 | def get_format(): 45 | """Format of the text, courtesy Avishek Rakshit (helluva designer) 46 | """ 47 | return {'subreddit_font': 'arial.ttf', 48 | 'title_font': 'arial.ttf', 49 | 'self_text_font': 'arial.ttf', 50 | 'subreddit_color': (159, 4, 4), 51 | 'title_color': (33, 32, 32), 52 | 'self_text_color': (0, 0, 0) 53 | } 54 | 55 | 56 | def get_img_output_file_paths(record): 57 | """ File paths to save the generated images to 58 | """ 59 | cur_folder_path = "/".join(os.path.dirname(os.path.realpath(__file__)).split('/')[:-1]) 60 | title_path = "".join( 61 | [cur_folder_path, "/content/images/generated/title_", record['id'], ".jpg"]) 62 | self_text_path = "".join( 63 | [cur_folder_path, "/content/images/generated/self_text_", record['id'], ".jpg"]) 64 | # creating directory structure if needed 65 | Path("/".join(title_path.split('/')[:-1])).mkdir(parents=True, exist_ok=True) 66 | return title_path, self_text_path 67 | 68 | 69 | def image_generated(record): 70 | """ Returns True if image has already been generated or uploaded 71 | """ 72 | cur_folder_path = "/".join(os.path.dirname(os.path.realpath(__file__)).split('/')[:-1]) 73 | title_path = "".join( 74 | [cur_folder_path, "/content/images/generated/title_", record['id'], ".jpg"]) 75 | if Path(title_path).is_file() or \ 76 | Path(title_path.replace("generated/title_", "uploaded/title_")).is_file(): 77 | return True 78 | 79 | 80 | def write_on_img(record=None): 81 | """ Generates image(s) for a record with the specified text 82 | """ 83 | record = json.loads(json.dumps(record)) 84 | if image_generated(record): 85 | return 86 | title, self_text = get_title_and_self_text(record) 87 | title_op, self_text_op = get_img_output_file_paths(record) 88 | 89 | """ 90 | Write title_img by default for all unless either title or self text crosses the threshold 91 | Save it as img_title_<>.jpg 92 | Write self_text_img only if there's some sizeable self_text. 93 | img_self_text_<>.jpg 94 | """ 95 | 96 | if title: 97 | title_img = get_bg_img() 98 | title_img.write_vertically_centred_text_box(left_padding=150, upper=0, lower=750, 99 | text="LPT:", 100 | box_width=1200, 101 | font_filename=get_format()['subreddit_font'], 102 | font_size=180, 103 | color=get_format()['subreddit_color'], 104 | place='center') 105 | title_img.write_vertically_centred_text_box(left_padding=150, upper=450, lower=1350, 106 | text=title, 107 | box_width=1200, 108 | font_filename=get_format()['title_font'], 109 | font_size=60, color=get_format()['title_color'], 110 | place='left') 111 | 112 | title_img.save(title_op) 113 | print("Image generated.") 114 | 115 | if self_text: 116 | self_text_img = get_bg_img() 117 | self_text_img.write_vertically_centred_text_box(left_padding=150, upper=300, lower=1200, 118 | text=self_text, box_width=1200, 119 | font_filename=get_format()[ 120 | 'self_text_font'], 121 | font_size=60, 122 | color=get_format()['self_text_color'], 123 | place='left') 124 | self_text_img.save(self_text_op) 125 | 126 | 127 | def main(): 128 | credentials_path = "/".join(os.path.dirname(os.path.realpath(__file__)).split('/')[:-1]) + \ 129 | "/service_account.json" 130 | sdb = SheetsDb(sheet_id=credentials.sheets_url, 131 | credentials_path=credentials_path) 132 | unuploaded_records = sdb.get_unuploaded_rows() 133 | for record in unuploaded_records: 134 | write_on_img(record) 135 | 136 | 137 | if __name__ == "__main__": 138 | main() 139 | -------------------------------------------------------------------------------- /insta_reddit/code/image_utils.py: -------------------------------------------------------------------------------- 1 | # Surya's modification of Alvaro Justen's gist - with due permissions: 2 | # https://gist.github.com/turicas/1455973/8ca2c5fc823b611ea1a0f631fe2fbfef4c9591d7 3 | 4 | from PIL import Image, ImageDraw, ImageFont 5 | 6 | 7 | class ImageText(object): 8 | def __init__(self, filename_or_size, mode='RGBA', background=(0, 0, 0, 0), 9 | encoding='utf8'): 10 | if isinstance(filename_or_size, str): 11 | self.filename = filename_or_size 12 | self.image = Image.open(self.filename) 13 | self.size = self.image.size 14 | elif isinstance(filename_or_size, (list, tuple)): 15 | self.size = filename_or_size 16 | self.image = Image.new(mode, self.size, color=background) 17 | self.filename = None 18 | self.draw = ImageDraw.Draw(self.image) 19 | self.encoding = encoding 20 | 21 | def save(self, filename=None): 22 | if filename.lower().endswith("jpg"): 23 | self.image.convert('RGB').save(filename or self.filename) 24 | elif filename.lower().endswith("png"): 25 | self.image.save(filename or self.filename) 26 | else: 27 | raise Exception("Unknown file format") 28 | 29 | def get_font_size(self, text, font, max_width=None, max_height=None): 30 | if max_width is None and max_height is None: 31 | raise ValueError('You need to pass max_width or max_height') 32 | font_size = 1 33 | text_size = self.get_text_size(font, font_size, text) 34 | if (max_width is not None and text_size[0] > max_width) or \ 35 | (max_height is not None and text_size[1] > max_height): 36 | raise ValueError("Text can't be filled in only (%dpx, %dpx)" % 37 | text_size) 38 | while True: 39 | if (max_width is not None and text_size[0] >= max_width) or \ 40 | (max_height is not None and text_size[1] >= max_height): 41 | return font_size - 1 42 | font_size += 1 43 | text_size = self.get_text_size(font, font_size, text) 44 | 45 | def write_text(self, xy, text, font_filename, font_size=11, 46 | color=(0, 0, 0), max_width=None, max_height=None): 47 | x, y = xy 48 | # if isinstance(text, str): 49 | # text = text.decode(self.encoding) 50 | if font_size == 'fill' and \ 51 | (max_width is not None or max_height is not None): 52 | font_size = self.get_font_size(text, font_filename, max_width, 53 | max_height) 54 | text_size = self.get_text_size(font_filename, font_size, text) 55 | font = ImageFont.truetype(font_filename, font_size) 56 | if x == 'center': 57 | x = (self.size[0] - text_size[0]) / 2 58 | if y == 'center': 59 | y = (self.size[1] - text_size[1]) / 2 60 | self.draw.text((x, y), text, font=font, fill=color) 61 | return text_size 62 | 63 | @staticmethod 64 | def get_text_size(font_filename, font_size, text): 65 | font = ImageFont.truetype(font_filename, font_size) 66 | return font.getsize(text) 67 | 68 | def write_text_box(self, xy, text, box_width, font_filename, 69 | font_size=11, color=(0, 0, 0), place='left', 70 | justify_last_line=False): 71 | x, y = xy 72 | lines = [] 73 | line = [] 74 | words = text.split() 75 | for word in words: 76 | new_line = ' '.join(line + [word]) 77 | size = self.get_text_size(font_filename, font_size, new_line) 78 | text_height = size[1] 79 | if size[0] <= box_width: 80 | line.append(word) 81 | else: 82 | lines.append(line) 83 | line = [word] 84 | if line: 85 | lines.append(line) 86 | lines = [' '.join(line) for line in lines if line] 87 | height = y 88 | for index, line in enumerate(lines): 89 | if place == 'left': 90 | self.write_text((x, height), line, font_filename, font_size, 91 | color) 92 | elif place == 'right': 93 | total_size = self.get_text_size(font_filename, font_size, line) 94 | x_left = x + box_width - total_size[0] 95 | self.write_text((x_left, height), line, font_filename, 96 | font_size, color) 97 | elif place == 'center': 98 | total_size = self.get_text_size(font_filename, font_size, line) 99 | x_left = int(x + ((box_width - total_size[0]) / 2)) 100 | self.write_text((x_left, height), line, font_filename, 101 | font_size, color) 102 | elif place == 'justify': 103 | words = line.split() 104 | if (index == len(lines) - 1 and not justify_last_line) or \ 105 | len(words) == 1: 106 | self.write_text((x, height), line, font_filename, font_size, 107 | color) 108 | continue 109 | line_without_spaces = ''.join(words) 110 | total_size = self.get_text_size(font_filename, font_size, 111 | line_without_spaces) 112 | space_width = (box_width - total_size[0]) / (len(words) - 1.0) 113 | start_x = x 114 | for word in words[:-1]: 115 | self.write_text((start_x, height), word, font_filename, 116 | font_size, color) 117 | word_size = self.get_text_size(font_filename, font_size, 118 | word) 119 | start_x += word_size[0] + space_width 120 | last_word_size = self.get_text_size(font_filename, font_size, 121 | words[-1]) 122 | last_word_x = x + box_width - last_word_size[0] 123 | self.write_text((last_word_x, height), words[-1], font_filename, 124 | font_size, color) 125 | height += text_height 126 | return box_width, height - y 127 | 128 | def write_vertically_centred_text_box(self, left_padding, upper, lower, text, box_width, 129 | font_filename, 130 | font_size=11, color=(0, 0, 0), place='left', 131 | justify_last_line=False): 132 | """ 133 | Create a textbox which is vertically centred within the limits of a container in the image 134 | :param left_padding: Pixels to leave on the left side before textbox begins 135 | :param upper: Upper pixel index to start the vertical centralizing container 136 | :param lower: Lower pixel index to end the vertical centralizing container 137 | :param text: Text to write on the image 138 | :param box_width: How wide the textbox will be 139 | :param font_filename: Font file name with .ttf suffix 140 | :param font_size: Size of the font 141 | :param color: Color of the font 142 | :param place: Text alignment: left, write, center, justify. 143 | :param justify_last_line: Whether to justify the last line 144 | :return: Dimensions of the textbox (width, height) 145 | """ 146 | x = left_padding 147 | lines = [] 148 | line = [] 149 | words = text.split() 150 | for word in words: 151 | new_line = ' '.join(line + [word]) 152 | size = self.get_text_size(font_filename, font_size, new_line) 153 | text_height = size[1] 154 | if size[0] <= box_width: 155 | line.append(word) 156 | else: 157 | lines.append(line) 158 | line = [word] 159 | if line: 160 | lines.append(line) 161 | lines = [' '.join(line) for line in lines if line] 162 | 163 | # Calculate y here 164 | height_of_text = len(lines) * text_height 165 | mid_section = ((upper + lower) / 2) 166 | y = mid_section - (height_of_text / 2) 167 | # End of calculate y here 168 | 169 | height = y 170 | for index, line in enumerate(lines): 171 | if place == 'left': 172 | self.write_text((x, height), line, font_filename, font_size, 173 | color) 174 | elif place == 'right': 175 | total_size = self.get_text_size(font_filename, font_size, line) 176 | x_left = x + box_width - total_size[0] 177 | self.write_text((x_left, height), line, font_filename, 178 | font_size, color) 179 | elif place == 'center': 180 | total_size = self.get_text_size(font_filename, font_size, line) 181 | x_left = int(x + ((box_width - total_size[0]) / 2)) 182 | self.write_text((x_left, height), line, font_filename, 183 | font_size, color) 184 | elif place == 'justify': 185 | words = line.split() 186 | if (index == len(lines) - 1 and not justify_last_line) or \ 187 | len(words) == 1: 188 | self.write_text((x, height), line, font_filename, font_size, 189 | color) 190 | continue 191 | line_without_spaces = ''.join(words) 192 | total_size = self.get_text_size(font_filename, font_size, 193 | line_without_spaces) 194 | space_width = (box_width - total_size[0]) / (len(words) - 1.0) 195 | start_x = x 196 | for word in words[:-1]: 197 | self.write_text((start_x, height), word, font_filename, 198 | font_size, color) 199 | word_size = self.get_text_size(font_filename, font_size, 200 | word) 201 | start_x += word_size[0] + space_width 202 | last_word_size = self.get_text_size(font_filename, font_size, 203 | words[-1]) 204 | last_word_x = x + box_width - last_word_size[0] 205 | self.write_text((last_word_x, height), words[-1], font_filename, 206 | font_size, color) 207 | height += text_height # moved this to the end else a 208 | return box_width, height - y 209 | --------------------------------------------------------------------------------