Skip to content

Commit

Permalink
Small bug fix for android
Browse files Browse the repository at this point in the history
  • Loading branch information
Commandcracker committed May 23, 2024
1 parent bc65fd6 commit 3afe540
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/gucken/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.5"
__version__ = "0.1.6"
11 changes: 8 additions & 3 deletions src/gucken/player/android.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from .common import Player


class AndroidChoosePlayer(Player):
# This is just that you can check if the player is an Android player
class AndroidPlayer(Player):
pass


class AndroidChoosePlayer(AndroidPlayer):
def play(
self,
url: str,
Expand All @@ -24,7 +29,7 @@ def play(


# http://mpv-android.github.io/mpv-android/intent.html
class AndroidMPVPlayer(Player):
class AndroidMPVPlayer(AndroidPlayer):
def play(
self,
url: str,
Expand All @@ -47,7 +52,7 @@ def play(


# https://wiki.videolan.org/Android_Player_Intents/
class AndroidVLCPlayer(Player):
class AndroidVLCPlayer(AndroidPlayer):
def play(
self,
url: str,
Expand Down
2 changes: 1 addition & 1 deletion src/gucken/player/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def is_available(cls) -> bool:
@staticmethod
@abstractmethod
def detect_executable() -> str:
raise NotImplementedError
pass

@abstractmethod
def play(
Expand Down
6 changes: 2 additions & 4 deletions src/gucken/player/flatpak.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
from .vlc import VLCPlayer


# This is just that you can check if the player is a flatpak player
# This is just that you can check if the player is a Flatpak player
class FlatpakPlayer(Player):
@staticmethod
def detect_executable() -> str:
pass
pass


# TODO: Dont use Popen, it will slow down
Expand Down
110 changes: 110 additions & 0 deletions test/img/RichPixels_ChafaSymbols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from abc import abstractmethod
from os import remove

from chafa import chafa
from chafa.loader import Loader
from httpx import AsyncClient
from rich.text import Text
from textual.app import RenderResult
from textual.widget import Widget
from rich_pixels import Pixels
from PIL import Image
from io import BytesIO
from tempfile import NamedTemporaryFile


class ImageWidget(Widget):
def __init__(self, width: int, height: int, *args, **kwargs):
super().__init__(*args, **kwargs)

self.width = width
self.styles.width = width
self.styles.max_width = width
self.styles.min_width = width

self.height = height
_h = int(height / 2)
self.styles.height = _h
self.styles.max_height = _h
self.styles.min_height = _h

self.render_result: RenderResult = ""

@abstractmethod
def update_from_url(self, url: str):
pass

@abstractmethod
def render(self):
return self.render_result


class ChafaSymbolsImage(ImageWidget):
# https://pypi.org/project/chafa.py/
# https://hpjansson.org/chafa/
# https://imagemagick.org/script/download.php


# TODO: Support different loaders
# https://chafapy.mage.black/usage/examples.html
# TODO: Support all PixelModes

async def update_from_url(self, url: str):
async with AsyncClient(verify=False) as client:
response = await client.get(url)
with NamedTemporaryFile(mode="wb", prefix="gucken-", delete=False) as tf:
tf.write(response.content)
tf.close()

config = chafa.CanvasConfig()

config.width = self.width
config.height = self.height

image = Loader(tf.name)

config.calc_canvas_geometry(
self.width,
self.height,
0.5
)

canvas = chafa.Canvas(config)

canvas.draw_all_pixels(
image.pixel_type,
image.get_pixels(),
image.width,
image.height,
image.rowstride
)

self.render_result = Text.from_ansi(canvas.print().decode(), no_wrap=True)
try:
remove(tf.name)
except FileNotFoundError:
pass
self.refresh()


class RichPixelsImage(ImageWidget):
# https://pypi.org/project/rich-pixels/

async def update_from_url(self, url: str):
async with AsyncClient(verify=False) as client:
response = await client.get(url)
img = Image.open(BytesIO(response.content))
img = img.resize((self.width, self.height))
self.render_result = Pixels.from_image(img)
self.refresh()

"""
yield ChafaSymbolsImage(25, 50)
yield RichPixelsImage(25, 50)
img = self.query_one(RichPixelsImage)
await img.update_from_url(series_search_result.cover)
img = self.query_one(ChafaSymbolsImage)
await img.update_from_url(series_search_result.cover)
"""
38 changes: 38 additions & 0 deletions test/img/iterm2_prot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# https://iterm2.com/documentation-images.html
from base64 import b64encode


def print_img(
fine_content: bytes,
width: int = None,
height: int = None,
preserve_aspect_ratio: int = None,
fine_name: str = None
):
s = ["\033]1337;File=inline=1"]

if width:
s.append(f";width={width}")
if height:
s.append(f";height={height}")
if preserve_aspect_ratio:
s.append(f";preserveAspectRatio={preserve_aspect_ratio}")
if fine_name:
s.append(f";name={b64encode(fine_name.encode()).decode()}")

s.append(f":{b64encode(fine_content).decode()}")
s.append("\007")
print("".join(s))


def main():
file_path = "<img_path>"

with open(file_path, "rb") as image_file:
fine_content = image_file.read()

print_img(fine_content, 50, 20)


if __name__ == "__main__":
main()
143 changes: 143 additions & 0 deletions test/img/kitty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# https://github.com/Textualize/rich/discussions/384#discussioncomment-9286378

import io
import sys
from base64 import b64encode
from pathlib import Path

from PIL import Image
from rich.segment import Segment
from textual.widget import Widget
from textual.app import App, ComposeResult
from textual.geometry import Size, NULL_SIZE
from textual.containers import Center, Middle
from rich.console import (
Console,
ConsoleOptions,
ConsoleRenderable,
RenderResult,
RichCast,
)
from rich.style import Style


PLACEHOLDER = 0x10EEEE

# fmt: off
NUMBER_TO_DIACRITIC = [
0x00305, 0x0030d, 0x0030e, 0x00310, 0x00312, 0x0033d, 0x0033e, 0x0033f, 0x00346, 0x0034a, 0x0034b, 0x0034c, 0x00350, 0x00351, 0x00352, 0x00357,
0x0035b, 0x00363, 0x00364, 0x00365, 0x00366, 0x00367, 0x00368, 0x00369, 0x0036a, 0x0036b, 0x0036c, 0x0036d, 0x0036e, 0x0036f, 0x00483, 0x00484,
0x00485, 0x00486, 0x00487, 0x00592, 0x00593, 0x00594, 0x00595, 0x00597, 0x00598, 0x00599, 0x0059c, 0x0059d, 0x0059e, 0x0059f, 0x005a0, 0x005a1,
0x005a8, 0x005a9, 0x005ab, 0x005ac, 0x005af, 0x005c4, 0x00610, 0x00611, 0x00612, 0x00613, 0x00614, 0x00615, 0x00616, 0x00617, 0x00657, 0x00658,
0x00659, 0x0065a, 0x0065b, 0x0065d, 0x0065e, 0x006d6, 0x006d7, 0x006d8, 0x006d9, 0x006da, 0x006db, 0x006dc, 0x006df, 0x006e0, 0x006e1, 0x006e2,
0x006e4, 0x006e7, 0x006e8, 0x006eb, 0x006ec, 0x00730, 0x00732, 0x00733, 0x00735, 0x00736, 0x0073a, 0x0073d, 0x0073f, 0x00740, 0x00741, 0x00743,
0x00745, 0x00747, 0x00749, 0x0074a, 0x007eb, 0x007ec, 0x007ed, 0x007ee, 0x007ef, 0x007f0, 0x007f1, 0x007f3, 0x00816, 0x00817, 0x00818, 0x00819,
0x0081b, 0x0081c, 0x0081d, 0x0081e, 0x0081f, 0x00820, 0x00821, 0x00822, 0x00823, 0x00825, 0x00826, 0x00827, 0x00829, 0x0082a, 0x0082b, 0x0082c,
0x0082d, 0x00951, 0x00953, 0x00954, 0x00f82, 0x00f83, 0x00f86, 0x00f87, 0x0135d, 0x0135e, 0x0135f, 0x017dd, 0x0193a, 0x01a17, 0x01a75, 0x01a76,
0x01a77, 0x01a78, 0x01a79, 0x01a7a, 0x01a7b, 0x01a7c, 0x01b6b, 0x01b6d, 0x01b6e, 0x01b6f, 0x01b70, 0x01b71, 0x01b72, 0x01b73, 0x01cd0, 0x01cd1,
0x01cd2, 0x01cda, 0x01cdb, 0x01ce0, 0x01dc0, 0x01dc1, 0x01dc3, 0x01dc4, 0x01dc5, 0x01dc6, 0x01dc7, 0x01dc8, 0x01dc9, 0x01dcb, 0x01dcc, 0x01dd1,
0x01dd2, 0x01dd3, 0x01dd4, 0x01dd5, 0x01dd6, 0x01dd7, 0x01dd8, 0x01dd9, 0x01dda, 0x01ddb, 0x01ddc, 0x01ddd, 0x01dde, 0x01ddf, 0x01de0, 0x01de1,
0x01de2, 0x01de3, 0x01de4, 0x01de5, 0x01de6, 0x01dfe, 0x020d0, 0x020d1, 0x020d4, 0x020d5, 0x020d6, 0x020d7, 0x020db, 0x020dc, 0x020e1, 0x020e7,
0x020e9, 0x020f0, 0x02cef, 0x02cf0, 0x02cf1, 0x02de0, 0x02de1, 0x02de2, 0x02de3, 0x02de4, 0x02de5, 0x02de6, 0x02de7, 0x02de8, 0x02de9, 0x02dea,
0x02deb, 0x02dec, 0x02ded, 0x02dee, 0x02def, 0x02df0, 0x02df1, 0x02df2, 0x02df3, 0x02df4, 0x02df5, 0x02df6, 0x02df7, 0x02df8, 0x02df9, 0x02dfa,
0x02dfb, 0x02dfc, 0x02dfd, 0x02dfe, 0x02dff, 0x0a66f, 0x0a67c, 0x0a67d, 0x0a6f0, 0x0a6f1, 0x0a8e0, 0x0a8e1, 0x0a8e2, 0x0a8e3, 0x0a8e4, 0x0a8e5,
0x0a8e6, 0x0a8e7, 0x0a8e8, 0x0a8e9, 0x0a8ea, 0x0a8eb, 0x0a8ec, 0x0a8ed, 0x0a8ee, 0x0a8ef, 0x0a8f0, 0x0a8f1, 0x0aab0, 0x0aab2, 0x0aab3, 0x0aab7,
0x0aab8, 0x0aabe, 0x0aabf, 0x0aac1, 0x0fe20, 0x0fe21, 0x0fe22, 0x0fe23, 0x0fe24, 0x0fe25, 0x0fe26, 0x10a0f, 0x10a38, 0x1d185, 0x1d186, 0x1d187,
0x1d188, 0x1d189, 0x1d1aa, 0x1d1ab, 0x1d1ac, 0x1d1ad, 0x1d242, 0x1d243, 0x1d244
]
# fmt: on


class KittyImage(Widget):
_next_image_id = 1

class _Renderable:
def __init__(self, image_id: int, size: Size) -> None:
self._image_id = image_id
self._size = size

def __rich_console__(
self, _console: Console, _options: ConsoleOptions
) -> RenderResult:
style = Style(color=f"rgb({(self._image_id >> 16) & 255}, {(self._image_id >> 8) & 255}, {self._image_id & 255})")
id_char = NUMBER_TO_DIACRITIC[(self._image_id >> 24) & 255]
for r in range(self._size.height):
line = ""
for c in range(self._size.width):
line += f"{chr(PLACEHOLDER)}{chr(NUMBER_TO_DIACRITIC[r])}{chr(NUMBER_TO_DIACRITIC[c])}{chr(id_char)}"
line += "\n"
yield Segment(line, style=style)

def __init__(
self,
image: Image,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
image_buffer = io.BytesIO()
image.save(image_buffer, format="png")
self._image_data = image_buffer.getvalue()

self._image_id = KittyImage._next_image_id
KittyImage._next_image_id += 1

self._placement_size = NULL_SIZE

self._send_image_to_terminal()

def _send_image_to_terminal(self) -> None:
data = b64encode(self._image_data)
while data:
chunk, data = data[:4096], data[4096:]

ans = [
f"\033_Gi={self._image_id},m={1 if data else 0},f=100,q=2".encode(
"ascii"
)
]
if chunk:
ans.append(b";")
ans.append(chunk)
ans.append(b"\033\\")

# Dangerous. Could interfer with the writer thread. But we can't use textual's functions
# to write to the terminal.
# It buffers output. There's no way around that (Driver.flush() is a no-op).
# This buffering re-chunks the data which leads to a failed transmission.
sys.__stdout__.buffer.write(b"".join(ans))
sys.__stdout__.buffer.flush()

def _create_virtual_placement(self, size: Size) -> None:
# Same issue as above, even though the size of the data probably would still work with the
# buffering. But we have this hack in place anyway, so it shouldn't matter anymore.
sys.__stdout__.buffer.write(
f"\033_Ga=p,U=1,i={self._image_id},c={size.width},r={size.height},q=2\033\\".encode(
"ascii"
)
)
sys.__stdout__.flush()

def render(self) -> ConsoleRenderable | RichCast:
if self._placement_size != self.content_size:
self._create_virtual_placement(self.content_size)
self._placement_size = self.content_size
return KittyImage._Renderable(self._image_id, self.content_size)


class ImageApp(App[None]):
def compose(self) -> ComposeResult:
with Center():
with Middle():
yield KittyImage(Image.open(Path("<img_path>")))

def on_mount(self) -> None:
self.query_one(KittyImage).styles.width = 20
self.query_one(KittyImage).styles.height = 15


if __name__ == "__main__":
ImageApp().run()
37 changes: 37 additions & 0 deletions test/img/sixels_prot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# pip install libsixel-python

from libsixel import SIXEL_OPTFLAG_OUTPUT, SIXEL_OPTFLAG_HEIGHT
from libsixel.encoder import Encoder, SIXEL_OPTFLAG_WIDTH
from tempfile import NamedTemporaryFile
from os import remove


def to_sic(
path: str,
width: int = None,
height: int = None
) -> str:
rf = NamedTemporaryFile(prefix="img-", suffix=".six", delete=False)

encoder = Encoder()
if width:
encoder.setopt(SIXEL_OPTFLAG_WIDTH, width)
if height:
encoder.setopt(SIXEL_OPTFLAG_HEIGHT, height)
encoder.setopt(SIXEL_OPTFLAG_OUTPUT, rf.name)
encoder.encode(path)

with open(rf.name, "r") as f:
sic = f.read()

remove(rf.name)
return sic


def main():
file_path = "<img_path>"
print(to_sic(file_path, 500, 500))


if __name__ == "__main__":
main()
Loading

0 comments on commit 3afe540

Please sign in to comment.