diff --git a/epd/epd_config.py b/epd/epd_config.py index 0478437..242edd4 100644 --- a/epd/epd_config.py +++ b/epd/epd_config.py @@ -1,48 +1,122 @@ -palArr=[[[0,0,0],[255,255,255]], -[[0,0,0],[255,255,255],[127,0,0]], -[[0,0,0],[255,255,255],[127,127,127]], -[[0,0,0],[255,255,255],[127,127,127],[127,0,0]], -[[0,0,0],[255,255,255]], -[[0,0,0],[255,255,255],[220,180,0]], -[[0,0,0]], -[[0,0,0],[255,255,255],[0,255,0],[0,0,255],[255,0,0],[255,255,0],[255,128,0]]]; +palArr = [ + [[0, 0, 0], [255, 255, 255]], + [[0, 0, 0], [255, 255, 255], [127, 0, 0]], + [[0, 0, 0], [255, 255, 255], [127, 127, 127]], + [[0, 0, 0], [255, 255, 255], [127, 127, 127], [127, 0, 0]], + [[0, 0, 0], [255, 255, 255]], + [[0, 0, 0], [255, 255, 255], [220, 180, 0]], + [[0, 0, 0]], + [ + [0, 0, 0], + [255, 255, 255], + [0, 255, 0], + [0, 0, 255], + [255, 0, 0], + [255, 255, 0], + [255, 128, 0], + ], +] -epdArr=[[200,200,0],[200,200,3],[152,152,5], -[122,250,0],[104,212,1],[104,212,5],[104,212,0], -[176,264,0],[176,264,1], -[128,296,0],[128,296,1],[128,296,5],[128,296,0], -[400,300,0],[400,300,1],[400,300,5], -[600,448,0],[600,448,1],[600,448,5], -[640,384,0],[640,384,1],[640,384,5], -[800,480,0],[800,480,1],[880,528,1], -[600,448,7],[880,528,0],[280,480,0], -[152,296,0],[648,480,1],[128,296,1], -[200,200,1],[104,214,1],[128,296,0], -[400,300,1],[152,296,1],[648,480,0], -[640,400,7],[176,264,1],[122,250,0], -[122,250,1],[240,360,0],[176,264,0], -[122,250,0],[400,300,0],[960,680,0], -[800,480,0],[128,296,1],[960,680,1]]; +epdArr = [ + [200, 200, 0], + [200, 200, 3], + [152, 152, 5], + [122, 250, 0], + [104, 212, 1], + [104, 212, 5], + [104, 212, 0], + [176, 264, 0], + [176, 264, 1], + [128, 296, 0], + [128, 296, 1], + [128, 296, 5], + [128, 296, 0], + [400, 300, 0], + [400, 300, 1], + [400, 300, 5], + [600, 448, 0], + [600, 448, 1], + [600, 448, 5], + [640, 384, 0], + [640, 384, 1], + [640, 384, 5], + [800, 480, 0], + [800, 480, 1], + [880, 528, 1], + [600, 448, 7], + [880, 528, 0], + [280, 480, 0], + [152, 296, 0], + [648, 480, 1], + [128, 296, 1], + [200, 200, 1], + [104, 214, 1], + [128, 296, 0], + [400, 300, 1], + [152, 296, 1], + [648, 480, 0], + [640, 400, 7], + [176, 264, 1], + [122, 250, 0], + [122, 250, 1], + [240, 360, 0], + [176, 264, 0], + [122, 250, 0], + [400, 300, 0], + [960, 680, 0], + [800, 480, 0], + [128, 296, 1], + [960, 680, 1], +] EPD_TYPES = { - '1.54': 0, '1.54b': 1, '1.54c': 2, - '2.13': 3, '2.13b': 4, '2.13c': 5, '2.13d': 6, - '2.7': 7, '2.7b': 8, - '2.9': 9, '2.9b': 10, '2.9c': 11, '2.9d': 12, - '4.2': 13, '4.2b': 14, '4.2c': 15, - '5.83': 16, '5.83b': 17, '5.83c': 18, - '7.5': 19, '7.5b': 20, '7.5c': 21, - '7.5 V2': 22, '7.5b V2': 23, - '7.5b HD': 24, '5.65f': 25, - '7.5 HD': 26, '3.7': 27, '2.66': 28, - '5.83b V2': 29, '2.9b V3': 30, - '1.54b V2': 31, '2.13b V3': 32, - '2.9 V2': 33, '4.2b V2': 34, - '2.66b': 35, '5.83 V2': 36, - '4.01 f': 37, '2.7b V2': 38, - '2.13 V3': 39, '2.13 B V4': 40, - '3.52': 41, '2.7 V2': 42, - '2.13 V4': 43, '4.2 V2': 44, - '13.3k': 45, '4.26': 46, - '2.9bV4': 47, '13.3b': 48 + "1.54": 0, + "1.54b": 1, + "1.54c": 2, + "2.13": 3, + "2.13b": 4, + "2.13c": 5, + "2.13d": 6, + "2.7": 7, + "2.7b": 8, + "2.9": 9, + "2.9b": 10, + "2.9c": 11, + "2.9d": 12, + "4.2": 13, + "4.2b": 14, + "4.2c": 15, + "5.83": 16, + "5.83b": 17, + "5.83c": 18, + "7.5": 19, + "7.5b": 20, + "7.5c": 21, + "7.5 V2": 22, + "7.5b V2": 23, + "7.5b HD": 24, + "5.65f": 25, + "7.5 HD": 26, + "3.7": 27, + "2.66": 28, + "5.83b V2": 29, + "2.9b V3": 30, + "1.54b V2": 31, + "2.13b V3": 32, + "2.9 V2": 33, + "4.2b V2": 34, + "2.66b": 35, + "5.83 V2": 36, + "4.01 f": 37, + "2.7b V2": 38, + "2.13 V3": 39, + "2.13 B V4": 40, + "3.52": 41, + "2.7 V2": 42, + "2.13 V4": 43, + "4.2 V2": 44, + "13.3k": 45, + "4.26": 46, + "2.9bV4": 47, + "13.3b": 48, } diff --git a/epd/epd_uploader.py b/epd/epd_uploader.py index 10bcdaf..78db32b 100644 --- a/epd/epd_uploader.py +++ b/epd/epd_uploader.py @@ -6,64 +6,70 @@ import sys def byteToStr(v): return chr((v & 0xF) + 97) + chr(((v >> 4) & 0xF) + 97) + def wordToStr(v): return byteToStr(v & 0xFF) + byteToStr((v >> 8) & 0xFF) + # Reimplementation of u_data from scriptD.js def _u_data(pixel_indices, c, px_ind_start): - rqMsg = '' + rqMsg = "" pxInd = px_ind_start - max_rq_len = 1000 # Max length of rqMsg + max_rq_len = 1000 # Max length of rqMsg - if c == -1: # 16-bit values + if c == -1: # 16-bit values while pxInd < len(pixel_indices) and len(rqMsg) < max_rq_len: v = 0 - for i in range(0, 16, 2): # This means 8 iterations, packing 8 pixels + for i in range(0, 16, 2): # This means 8 iterations, packing 8 pixels if pxInd < len(pixel_indices): - v |= (pixel_indices[pxInd] << i) # This is likely wrong, should be 4 bits per pixel + v |= ( + pixel_indices[pxInd] << i + ) # This is likely wrong, should be 4 bits per pixel pxInd += 1 rqMsg += wordToStr(v) - elif c == -2: # 7-color, 4-bit values + elif c == -2: # 7-color, 4-bit values while pxInd < len(pixel_indices) and len(rqMsg) < max_rq_len: v = 0 - for i in range(0, 16, 4): # 4 iterations, packing 4 pixels + for i in range(0, 16, 4): # 4 iterations, packing 4 pixels if pxInd < len(pixel_indices): - v |= (pixel_indices[pxInd] << i) + v |= pixel_indices[pxInd] << i pxInd += 1 rqMsg += wordToStr(v) - else: # Monochrome, 1-bit values + else: # Monochrome, 1-bit values while pxInd < len(pixel_indices) and len(rqMsg) < max_rq_len: v = 0 - for i in range(8): # Packs 8 pixels into 1 byte + for i in range(8): # Packs 8 pixels into 1 byte if pxInd < len(pixel_indices) and pixel_indices[pxInd] != c: - v |= (128 >> i) + v |= 128 >> i pxInd += 1 rqMsg += byteToStr(v) return rqMsg, pxInd + # Reimplementation of u_line from scriptD.js def _u_line(pixel_indices, c, px_ind_start): - rqMsg = '' + rqMsg = "" pxInd = px_ind_start - max_rq_len = 1000 # Max length of rqMsg + max_rq_len = 1000 # Max length of rqMsg while len(rqMsg) < max_rq_len: x = 0 - while x < 122: # Processes 122 pixels at a time + while x < 122: # Processes 122 pixels at a time v = 0 for i in range(8): if pxInd < len(pixel_indices) and x < 122: if pixel_indices[pxInd] != c: - v |= (128 >> i) + v |= 128 >> i pxInd += 1 x += 1 else: - break # Break if no more pixels or 122 pixels processed + break # Break if no more pixels or 122 pixels processed rqMsg += byteToStr(v) - if x >= 122: # If 122 pixels processed, break inner loop + if x >= 122: # If 122 pixels processed, break inner loop break return rqMsg, pxInd + def upload_image(img, ip_addr, epd_ind, epdArr, palArr, getNear): print(f"[DEBUG] Starting upload_image for EPD index {epd_ind} to {ip_addr}") url_prefix = f"http://{ip_addr}/" @@ -72,7 +78,7 @@ def upload_image(img, ip_addr, epd_ind, epdArr, palArr, getNear): pal_ind = epdArr[epd_ind][2] curPal = palArr[pal_ind] pixels = list(img.getdata()) - pixel_indices = [getNear(r,g,b, curPal) for r,g,b in pixels] + pixel_indices = [getNear(r, g, b, curPal) for r, g, b in pixels] pxInd = 0 stInd = 0 @@ -82,44 +88,46 @@ def upload_image(img, ip_addr, epd_ind, epdArr, palArr, getNear): full_url = url_prefix + cmd try: response = requests.post(full_url) - response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) + response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) if next_state: stInd += 1 return 0 except requests.exceptions.RequestException as e: print(f"[ERROR] Error sending command {cmd}: {e}") - raise # Re-raise the exception to stop execution + raise # Re-raise the exception to stop execution def u_next(): nonlocal pxInd pxInd = 0 - return u_send('NEXT_', True) + return u_send("NEXT_", True) def u_done(): - sys.stdout.write('\n') # Ensure progress bar finishes on a new line + sys.stdout.write("\n") # Ensure progress bar finishes on a new line print("Image upload complete!") - return u_send('SHOW_', True) + return u_send("SHOW_", True) def u_show(a_len, k1, k2, rqMsg_val): nonlocal pxInd total_pixels = len(pixel_indices) current_progress_pixels = pxInd - overall_percentage = (current_progress_pixels / total_pixels) * 100 if total_pixels > 0 else 0 + overall_percentage = ( + (current_progress_pixels / total_pixels) * 100 if total_pixels > 0 else 0 + ) bar_length = 50 filled_length = int(bar_length * overall_percentage // 100) - bar = '#' * filled_length + '-' * (bar_length - filled_length) - sys.stdout.write(f'\rProgress: |{bar}| {overall_percentage:.2f}%') + bar = "#" * filled_length + "-" * (bar_length - filled_length) + sys.stdout.write(f"\rProgress: |{bar}| {overall_percentage:.2f}%") sys.stdout.flush() - return u_send(rqMsg_val + wordToStr(len(rqMsg_val)) + 'LOAD_', pxInd >= a_len) + return u_send(rqMsg_val + wordToStr(len(rqMsg_val)) + "LOAD_", pxInd >= a_len) # Main upload logic based on scriptD.js try: # Initial EPD command epd_cmd_char = chr(epd_ind + 97) if epd_ind < 26 else chr(epd_ind - 26 + 65) - u_send(f'EPD{epd_cmd_char}_') + u_send(f"EPD{epd_cmd_char}_") # Conditional logic based on epd_ind, mimicking scriptD.js if epd_ind in [3, 39, 43]: @@ -185,8 +193,12 @@ def upload_image(img, ip_addr, epd_ind, epdArr, palArr, getNear): # Special handling for 7.5b V2 (epd_ind 23) red channel if epd_ind == 23: # Encode only red pixels (palette index 2) - red_pixel_indices = [0 if p == 2 else 2 for p in pixel_indices] # Invert red channel: 0 for red, 2 for non-red - rqMsg, pxInd = _u_data(red_pixel_indices, 0, pxInd) # Use monochrome encoding for this layer + red_pixel_indices = [ + 0 if p == 2 else 2 for p in pixel_indices + ] # Invert red channel: 0 for red, 2 for non-red + rqMsg, pxInd = _u_data( + red_pixel_indices, 0, pxInd + ) # Use monochrome encoding for this layer else: rqMsg, pxInd = _u_data(pixel_indices, 3, pxInd) u_show(len(pixel_indices), 50, 50, rqMsg) diff --git a/epd/image_processor.py b/epd/image_processor.py index e787b4b..8162134 100644 --- a/epd/image_processor.py +++ b/epd/image_processor.py @@ -1,27 +1,31 @@ from PIL import Image -def getErr(r,g,b,stdCol): - r-=stdCol[0] - g-=stdCol[1] - b-=stdCol[2] - return r*r + g*g + b*b -def getNear(r,g,b, curPal): - ind=0 - err=getErr(r,g,b,curPal[0]) - for i in range(1,len(curPal)): - cur=getErr(r,g,b,curPal[i]) - if (cur 1: - errArr[bInd][i+1] = addVal(errArr[bInd][i+1], r_err, g_err, b_err, 2.0) - errArr[aInd][i+1] = addVal(errArr[aInd][i+1], r_err, g_err, b_err, 7.0) + errArr[bInd][i + 1] = addVal( + errArr[bInd][i + 1], r_err, g_err, b_err, 2.0 + ) + errArr[aInd][i + 1] = addVal( + errArr[aInd][i + 1], r_err, g_err, b_err, 7.0 + ) elif i == dW - 1: - errArr[bInd][i-1] = addVal(errArr[bInd][i-1], r_err, g_err, b_err, 7.0) - errArr[bInd][i] = addVal(errArr[bInd][i], r_err, g_err, b_err, 9.0) + errArr[bInd][i - 1] = addVal( + errArr[bInd][i - 1], r_err, g_err, b_err, 7.0 + ) + errArr[bInd][i] = addVal( + errArr[bInd][i], r_err, g_err, b_err, 9.0 + ) else: - errArr[bInd][i-1] = addVal(errArr[bInd][i-1], r_err, g_err, b_err, 3.0) - errArr[bInd][i] = addVal(errArr[bInd][i], r_err, g_err, b_err, 5.0) - errArr[bInd][i+1] = addVal(errArr[bInd][i+1], r_err, g_err, b_err, 1.0) - errArr[aInd][i+1] = addVal(errArr[aInd][i+1], r_err, g_err, b_err, 7.0) + errArr[bInd][i - 1] = addVal( + errArr[bInd][i - 1], r_err, g_err, b_err, 3.0 + ) + errArr[bInd][i] = addVal( + errArr[bInd][i], r_err, g_err, b_err, 5.0 + ) + errArr[bInd][i + 1] = addVal( + errArr[bInd][i + 1], r_err, g_err, b_err, 1.0 + ) + errArr[aInd][i + 1] = addVal( + errArr[aInd][i + 1], r_err, g_err, b_err, 7.0 + ) else: - pixels[i, j] = tuple(curPal[(i + j) % 2]) # checkerboard + pixels[i, j] = tuple(curPal[(i + j) % 2]) # checkerboard return pDst diff --git a/pyproject.toml b/pyproject.toml index 251f305..42ed753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,6 @@ dependencies = [ "Pillow==11.3.0", "python-dotenv==1.1.1", ] + +[project.optional-dependencies] +dev = ["black==24.4.2"] diff --git a/szuru-eink.py b/szuru-eink.py index 1757cb0..ceb5eec 100644 --- a/szuru-eink.py +++ b/szuru-eink.py @@ -16,7 +16,6 @@ from epd.image_processor import getErr, getNear, addVal, procImg from epd.epd_uploader import upload_image - # get truly random post try: szuru_url = os.environ["SZURU_URL"] @@ -32,11 +31,13 @@ post = None while post is None: random_id = random.randint(0, highest_post.id_) temp_post = booru.getPost(random_id) - if temp_post and temp_post.mime.startswith('image/') and temp_post.content: + if temp_post and temp_post.mime.startswith("image/") and temp_post.content: post = temp_post print(f"[DEBUG] Found image post with ID: {post.id_}, MIME: {post.mime}") else: - print(f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image.") + print( + f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image." + ) # download image image_downloaded = False @@ -45,19 +46,21 @@ while not image_downloaded: r = requests.get(post.content, stream=True) print(f"[DEBUG] Downloading image from {post.content}") r.raise_for_status() - with open("image.jpg", 'wb') as f: + with open("image.jpg", "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) print("[DEBUG] Image downloaded and saved as image.jpg") # Attempt to open the image to verify img = Image.open("image.jpg") - img.verify() # Verify that it is an image + img.verify() # Verify that it is an image img.close() image_downloaded = True print("[DEBUG] Image successfully verified.") except (PIL.UnidentifiedImageError, requests.exceptions.RequestException) as e: - print(f"[ERROR] Failed to process downloaded file (Error: {e}). Retrying with a new post...") + print( + f"[ERROR] Failed to process downloaded file (Error: {e}). Retrying with a new post..." + ) # Clean up the invalid file if os.path.exists("image.jpg"): os.remove("image.jpg") @@ -66,13 +69,17 @@ while not image_downloaded: while post is None: random_id = random.randint(0, highest_post.id_) temp_post = booru.getPost(random_id) - if temp_post and temp_post.mime.startswith('image/') and temp_post.content: + if temp_post and temp_post.mime.startswith("image/") and temp_post.content: post = temp_post - print(f"[DEBUG] Found new image post with ID: {post.id_}, MIME: {post.mime}") + print( + f"[DEBUG] Found new image post with ID: {post.id_}, MIME: {post.mime}" + ) else: - print(f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image.") + print( + f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image." + ) -# Process and upload + # Process and upload img = Image.open("image.jpg") # Get configuration from environment variables @@ -84,7 +91,9 @@ except KeyError as e: raise ValueError(f"Missing required environment variable: {e}") if epd_type_str not in EPD_TYPES: - raise ValueError(f"Invalid EPD_TYPE: {epd_type_str}. Must be one of {list(EPD_TYPES.keys())}") + raise ValueError( + f"Invalid EPD_TYPE: {epd_type_str}. Must be one of {list(EPD_TYPES.keys())}" + ) epd_ind = EPD_TYPES[epd_type_str] is_red = False @@ -93,12 +102,16 @@ if dithering_mode == "color": # EPDs that are explicitly monochrome in scriptD.js despite palArr: monochrome_only_epds = [0, 3, 6, 7, 9, 12, 16, 19, 22, 26, 27, 28, 39, 40, 43] if epd_ind in monochrome_only_epds: - print(f"[WARNING] Dithering mode is set to 'color', but EPD type '{epd_type_str}' (Index {epd_ind}) is treated as monochrome by the device's protocol. Color will not appear.") - is_red = False # Force monochrome for this EPD type + print( + f"[WARNING] Dithering mode is set to 'color', but EPD type '{epd_type_str}' (Index {epd_ind}) is treated as monochrome by the device's protocol. Color will not appear." + ) + is_red = False # Force monochrome for this EPD type else: is_red = True elif dithering_mode != "mono": - raise ValueError(f"Invalid DITHERING_MODE: {dithering_mode}. Must be 'mono' or 'color'.") + raise ValueError( + f"Invalid DITHERING_MODE: {dithering_mode}. Must be 'mono' or 'color'." + ) # Get display dimensions and palette from epdArr dW, dH, pal_ind = epdArr[epd_ind] @@ -122,9 +135,11 @@ print(f"[DEBUG] EPD Type String: {epd_type_str}") print(f"[DEBUG] EPD Index: {epd_ind}") print(f"[DEBUG] Palette Index: {pal_ind}") # Process the final image with dithering -processed_img = procImg(img, dW, dH, 0, 0, pal_ind, isLvl=False, isRed=is_red, palArr=palArr) +processed_img = procImg( + img, dW, dH, 0, 0, pal_ind, isLvl=False, isRed=is_red, palArr=palArr +) -processed_img.save("processed.png") # Save for debugging +processed_img.save("processed.png") # Save for debugging print("[DEBUG] Imaged processed and saved as processed.png") upload_image(processed_img, ip_addr, epd_ind, epdArr, palArr, getNear)