rmink 2.2.0__tar.gz → 2.2.1__tar.gz
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.
- {rmink-2.2.0 → rmink-2.2.1}/CHANGELOG.md +4 -0
- {rmink-2.2.0 → rmink-2.2.1}/PKG-INFO +1 -1
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/requirements.txt +1 -1
- {rmink-2.2.0 → rmink-2.2.1}/pyproject.toml +1 -1
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/api.py +21 -8
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/extract/raster.py +3 -2
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/render/preview.py +6 -1
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/web/static/index.html +7 -2
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/writer/rm_v6.py +9 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_shading.py +13 -2
- {rmink-2.2.0 → rmink-2.2.1}/.dockerignore +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/.github/FUNDING.yml +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/.github/workflows/ci.yml +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/.github/workflows/release.yml +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/.gitignore +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/Dockerfile +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/LICENSE +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/README.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/.python-version +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/app.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/bin/rmapi +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/fetch-rmapi.sh +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/og.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/promo/og-en-7446af05.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/promo/og-sv-92100a5e.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/promo/poster-en.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/promo/poster-sv.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/promo/rmink-promo-en.mp4 +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/promo/rmink-promo-sv.mp4 +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/video-sv.html +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/public/video.html +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/deploy/vercel/vercel.json +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/docs/device-acceptance.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/docs/hosting.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/docs/releasing.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/examples/document.pdf +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/examples/handwriting.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/examples/logo.svg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/examples/make_examples.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/.gitignore +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/README.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/build_data.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/capture_desktop.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/i18n.json +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/make_face.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/make_note.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/make_shade.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/make_share.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/post-texts.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/render.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/promo/timeline.html +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/skills/rmink/SKILL.md +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/cli.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/color/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/color/palette.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/devices.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/extract/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/extract/centerline.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/extract/shade.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/extract/tone.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/extract/vector.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/geometry/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/geometry/hatch.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/geometry/order.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/geometry/simplify.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/geometry/transform.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/ingest/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/ingest/image.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/ingest/pdf.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/ingest/svg.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/mcp_server.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/model.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/options.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/package/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/package/rmdoc.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/render/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/support.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/web/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/web/app.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/web/cloud.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/web/static/og.jpg +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/web/static/thanks.html +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/src/rmink/writer/__init__.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/conftest.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_centerline.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_cloud.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_fidelity.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_geometry.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_interfaces.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_web.py +0 -0
- {rmink-2.2.0 → rmink-2.2.1}/tests/test_writer.py +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.1 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- Web UI: the shading previews are about twice as fast, so they also appear for camera photos in Safari on iPhone. If they still fail, the strip says why instead of disappearing.
|
|
6
|
+
|
|
3
7
|
## 2.2.0 — 2026-10-24
|
|
4
8
|
|
|
5
9
|
- Web UI: "Send to reMarkable" uploads the notebook straight to the reMarkable Cloud, so it syncs to the tablet without USB. It uses [rmapi](https://github.com/ddvk/rmapi) (set `RMINK_RMAPI` or have `rmapi` on the PATH; the button is hidden otherwise). The first time it asks for a one-time code from my.remarkable.com; the connection is kept only in the browser. Notebooks land in *My files*, and a taken name gets `_2`, `_3` and so on.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rmink
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Convert images, photos, SVGs and PDFs into native, editable reMarkable ink (.rm v6 / .rmdoc)
|
|
5
5
|
Project-URL: Homepage, https://github.com/thagvall/rmink
|
|
6
6
|
Project-URL: Source, https://github.com/thagvall/rmink
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rmink"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.1"
|
|
8
8
|
description = "Convert images, photos, SVGs and PDFs into native, editable reMarkable ink (.rm v6 / .rmdoc)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -21,11 +21,14 @@ from rmink.ingest import RasterSource, Source, VectorSource, load
|
|
|
21
21
|
from rmink.model import Document, Page, Stroke
|
|
22
22
|
from rmink.options import ConvertOptions, ConvertResult, FillMode, Mode, PageStats, Shading
|
|
23
23
|
from rmink.package.rmdoc import RmdocPage, UuidFactory, write_rmdoc
|
|
24
|
-
from rmink.render.preview import contact_sheet, render_rm, thumbnail_png
|
|
25
|
-
from rmink.writer.rm_v6 import write_page
|
|
24
|
+
from rmink.render.preview import contact_sheet, render_lines, render_rm, thumbnail_png
|
|
25
|
+
from rmink.writer.rm_v6 import page_lines, write_page
|
|
26
26
|
|
|
27
27
|
log = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
+
# Style thumbnails extract lines at device resolution instead of 2x.
|
|
30
|
+
_STYLE_OVERSAMPLE = 1.0
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
@dataclass
|
|
31
34
|
class Rendered:
|
|
@@ -85,7 +88,11 @@ def _raster_placement(src: RasterSource, device: DeviceProfile, opts: ConvertOpt
|
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
def _raster_lines(
|
|
88
|
-
src: RasterSource,
|
|
91
|
+
src: RasterSource,
|
|
92
|
+
placement: Placement,
|
|
93
|
+
device: DeviceProfile,
|
|
94
|
+
opts: ConvertOptions,
|
|
95
|
+
oversample: float | None = None,
|
|
89
96
|
) -> list[Stroke]:
|
|
90
97
|
palette, clusters = _palette(device, opts.colors)
|
|
91
98
|
# Photographed black ink reads as grey; gray is only offered when it is
|
|
@@ -94,7 +101,8 @@ def _raster_lines(
|
|
|
94
101
|
if opts.shading != Shading.none and opts.fill == FillMode.hatch:
|
|
95
102
|
# Shading draws the dark areas; even hatching would hide it.
|
|
96
103
|
opts = opts.model_copy(update={"fill": FillMode.outline})
|
|
97
|
-
|
|
104
|
+
extra = {} if oversample is None else {"oversample": oversample}
|
|
105
|
+
return extract_raster(src, placement, device, raster_palette, clusters, opts, **extra)
|
|
98
106
|
|
|
99
107
|
|
|
100
108
|
def source_to_page(src: Source, device: DeviceProfile, opts: ConvertOptions) -> Page:
|
|
@@ -141,14 +149,17 @@ def style_previews(
|
|
|
141
149
|
return []
|
|
142
150
|
styles = styles or list(Shading)
|
|
143
151
|
placement = _raster_placement(src, device, opts)
|
|
144
|
-
|
|
152
|
+
# Lines at device resolution instead of 2x: half the time, and the
|
|
153
|
+
# thumbnails are far too small to show the difference.
|
|
154
|
+
plain = _raster_lines(
|
|
155
|
+
src, placement, device, opts.model_copy(update={"shading": Shading.none}), _STYLE_OVERSAMPLE
|
|
156
|
+
)
|
|
145
157
|
if opts.fill == FillMode.hatch:
|
|
146
158
|
shaded = opts.model_copy(update={"shading": Shading.stipple})
|
|
147
|
-
lines = _raster_lines(src, placement, device, shaded)
|
|
159
|
+
lines = _raster_lines(src, placement, device, shaded, _STYLE_OVERSAMPLE)
|
|
148
160
|
else:
|
|
149
161
|
lines = plain
|
|
150
162
|
tone = device_tone(src, placement, opts.shade_contrast)
|
|
151
|
-
author = UuidFactory(0)()
|
|
152
163
|
out = []
|
|
153
164
|
for style in styles:
|
|
154
165
|
style_opts = opts.model_copy(update={"shading": style})
|
|
@@ -159,7 +170,9 @@ def style_previews(
|
|
|
159
170
|
)
|
|
160
171
|
strokes = enforce_budget(reading_order(strokes), opts.max_points, opts.simplify)
|
|
161
172
|
page = Page(device.width, placement.page_height, strokes)
|
|
162
|
-
|
|
173
|
+
# Straight from the lines: writing and re-reading the .rm file took two
|
|
174
|
+
# thirds of the time, and the main preview already round-trips the writer.
|
|
175
|
+
image = render_lines(page_lines(page, device), device, page.height)
|
|
163
176
|
out.append(StylePreview(style, image, len(page.strokes), page.point_count))
|
|
164
177
|
return out
|
|
165
178
|
|
|
@@ -139,10 +139,11 @@ def extract_raster(
|
|
|
139
139
|
palette: tuple[PaletteEntry, ...],
|
|
140
140
|
clusters: int | None,
|
|
141
141
|
opts: ConvertOptions,
|
|
142
|
+
oversample: float = _OVERSAMPLE,
|
|
142
143
|
) -> list[Stroke]:
|
|
143
144
|
rgb = src.image
|
|
144
|
-
# Resample so one mask pixel is 1/
|
|
145
|
-
factor = min(placement.scale *
|
|
145
|
+
# Resample so one mask pixel is 1/oversample device pixel.
|
|
146
|
+
factor = min(placement.scale * oversample, _MAX_UPSAMPLE)
|
|
146
147
|
if abs(factor - 1) > 0.05:
|
|
147
148
|
h, w = rgb.shape[:2]
|
|
148
149
|
size = (max(1, round(w * factor)), max(1, round(h * factor)))
|
|
@@ -39,7 +39,12 @@ def _rgb(line: si.Line) -> tuple[int, int, int]:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def render_rm(rm_bytes: bytes, device: DeviceProfile, height: int | None = None) -> Image.Image:
|
|
42
|
-
|
|
42
|
+
return render_lines(read_lines(rm_bytes), device, height)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_lines(
|
|
46
|
+
lines: list[si.Line], device: DeviceProfile, height: int | None = None
|
|
47
|
+
) -> Image.Image:
|
|
43
48
|
s = device.ink_scale
|
|
44
49
|
max_y = max((p.y for ln in lines for p in ln.points), default=0) / s
|
|
45
50
|
h = max(height or device.height, int(max_y) + 1)
|
|
@@ -637,8 +637,13 @@ dialog img { display: block; max-width: 96vw; max-height: 92vh; background: #fff
|
|
|
637
637
|
}));
|
|
638
638
|
markStyles();
|
|
639
639
|
} catch (e) {
|
|
640
|
-
if (e.name === "AbortError") return;
|
|
641
|
-
|
|
640
|
+
if (e.name === "AbortError" || seq !== state.styleSeq) return;
|
|
641
|
+
// Say why instead of hiding the strip; phones are slow enough to hit timeouts.
|
|
642
|
+
box.hidden = false;
|
|
643
|
+
$("#style-cards").replaceChildren(Object.assign(document.createElement("p"), {
|
|
644
|
+
className: "notice err", style: "grid-column: 1 / -1",
|
|
645
|
+
textContent: `Could not make the shading previews: ${e.message}`,
|
|
646
|
+
}));
|
|
642
647
|
}
|
|
643
648
|
}
|
|
644
649
|
|
|
@@ -122,6 +122,15 @@ def page_blocks(page: Page, device: DeviceProfile, author_uuid: UUID) -> Iterato
|
|
|
122
122
|
counter += 1
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def page_lines(page: Page, device: DeviceProfile) -> list[si.Line]:
|
|
126
|
+
"""The lines ``write_page`` would store, without serialising them."""
|
|
127
|
+
return [
|
|
128
|
+
b.item.value
|
|
129
|
+
for b in page_blocks(page, device, UUID(int=0))
|
|
130
|
+
if isinstance(b, SceneLineItemBlock)
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
125
134
|
def write_page(page: Page, device: DeviceProfile, author_uuid: UUID) -> bytes:
|
|
126
135
|
buf = io.BytesIO()
|
|
127
136
|
write_blocks(buf, page_blocks(page, device, author_uuid))
|
|
@@ -11,8 +11,8 @@ from rmink.extract.shade import MAX_SHADE_STROKES, shade
|
|
|
11
11
|
from rmink.extract.tone import tone_map
|
|
12
12
|
from rmink.model import Page, Stroke
|
|
13
13
|
from rmink.options import ConvertOptions, Shading
|
|
14
|
-
from rmink.render.preview import read_lines
|
|
15
|
-
from rmink.writer.rm_v6 import write_page
|
|
14
|
+
from rmink.render.preview import read_lines, render_lines, render_rm
|
|
15
|
+
from rmink.writer.rm_v6 import page_lines, write_page
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _ramp(w=600, h=200) -> np.ndarray:
|
|
@@ -106,3 +106,14 @@ def test_style_previews(tmp_path, examples):
|
|
|
106
106
|
assert [p.style for p in previews] == list(Shading)
|
|
107
107
|
assert all(p.image.size[0] == get_device("rm2").width for p in previews)
|
|
108
108
|
assert api.style_previews(examples["svg"], ConvertOptions()) == []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_rendering_lines_matches_the_written_file(tmp_path):
|
|
112
|
+
# Style previews skip the .rm round trip; the picture must not change.
|
|
113
|
+
src = tmp_path / "ramp.png"
|
|
114
|
+
Image.fromarray(_ramp()).save(src)
|
|
115
|
+
device = get_device("rm2")
|
|
116
|
+
page = api.render([src], ConvertOptions(shading="pencil")).document.pages[0]
|
|
117
|
+
direct = np.asarray(render_lines(page_lines(page, device), device, page.height), float)
|
|
118
|
+
stored = render_rm(write_page(page, device, uuid.uuid4()), device, page.height)
|
|
119
|
+
assert np.abs(direct - np.asarray(stored, float)).mean() < 0.5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|