cookertron

Image Viewer

Aug 26th, 2024 (edited)
54
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.32 KB | Source Code | 0 0
  1. from kivy.app import App
  2. from kivy.uix.boxlayout import BoxLayout
  3. from kivy.uix.gridlayout import GridLayout
  4. from kivy.uix.scrollview import ScrollView
  5. from kivy.uix.label import Label
  6. from kivy.uix.textinput import TextInput
  7. from kivy.uix.button import Button
  8. from kivy.uix.image import Image
  9. from kivy.core.image import Image as CoreImage
  10. from kivy.core.window import Window
  11. from kivy.uix.popup import Popup
  12. from kivy.uix.scatter import Scatter
  13. from kivy.uix.progressbar import ProgressBar
  14. from kivy.clock import Clock
  15. from kivy.uix.scatterlayout import ScatterPlane
  16. from kivy.graphics.transformation import Matrix
  17. from kivy.vector import Vector
  18. import requests
  19. import base64
  20. import io
  21. import json
  22. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  23. from cryptography.hazmat.backends import default_backend
  24. from cryptography.hazmat.primitives import padding
  25. import hashlib
  26. import threading
  27. import queue
  28.  
  29. class ZoomableImage(ScatterPlane):
  30.     def __init__(self, texture, **kwargs):
  31.         super(ZoomableImage, self).__init__(**kwargs)
  32.         self.texture = texture
  33.         self.do_rotation = False
  34.         self.do_scale = True
  35.         self.do_translation = True
  36.        
  37.         # Create the image widget
  38.         self.image = Image(texture=texture, size=texture.size)
  39.         self.add_widget(self.image)
  40.        
  41.         # Set up initial state
  42.         self.original_size = texture.size
  43.         self.fit_image_to_screen()
  44.        
  45.         # Set up zoom limits
  46.         self.min_zoom = self.scale
  47.         self.max_zoom = 3.0  # Allow zooming in up to 3x the original size
  48.  
  49.     def fit_image_to_screen(self):
  50.         # Calculate the scale to fit the image within the window
  51.         scale_x = Window.width / self.original_size[0]
  52.         scale_y = Window.height / self.original_size[1]
  53.         self.scale = min(scale_x, scale_y) * 0.9  # 90% of fit scale for padding
  54.        
  55.         # Center the image
  56.         self.center = Window.center
  57.  
  58.     def on_touch_down(self, touch):
  59.         if touch.is_double_tap:
  60.             self.fit_image_to_screen()
  61.             return True
  62.         return super(ZoomableImage, self).on_touch_down(touch)
  63.  
  64.     def on_touch_move(self, touch):
  65.         if len(self._touches) >= 2:
  66.             # Use two fingers to zoom
  67.             touch1, touch2 = self._touches[:2]
  68.             old_distance = Vector(touch1.px - touch2.px, touch1.py - touch2.py).length()
  69.             new_distance = Vector(touch1.x - touch2.x, touch1.y - touch2.y).length()
  70.            
  71.             if old_distance > 0 and new_distance > 0:
  72.                 scale = new_distance / old_distance
  73.                 new_scale = scale * self.scale
  74.                 if self.min_zoom <= new_scale <= self.max_zoom:
  75.                     self.scale = new_scale
  76.                     self.center = touch.pos
  77.         else:
  78.             # Use one finger to pan
  79.             super(ZoomableImage, self).on_touch_move(touch)
  80.  
  81. class ImageViewer(BoxLayout):
  82.     def __init__(self, **kwargs):
  83.         super().__init__(**kwargs)
  84.         self.orientation = 'vertical'
  85.         self.gallery_url = None
  86.         self.password = None
  87.         self.images = []
  88.         self.encrypted = False
  89.         self.album_title = ""
  90.         self.image_queue = queue.Queue()
  91.        
  92.         self.input_layout = BoxLayout(orientation='vertical', size_hint=(1, None), height=200)
  93.         self.add_widget(self.input_layout)
  94.        
  95.         self.input_layout.add_widget(Label(text='Enter Gallery URL or ID:'))
  96.         self.url_input = TextInput(multiline=False)
  97.         self.input_layout.add_widget(self.url_input)
  98.        
  99.         self.input_layout.add_widget(Label(text='Enter password (leave empty if none):'))
  100.         self.password_input = TextInput(multiline=False, password=True)
  101.         self.input_layout.add_widget(self.password_input)
  102.        
  103.         self.submit_button = Button(text='Submit')
  104.         self.submit_button.bind(on_press=self.on_submit)
  105.         self.input_layout.add_widget(self.submit_button)
  106.        
  107.         self.progress_bar = ProgressBar(max=100, size_hint=(1, None), height=40)
  108.         self.add_widget(self.progress_bar)
  109.        
  110.         self.scroll_view = ScrollView(size_hint=(1, None), size=(Window.width, Window.height - 240))
  111.         self.add_widget(self.scroll_view)
  112.        
  113.         self.image_grid = GridLayout(cols=3, spacing=10, size_hint_y=None)
  114.         self.image_grid.bind(minimum_height=self.image_grid.setter('height'))
  115.         self.scroll_view.add_widget(self.image_grid)
  116.  
  117.     def on_submit(self, instance):
  118.         self.gallery_url = self.process_url(self.url_input.text)
  119.         self.password = self.password_input.text
  120.         self.load_gallery_info()
  121.  
  122.     def process_url(self, url):
  123.         if url.startswith('http://') or url.startswith('https://'):
  124.             paste_id = url.split('/')[-1]
  125.             return f'https://paste.ee/r/{paste_id}'
  126.         else:
  127.             return f'https://paste.ee/r/{url}'
  128.  
  129.     def decrypt_data(self, encrypted_data):
  130.         try:
  131.             encrypted_data = base64.b64decode(encrypted_data)
  132.             iv = encrypted_data[:16]
  133.             encrypted_data = encrypted_data[16:]
  134.             key = hashlib.sha256(self.password.encode()).digest()
  135.             cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
  136.             decryptor = cipher.decryptor()
  137.             decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
  138.             unpadder = padding.PKCS7(128).unpadder()
  139.             unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
  140.             return unpadded_data.decode()
  141.         except Exception as e:
  142.             print(f"Decryption error: {e}")
  143.             return None
  144.  
  145.     def verify_password(self, encrypted_hash):
  146.         if not encrypted_hash:
  147.             return False
  148.         decrypted_hash = self.decrypt_data(encrypted_hash)
  149.         if decrypted_hash is None:
  150.             return False
  151.        
  152.         salt = base64.b64decode(decrypted_hash)[:16]
  153.         stored_hash = base64.b64decode(decrypted_hash)[16:]
  154.        
  155.         computed_hash = hashlib.sha256(salt + self.password.encode()).digest()
  156.         return computed_hash == stored_hash
  157.  
  158.     def load_gallery_info(self):
  159.         try:
  160.             response = requests.get(self.gallery_url)
  161.             gallery_info = json.loads(response.text)
  162.            
  163.             self.encrypted = gallery_info.get('encrypted', False)
  164.            
  165.             if self.encrypted:
  166.                 password_hash = gallery_info.get('password_hash')
  167.                 if not password_hash or not self.verify_password(password_hash):
  168.                     self.show_error_message("Incorrect password or missing password hash. Unable to decrypt gallery.")
  169.                     return
  170.                
  171.                 decrypted_title = self.decrypt_data(gallery_info['album_title'])
  172.                 self.album_title = decrypted_title if decrypted_title else gallery_info['album_title']
  173.             else:
  174.                 self.album_title = gallery_info['album_title']
  175.            
  176.             self.images = gallery_info['image_list']
  177.            
  178.             # Start loading images in a separate thread
  179.             threading.Thread(target=self.load_images).start()
  180.             # Start processing the queue in the main thread
  181.             Clock.schedule_interval(self.process_image_queue, 0.1)
  182.         except requests.exceptions.RequestException:
  183.             self.show_error_message("Network error. Please check your connection and try again.")
  184.         except json.JSONDecodeError:
  185.             self.show_error_message("Error decoding gallery information. Please check the URL.")
  186.  
  187.     def load_images(self):
  188.         total_images = len(self.images)
  189.         for i, url in enumerate(self.images):
  190.             full_url = self.process_url(url)
  191.             try:
  192.                 response = requests.get(full_url)
  193.                 image_data = response.text
  194.                 if self.encrypted and self.password:
  195.                     decrypted_data = self.decrypt_data(image_data)
  196.                     image_data = decrypted_data if decrypted_data else image_data
  197.                 decoded_image = base64.b64decode(image_data)
  198.                
  199.                 # Put the decoded image data in the queue
  200.                 self.image_queue.put((decoded_image, i, total_images))
  201.             except Exception as e:
  202.                 print(f"Error loading image: {e}")
  203.                 Clock.schedule_once(lambda dt: self.show_error_message(f"Error loading image: {str(e)}"))
  204.  
  205.     def process_image_queue(self, dt):
  206.         try:
  207.             while True:
  208.                 decoded_image, i, total_images = self.image_queue.get_nowait()
  209.                
  210.                 # Load image data into memory
  211.                 buf = io.BytesIO(decoded_image)
  212.                 coreimage = CoreImage(buf, ext='png')
  213.                
  214.                 # Create Image widget from CoreImage
  215.                 image = Image(texture=coreimage.texture, size_hint=(1, None), height=200)
  216.                 image.bind(on_touch_down=self.on_image_touch)
  217.                 self.image_grid.add_widget(image)
  218.                
  219.                 # Update progress bar
  220.                 progress = (i + 1) / total_images * 100
  221.                 self.progress_bar.value = progress
  222.  
  223.                 if i + 1 == total_images:
  224.                     # All images loaded, stop the scheduled interval
  225.                     Clock.unschedule(self.process_image_queue)
  226.                     # Hide progress bar when done
  227.                     self.progress_bar.opacity = 0
  228.         except queue.Empty:
  229.             pass
  230.  
  231.     def on_image_touch(self, instance, touch):
  232.         if instance.collide_point(*touch.pos):
  233.             content = ZoomableImage(instance.texture)
  234.             popup = Popup(title=self.album_title, content=content, size_hint=(0.9, 0.9))
  235.             # Bind to window size to update image position and scale when orientation changes
  236.             Window.bind(on_resize=lambda instance, width, height: content.fit_image_to_screen())
  237.             popup.open()
  238.  
  239.     def show_error_message(self, message):
  240.         popup = Popup(title='Error', content=Label(text=message), size_hint=(0.8, 0.4))
  241.         popup.open()
  242.  
  243. class GalleryViewerApp(App):
  244.     def build(self):
  245.         return ImageViewer()
  246.  
  247. if __name__ == '__main__':
  248.     GalleryViewerApp().run()
Add Comment
Please, Sign In to add comment