euporie 2.8.4__py3-none-any.whl → 2.8.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/_commands.py +143 -0
- euporie/console/_settings.py +58 -0
- euporie/console/app.py +25 -71
- euporie/console/tabs/console.py +58 -62
- euporie/core/__init__.py +1 -1
- euporie/core/__main__.py +28 -11
- euporie/core/_settings.py +109 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +95 -0
- euporie/core/app/_settings.py +457 -0
- euporie/core/{app.py → app/app.py} +212 -576
- euporie/core/app/base.py +51 -0
- euporie/core/{current.py → app/current.py} +13 -4
- euporie/core/app/cursor.py +35 -0
- euporie/core/app/dummy.py +12 -0
- euporie/core/app/launch.py +28 -0
- euporie/core/bars/__init__.py +11 -0
- euporie/core/bars/command.py +205 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +20 -16
- euporie/core/{widgets → bars}/status.py +6 -23
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +16 -7
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +10 -20
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +116 -53
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +9 -23
- euporie/core/convert/formats/common.py +11 -23
- euporie/core/convert/formats/html.py +45 -40
- euporie/core/convert/formats/pil.py +1 -1
- euporie/core/convert/formats/png.py +3 -5
- euporie/core/convert/formats/sixel.py +3 -3
- euporie/core/convert/registry.py +4 -6
- euporie/core/convert/utils.py +41 -4
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +98 -40
- euporie/core/format.py +2 -3
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +12 -21
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +386 -133
- euporie/core/history.py +2 -2
- euporie/core/inspection.py +3 -2
- euporie/core/io.py +207 -28
- euporie/core/kernel/__init__.py +1 -0
- euporie/core/{kernel.py → kernel/client.py} +45 -108
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +1 -8
- euporie/core/key_binding/bindings/basic.py +47 -7
- euporie/core/key_binding/bindings/completion.py +3 -8
- euporie/core/key_binding/bindings/micro.py +1 -6
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/key_binding/bindings/terminal.py +193 -0
- euporie/core/key_binding/key_processor.py +43 -2
- euporie/core/key_binding/registry.py +2 -0
- euporie/core/key_binding/utils.py +22 -2
- euporie/core/keys.py +7156 -93
- euporie/core/layout/cache.py +3 -3
- euporie/core/layout/containers.py +48 -4
- euporie/core/layout/decor.py +2 -2
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +2 -1
- euporie/core/layout/scroll.py +39 -34
- euporie/core/log.py +76 -64
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +1 -1
- euporie/core/path.py +62 -13
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +57 -39
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +32 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +80 -470
- euporie/core/tabs/kernel.py +419 -0
- euporie/core/tabs/notebook.py +24 -101
- euporie/core/utils.py +92 -15
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +19 -50
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +62 -27
- euporie/core/widgets/display.py +12 -15
- euporie/core/widgets/file_browser.py +2 -23
- euporie/core/widgets/forms.py +8 -5
- euporie/core/widgets/inputs.py +13 -70
- euporie/core/widgets/layout.py +2 -1
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +10 -8
- euporie/core/widgets/pager.py +6 -10
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -35
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +49 -171
- euporie/notebook/filters.py +1 -1
- euporie/notebook/tabs/__init__.py +46 -7
- euporie/notebook/tabs/_commands.py +714 -0
- euporie/notebook/tabs/_settings.py +32 -0
- euporie/notebook/tabs/display.py +4 -4
- euporie/notebook/tabs/edit.py +11 -44
- euporie/notebook/tabs/json.py +5 -5
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +11 -660
- euporie/notebook/widgets/_commands.py +11 -0
- euporie/notebook/widgets/_settings.py +19 -0
- euporie/notebook/widgets/side_bar.py +14 -34
- euporie/preview/_settings.py +104 -0
- euporie/preview/app.py +6 -31
- euporie/preview/tabs/notebook.py +6 -72
- euporie/web/__init__.py +1 -0
- euporie/web/tabs/__init__.py +14 -0
- euporie/web/tabs/web.py +11 -6
- euporie/web/widgets/__init__.py +1 -0
- euporie/web/widgets/webview.py +5 -15
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
- euporie-2.8.6.dist-info/RECORD +175 -0
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -64
- euporie/core/terminal.py +0 -522
- euporie-2.8.4.dist-info/RECORD +0 -147
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/convert/__init__.py
CHANGED
@@ -1,31 +1 @@
|
|
1
1
|
"""Sub-module concerned with the conversion of data formats."""
|
2
|
-
|
3
|
-
from euporie.core.convert.formats import (
|
4
|
-
ansi,
|
5
|
-
base64,
|
6
|
-
ft,
|
7
|
-
html,
|
8
|
-
jpeg,
|
9
|
-
markdown,
|
10
|
-
pdf,
|
11
|
-
pil,
|
12
|
-
png,
|
13
|
-
rich,
|
14
|
-
sixel,
|
15
|
-
svg,
|
16
|
-
)
|
17
|
-
|
18
|
-
__all__ = [
|
19
|
-
"ansi",
|
20
|
-
"base64",
|
21
|
-
"jpeg",
|
22
|
-
"html",
|
23
|
-
"markdown",
|
24
|
-
"ft",
|
25
|
-
"pdf",
|
26
|
-
"pil",
|
27
|
-
"png",
|
28
|
-
"rich",
|
29
|
-
"sixel",
|
30
|
-
"svg",
|
31
|
-
]
|
euporie/core/convert/datum.py
CHANGED
@@ -3,31 +3,31 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
import hashlib
|
7
6
|
import inspect
|
8
7
|
import io
|
9
8
|
import logging
|
10
9
|
import threading
|
10
|
+
from hashlib import md5
|
11
11
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
12
12
|
from weakref import ReferenceType, WeakValueDictionary, finalize, ref
|
13
13
|
|
14
|
-
import imagesize
|
15
|
-
from PIL.Image import Image as PilImage
|
16
14
|
from prompt_toolkit.data_structures import Size
|
17
15
|
from prompt_toolkit.layout.containers import WindowAlign
|
18
16
|
|
17
|
+
from euporie.core.app.current import get_app
|
19
18
|
from euporie.core.convert.registry import (
|
20
19
|
_CONVERTOR_ROUTE_CACHE,
|
21
20
|
_FILTER_CACHE,
|
22
21
|
converters,
|
23
22
|
)
|
24
|
-
from euporie.core.current import get_app
|
25
23
|
from euporie.core.ft.utils import to_plain_text
|
26
24
|
|
27
25
|
if TYPE_CHECKING:
|
26
|
+
from collections.abc import Coroutine
|
28
27
|
from pathlib import Path
|
29
|
-
from typing import Any, ClassVar
|
28
|
+
from typing import Any, ClassVar
|
30
29
|
|
30
|
+
from PIL.Image import Image as PilImage
|
31
31
|
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
32
32
|
from rich.console import ConsoleRenderable
|
33
33
|
|
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
|
35
35
|
from euporie.core.style import ColorPaletteColor
|
36
36
|
|
37
37
|
|
38
|
-
T = TypeVar("T", bytes, str, "StyleAndTextTuples", PilImage, "ConsoleRenderable")
|
38
|
+
T = TypeVar("T", bytes, str, "StyleAndTextTuples", "PilImage", "ConsoleRenderable")
|
39
39
|
|
40
40
|
|
41
41
|
log = logging.getLogger(__name__)
|
@@ -139,6 +139,10 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
139
139
|
self._conversions: dict[
|
140
140
|
tuple[str, int | None, int | None, str | None, str | None, bool], T | None
|
141
141
|
] = {}
|
142
|
+
self._queue: dict[
|
143
|
+
tuple[str, int | None, int | None, str | None, str | None, bool],
|
144
|
+
asyncio.Event,
|
145
|
+
] = {}
|
142
146
|
self._finalizer = finalize(self, self._cleanup_datum_sizes, self.hash)
|
143
147
|
self._finalizer.atexit = False
|
144
148
|
|
@@ -166,23 +170,31 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
166
170
|
return data.encode()
|
167
171
|
elif isinstance(data, list):
|
168
172
|
return to_plain_text(data).encode()
|
169
|
-
elif isinstance(data, PilImage):
|
170
|
-
return data.tobytes()
|
171
173
|
elif isinstance(data, bytes):
|
172
174
|
return data
|
173
175
|
else:
|
174
|
-
|
176
|
+
from PIL.Image import Image as PilImage
|
177
|
+
|
178
|
+
if isinstance(data, PilImage):
|
179
|
+
return data.tobytes()
|
180
|
+
else:
|
181
|
+
return b"Error"
|
175
182
|
|
176
183
|
@staticmethod
|
177
184
|
def get_hash(data: Any) -> str:
|
178
185
|
"""Calculate a hash of data."""
|
179
|
-
if isinstance(data,
|
186
|
+
if isinstance(data, bytes):
|
187
|
+
hash_data = data
|
188
|
+
elif isinstance(data, str):
|
180
189
|
hash_data = data.encode()
|
181
|
-
elif isinstance(data, PilImage):
|
182
|
-
hash_data = data.tobytes()
|
183
190
|
else:
|
184
|
-
|
185
|
-
|
191
|
+
from PIL.Image import Image as PilImage
|
192
|
+
|
193
|
+
if isinstance(data, PilImage):
|
194
|
+
hash_data = data.tobytes()
|
195
|
+
else:
|
196
|
+
hash_data = data
|
197
|
+
return md5(hash_data, usedforsecurity=False).hexdigest()
|
186
198
|
|
187
199
|
@property
|
188
200
|
def hash(self) -> str:
|
@@ -229,20 +241,30 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
229
241
|
if not bg and hasattr(app := get_app(), "color_palette"):
|
230
242
|
bg = self.bg or app.color_palette.bg.base_hex
|
231
243
|
|
232
|
-
if (
|
233
|
-
|
244
|
+
if (key_conv := (to, cols, rows, fg, bg, extend)) in self._queue:
|
245
|
+
await self._queue[key_conv].wait()
|
246
|
+
if key_conv in self._conversions:
|
247
|
+
return self._conversions[key_conv]
|
248
|
+
|
249
|
+
self._queue[key_conv] = event = asyncio.Event()
|
234
250
|
|
235
251
|
routes = _CONVERTOR_ROUTE_CACHE[(self.format, to)]
|
236
|
-
|
252
|
+
log.debug(
|
253
|
+
"Converting %s->'%s'@%s using routes: %s",
|
254
|
+
self,
|
255
|
+
to,
|
256
|
+
(cols, rows),
|
257
|
+
routes,
|
258
|
+
)
|
237
259
|
output: T | None = None
|
238
260
|
if routes:
|
239
261
|
datum = self
|
240
262
|
output = None
|
241
263
|
for route in routes:
|
242
264
|
for stage_a, stage_b in zip(route, route[1:]):
|
243
|
-
|
244
|
-
if
|
245
|
-
output = self._conversions[
|
265
|
+
key_stage = (stage_b, cols, rows, fg, bg, extend)
|
266
|
+
if key_stage in self._conversions:
|
267
|
+
output = self._conversions[key_stage]
|
246
268
|
else:
|
247
269
|
# Find converter with lowest weight
|
248
270
|
for converter in sorted(
|
@@ -257,14 +279,18 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
257
279
|
output = await converter.func(
|
258
280
|
datum, cols, rows, fg, bg, extend
|
259
281
|
)
|
260
|
-
self._conversions[
|
282
|
+
self._conversions[key_stage] = output
|
261
283
|
except Exception:
|
262
|
-
log.debug(
|
284
|
+
log.debug(
|
285
|
+
"Conversion step %s failed",
|
286
|
+
converter,
|
287
|
+
exc_info=True,
|
288
|
+
)
|
263
289
|
continue
|
264
290
|
else:
|
265
291
|
break
|
266
292
|
else:
|
267
|
-
log.
|
293
|
+
log.warning("An error occurred during format conversion")
|
268
294
|
output = None
|
269
295
|
if output is None:
|
270
296
|
log.error(
|
@@ -298,6 +324,9 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
298
324
|
if output is None:
|
299
325
|
output = ERROR_OUTPUTS.get(to, "(Conversion Error)")
|
300
326
|
|
327
|
+
event.set()
|
328
|
+
del self._queue[key_conv]
|
329
|
+
|
301
330
|
return output
|
302
331
|
|
303
332
|
def _to_sync(self, coro: Coroutine) -> Any:
|
@@ -331,36 +360,68 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
331
360
|
try:
|
332
361
|
return self._pixel_size
|
333
362
|
except AttributeError:
|
334
|
-
|
363
|
+
pass
|
364
|
+
|
365
|
+
px, py = self.px, self.py
|
366
|
+
self_data = self.data
|
367
|
+
format = self.format
|
368
|
+
data: bytes
|
369
|
+
|
370
|
+
while px is None or py is None:
|
335
371
|
# Do not bother trying if the format is ANSI
|
336
|
-
if
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
372
|
+
if format == "ansi":
|
373
|
+
break
|
374
|
+
|
375
|
+
from PIL.Image import Image as PilImage
|
376
|
+
|
377
|
+
if isinstance(self_data, PilImage):
|
378
|
+
px, py = self_data.size
|
379
|
+
break
|
380
|
+
|
381
|
+
# Decode base64 data
|
382
|
+
if format.startswith("base64-"):
|
383
|
+
data = await self.convert_async(to=format[7:])
|
384
|
+
|
385
|
+
# Encode string data
|
386
|
+
if isinstance(self_data, str):
|
387
|
+
data = self_data.encode()
|
388
|
+
|
389
|
+
if isinstance(self_data, bytes):
|
390
|
+
data = self_data
|
391
|
+
|
392
|
+
# Try using imagesize to get the size of the output
|
393
|
+
try:
|
394
|
+
import imagesize
|
395
|
+
|
396
|
+
px_calc, py_calc = imagesize.get(io.BytesIO(data))
|
397
|
+
except ValueError:
|
398
|
+
px_calc = py_calc = -1
|
399
|
+
|
400
|
+
if (
|
401
|
+
format != "png"
|
402
|
+
and px_calc <= 0
|
403
|
+
and py_calc <= 0
|
404
|
+
and _CONVERTOR_ROUTE_CACHE[(format, "png")]
|
405
|
+
):
|
406
|
+
# Try converting to PNG on failure
|
407
|
+
self_data = await self.convert_async(to="png")
|
408
|
+
format = "png"
|
409
|
+
continue
|
410
|
+
|
411
|
+
if px is None and px_calc > 0:
|
412
|
+
if py is not None and py_calc > 0:
|
413
|
+
px = int(px_calc * py / py_calc)
|
343
414
|
else:
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
px_calc, py_calc = imagesize.get(io.BytesIO(data))
|
349
|
-
except ValueError:
|
350
|
-
pass
|
415
|
+
px = px_calc
|
416
|
+
if py is None and py_calc > 0:
|
417
|
+
if px is not None and px_calc > 0:
|
418
|
+
py = int(py_calc * px / px_calc)
|
351
419
|
else:
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
if py is None and py_calc > 0:
|
358
|
-
if px is not None and px_calc > 0:
|
359
|
-
py = py_calc * px / px_calc
|
360
|
-
else:
|
361
|
-
py = py_calc
|
362
|
-
self._pixel_size = (px, py)
|
363
|
-
return self._pixel_size
|
420
|
+
py = py_calc
|
421
|
+
break
|
422
|
+
|
423
|
+
self._pixel_size = (px, py)
|
424
|
+
return self._pixel_size
|
364
425
|
|
365
426
|
def pixel_size(self) -> Any:
|
366
427
|
"""Get data dimensions synchronously."""
|
@@ -378,8 +439,8 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
378
439
|
px, py = await self.pixel_size_async()
|
379
440
|
if px is not None and py is not None:
|
380
441
|
app = get_app()
|
381
|
-
if hasattr(app, "
|
382
|
-
cell_px, cell_py = app.
|
442
|
+
if hasattr(app, "cell_size_px"):
|
443
|
+
cell_px, cell_py = app.cell_size_px
|
383
444
|
else:
|
384
445
|
cell_px, cell_py = 10, 20
|
385
446
|
cols = max(1, int(px // cell_px))
|
@@ -389,7 +450,9 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
389
450
|
|
390
451
|
def cell_size(self) -> Any:
|
391
452
|
"""Get cell width and aspect synchronously."""
|
392
|
-
|
453
|
+
if self._cell_size is None:
|
454
|
+
return self._to_sync(self.cell_size_async())
|
455
|
+
return self._cell_size
|
393
456
|
|
394
457
|
# def crop(self, bbox: DiInt) -> T:
|
395
458
|
# """Crop displayable data."""
|
@@ -3,4 +3,35 @@
|
|
3
3
|
They are grouped into sub-modules based on output format.
|
4
4
|
"""
|
5
5
|
|
6
|
+
from . import (
|
7
|
+
ansi,
|
8
|
+
base64,
|
9
|
+
ft,
|
10
|
+
html,
|
11
|
+
jpeg,
|
12
|
+
markdown,
|
13
|
+
pdf,
|
14
|
+
pil,
|
15
|
+
png,
|
16
|
+
rich,
|
17
|
+
sixel,
|
18
|
+
svg,
|
19
|
+
)
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
"BASE64_FORMATS",
|
23
|
+
"ansi",
|
24
|
+
"base64",
|
25
|
+
"ft",
|
26
|
+
"html",
|
27
|
+
"jpeg",
|
28
|
+
"markdown",
|
29
|
+
"pdf",
|
30
|
+
"pil",
|
31
|
+
"png",
|
32
|
+
"rich",
|
33
|
+
"sixel",
|
34
|
+
"svg",
|
35
|
+
]
|
36
|
+
|
6
37
|
BASE64_FORMATS = {"png", "jpeg", "pdf", "gif"}
|
@@ -4,14 +4,13 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
from functools import partial
|
7
|
-
from math import ceil
|
8
7
|
from typing import TYPE_CHECKING
|
9
8
|
|
9
|
+
from euporie.core.app.current import get_app
|
10
10
|
from euporie.core.convert.formats.common import chafa_convert_cmd, chafa_convert_py
|
11
11
|
from euporie.core.convert.formats.pil import set_background
|
12
12
|
from euporie.core.convert.registry import register
|
13
|
-
from euporie.core.convert.utils import call_subproc
|
14
|
-
from euporie.core.current import get_app
|
13
|
+
from euporie.core.convert.utils import call_subproc, scale_to_fit
|
15
14
|
from euporie.core.filters import command_exists, have_modules
|
16
15
|
|
17
16
|
if TYPE_CHECKING:
|
@@ -274,27 +273,14 @@ async def pil_to_ansi_py_timg(
|
|
274
273
|
"""Convert a PIL image to ANSI text using :py:mod:`timg`."""
|
275
274
|
import timg
|
276
275
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
# Calculate rows based on image aspect ratio
|
281
|
-
w, h = data.size
|
282
|
-
if rows is None and cols is not None:
|
283
|
-
w, h = data.size
|
284
|
-
rows = ceil(cols / w * h)
|
285
|
-
elif cols is None and rows is not None:
|
286
|
-
w, h = data.size
|
287
|
-
cols = ceil(rows / h * w)
|
288
|
-
elif rows is None and cols is None:
|
289
|
-
cols = ceil(w / px)
|
290
|
-
rows = ceil(h / py)
|
291
|
-
assert rows is not None
|
292
|
-
assert cols is not None
|
293
|
-
|
276
|
+
# Scale image to fit available space
|
277
|
+
cols, rows = await scale_to_fit(datum, cols, rows)
|
294
278
|
# `timg` assumes a 2x1 terminal cell aspect ratio, so we correct for this while
|
295
|
-
|
296
|
-
|
297
|
-
|
279
|
+
px, py = get_app().cell_size_px
|
280
|
+
rows = int(rows * 2 * (px / py) / 0.5)
|
281
|
+
# Resize the image
|
282
|
+
data = datum.data.resize((cols, rows))
|
283
|
+
# Set background if necessary
|
298
284
|
if bg:
|
299
285
|
data = set_background(data, bg)
|
300
286
|
return timg.Ansi24HblockMethod(data).to_string()
|
@@ -6,8 +6,8 @@ import base64
|
|
6
6
|
import logging
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
|
-
from euporie.core.
|
10
|
-
from euporie.core.
|
9
|
+
from euporie.core.app.current import get_app
|
10
|
+
from euporie.core.convert.utils import call_subproc, scale_to_fit
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
13
|
from typing import Any, Literal
|
@@ -41,14 +41,15 @@ async def imagemagick_convert(
|
|
41
41
|
extend: bool = True,
|
42
42
|
) -> str | bytes:
|
43
43
|
"""Convert image data to PNG bytes using ``imagemagick``."""
|
44
|
-
cmd: list[Any] = ["
|
44
|
+
cmd: list[Any] = ["magick"]
|
45
|
+
if bg:
|
46
|
+
cmd += ["-background", bg]
|
47
|
+
cmd.extend(["-[0]", "-density", "300"])
|
45
48
|
app = get_app()
|
46
|
-
if cols is not None and hasattr(app, "
|
47
|
-
px, _ = app.
|
49
|
+
if cols is not None and hasattr(app, "cell_size_px"):
|
50
|
+
px, _ = app.cell_size_px
|
48
51
|
cmd += ["-geometry", f"{int(cols * px)}"]
|
49
|
-
|
50
|
-
cmd += ["-background", bg, "-flatten"]
|
51
|
-
cmd += ["-[0]", f"{output_format}:-"]
|
52
|
+
cmd += [f"{output_format}:-"]
|
52
53
|
result: bytes | str = await call_subproc(datum.data, cmd)
|
53
54
|
|
54
55
|
if output_format in {"sixel", "svg"} and isinstance(result, bytes):
|
@@ -90,7 +91,7 @@ async def chafa_convert_py(
|
|
90
91
|
extend: bool = True,
|
91
92
|
) -> str | bytes:
|
92
93
|
"""Convert image data to ANSI text using ::`chafa.py`."""
|
93
|
-
from chafa
|
94
|
+
from chafa import Canvas, CanvasConfig, PixelMode, PixelType
|
94
95
|
from PIL import Image
|
95
96
|
|
96
97
|
pil_mode_to_pixel_type = {
|
@@ -111,25 +112,12 @@ async def chafa_convert_py(
|
|
111
112
|
# Always convert the image, as unconverted images sometime result in an off-by-one
|
112
113
|
# line width errors resulting in diagonal image striping for some reason
|
113
114
|
data = data.convert("RGBA", palette=Image.Palette.ADAPTIVE, colors=16)
|
114
|
-
|
115
115
|
# Init canvas config
|
116
116
|
config = CanvasConfig()
|
117
117
|
# Set output mode
|
118
118
|
config.pixel_mode = str_to_pixel_mode[output_format]
|
119
119
|
# Configure the canvas geometry based on our cell size
|
120
|
-
|
121
|
-
px, py = app.term_info.cell_size_px
|
122
|
-
else:
|
123
|
-
px, py = 10, 20
|
124
|
-
config.cell_width, config.cell_height = px, py
|
125
|
-
# Set canvas height and width
|
126
|
-
if cols:
|
127
|
-
config.width = cols
|
128
|
-
if rows:
|
129
|
-
config.height = max(1, rows)
|
130
|
-
# If we don't have specified, use the image's aspect
|
131
|
-
else:
|
132
|
-
config.height = max(1, int(cols / data.size[0] * data.size[1] * px / py))
|
120
|
+
config.width, config.height = await scale_to_fit(datum, cols, rows)
|
133
121
|
|
134
122
|
# Set the foreground color
|
135
123
|
if fg and (color := fg.lstrip("#")):
|
@@ -3,61 +3,66 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import logging
|
6
|
+
from functools import cache
|
6
7
|
from typing import TYPE_CHECKING
|
7
8
|
|
8
|
-
from
|
9
|
-
from mdit_py_plugins.amsmath import amsmath_plugin
|
10
|
-
from mdit_py_plugins.dollarmath.index import dollarmath_plugin
|
11
|
-
from mdit_py_plugins.texmath.index import texmath_plugin
|
12
|
-
from pygments import highlight
|
13
|
-
from pygments.formatters import HtmlFormatter
|
14
|
-
|
9
|
+
from euporie.core.app.current import get_app
|
15
10
|
from euporie.core.convert.registry import register
|
16
|
-
from euporie.core.current import get_app
|
17
11
|
from euporie.core.lexers import detect_lexer
|
18
12
|
|
19
13
|
if TYPE_CHECKING:
|
14
|
+
from markdown_it import MarkdownIt
|
15
|
+
|
20
16
|
from euporie.core.convert.datum import Datum
|
21
17
|
|
22
18
|
log = logging.getLogger(__name__)
|
23
19
|
|
24
20
|
|
25
|
-
|
26
|
-
|
21
|
+
@cache
|
22
|
+
def markdown_parser() -> MarkdownIt:
|
23
|
+
"""Lazy-load a markdown parser."""
|
24
|
+
from markdown_it import MarkdownIt
|
25
|
+
from mdit_py_plugins.amsmath import amsmath_plugin
|
26
|
+
from mdit_py_plugins.dollarmath.index import dollarmath_plugin
|
27
|
+
from mdit_py_plugins.texmath.index import texmath_plugin
|
28
|
+
from pygments import highlight
|
29
|
+
from pygments.formatters import HtmlFormatter
|
27
30
|
|
28
|
-
|
29
|
-
"""
|
30
|
-
return True
|
31
|
+
class MarkdownParser(MarkdownIt):
|
32
|
+
"""Subclas the markdown parser to allow ``file:`` URIs."""
|
31
33
|
|
34
|
+
def validateLink(self, url: str) -> bool:
|
35
|
+
"""Allow all link URIs."""
|
36
|
+
return True
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
38
|
+
return (
|
39
|
+
(
|
40
|
+
MarkdownParser(
|
41
|
+
options_update={
|
42
|
+
"highlight": lambda text, language, lang_args: highlight(
|
43
|
+
text,
|
44
|
+
detect_lexer(text, language=language),
|
45
|
+
HtmlFormatter(
|
46
|
+
nowrap=True,
|
47
|
+
noclasses=True,
|
48
|
+
style=(
|
49
|
+
app.syntax_theme
|
50
|
+
if hasattr((app := get_app()), "syntax_theme")
|
51
|
+
else "default"
|
52
|
+
),
|
47
53
|
),
|
48
|
-
)
|
49
|
-
|
50
|
-
|
54
|
+
)
|
55
|
+
}
|
56
|
+
)
|
57
|
+
.enable("linkify")
|
58
|
+
.enable("table")
|
59
|
+
.enable("strikethrough")
|
51
60
|
)
|
52
|
-
.
|
53
|
-
.
|
54
|
-
.
|
61
|
+
.use(texmath_plugin)
|
62
|
+
.use(dollarmath_plugin)
|
63
|
+
.use(amsmath_plugin)
|
64
|
+
# .use(tasklists_plugin)
|
55
65
|
)
|
56
|
-
.use(texmath_plugin)
|
57
|
-
.use(dollarmath_plugin)
|
58
|
-
.use(amsmath_plugin)
|
59
|
-
# .use(tasklists_plugin)
|
60
|
-
)
|
61
66
|
|
62
67
|
|
63
68
|
@register(from_="markdown", to="html")
|
@@ -70,7 +75,7 @@ async def markdown_to_html_markdown_it(
|
|
70
75
|
extend: bool = True,
|
71
76
|
) -> str:
|
72
77
|
"""Convert markdown to HTML using :py:mod:`markdownit_py`."""
|
73
|
-
|
78
|
+
parser = markdown_parser()
|
74
79
|
data = datum.data
|
75
80
|
markup = data.decode() if isinstance(data, bytes) else data
|
76
|
-
return
|
81
|
+
return parser.render(markup)
|
@@ -72,7 +72,7 @@ def crop(data: PilImage, bbox: DiInt) -> PilImage:
|
|
72
72
|
rows=full_height,
|
73
73
|
)
|
74
74
|
if image is not None:
|
75
|
-
cell_size_x, cell_size_y = self.app.
|
75
|
+
cell_size_x, cell_size_y = self.app.cell_size_px
|
76
76
|
# Downscale image to fit target region for precise cropping
|
77
77
|
image.thumbnail((full_width * cell_size_x, full_height * cell_size_y))
|
78
78
|
image = image.crop(
|
@@ -13,7 +13,6 @@ from euporie.core.filters import command_exists, have_modules
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from euporie.core.convert.datum import Datum
|
15
15
|
|
16
|
-
|
17
16
|
register(
|
18
17
|
from_="base64-png",
|
19
18
|
to="png",
|
@@ -126,9 +125,8 @@ async def latex_to_png_py_mpl(
|
|
126
125
|
from matplotlib.backends import backend_agg
|
127
126
|
|
128
127
|
# mpl mathtext doesn't support display math, force inline
|
129
|
-
data = datum.data.
|
130
|
-
|
131
|
-
data = f"${data}$"
|
128
|
+
data = datum.data.replace("$", "").strip()
|
129
|
+
data = f"${data}$"
|
132
130
|
buffer = BytesIO()
|
133
131
|
prop = font_manager.FontProperties(size=12)
|
134
132
|
parser = mathtext.MathTextParser("path")
|
@@ -143,7 +141,7 @@ async def latex_to_png_py_mpl(
|
|
143
141
|
register(
|
144
142
|
from_=("svg", "jpeg", "pdf", "gif"),
|
145
143
|
to="png",
|
146
|
-
filter_=command_exists("
|
144
|
+
filter_=command_exists("magick"),
|
147
145
|
)(partial(imagemagick_convert, "PNG"))
|
148
146
|
|
149
147
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from functools import partial
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
+
from euporie.core.app.current import get_app
|
8
9
|
from euporie.core.convert.formats.common import (
|
9
10
|
chafa_convert_cmd,
|
10
11
|
chafa_convert_py,
|
@@ -12,7 +13,6 @@ from euporie.core.convert.formats.common import (
|
|
12
13
|
)
|
13
14
|
from euporie.core.convert.registry import register
|
14
15
|
from euporie.core.convert.utils import call_subproc
|
15
|
-
from euporie.core.current import get_app
|
16
16
|
from euporie.core.filters import command_exists, have_modules
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
@@ -53,7 +53,7 @@ async def png_to_sixel_img2sixel(
|
|
53
53
|
if bg:
|
54
54
|
cmd += [f"--bgcolor={bg}"]
|
55
55
|
if cols is not None:
|
56
|
-
px, _ = get_app().
|
56
|
+
px, _ = get_app().cell_size_px
|
57
57
|
cmd += [f"--width={int(cols * px)}"]
|
58
58
|
return (await call_subproc(datum.data, cmd)).decode()
|
59
59
|
|
@@ -61,7 +61,7 @@ async def png_to_sixel_img2sixel(
|
|
61
61
|
register(
|
62
62
|
from_=("png", "jpeg", "svg", "pdf"),
|
63
63
|
to="sixel",
|
64
|
-
filter_=command_exists("
|
64
|
+
filter_=command_exists("magick"),
|
65
65
|
)(partial(imagemagick_convert, "sixel"))
|
66
66
|
|
67
67
|
|