ricklupton / rmc

Convert to/from v6 .rm files from the reMarkable tablet
MIT License
56 stars 14 forks source link

use svg.py #15

Open KMIJPH opened 1 month ago

KMIJPH commented 1 month ago

I propose the following:

1) use svg.py to create the svg 2) use width and height as inputs to blocks_to_svg 3) add SceneGlyphItemBlock (highlighted text)

Comment on 2): If a pdf (which might have different width,height) is annotated, the svg needs to have the same width and height. Using this approach the pdf and annotations (remarkable lines) are aligned (at least in my tests).

All in all, it also makes the code much more readable.

PS: the same logic could be used for SceneTree.

Cheers

from typing import Iterable

import svg
from rmscene import Block, SceneLineItemBlock, RootTextBlock, SceneGlyphItemBlock

from .writing_tools import Pen, remarkable_palette

def blocks_to_svg(blocks: Iterable[Block], width: float, height: float) -> str:
    """Convert Blocks to SVG."""
    elements = []

    for block in list(blocks):
        if isinstance(block, SceneLineItemBlock):
            if block.item.value is not None:
                elements.append(stroke(block))
        elif isinstance(block, RootTextBlock):
            if block.value is not None:
                elements.append(text(block))
        elif isinstance(block, SceneGlyphItemBlock):
            elements.append(rect(block))

    xpos_shift = width / 2

    g = svg.G(
        transform=[svg.Translate(xpos_shift, 0)],
        id="p1",
        style="display:inline",
        elements=elements,
    )
    result = svg.SVG(width=width, height=height, elements=[g])
    return result.as_str()

def stroke(block: SceneLineItemBlock) -> svg.Element:
    points = []
    pen = Pen.create(
        block.item.value.tool.value,
        block.item.value.color.value,
        block.item.value.thickness_scale,
    )
    last_xpos = -1.0
    last_ypos = -1.0
    last_segment_width: float = 0

    for point_id, point in enumerate(block.item.value.points):
        xpos = point.x
        ypos = point.y
        if point_id % pen.segment_length == 0:
            segment_color = pen.get_segment_color(
                point.speed,
                point.direction,
                point.width,
                point.pressure,
                last_segment_width,
            )
            segment_width = pen.get_segment_width(
                point.speed,
                point.direction,
                point.width,
                point.pressure,
                last_segment_width,
            )
            segment_opacity = pen.get_segment_opacity(
                point.speed,
                point.direction,
                point.width,
                point.pressure,
                last_segment_width,
            )
            if last_xpos != -1.0:
                points += [last_xpos, last_ypos]

        last_xpos = xpos
        last_ypos = ypos
        last_segment_width = segment_width
        points += [xpos, ypos]

    return svg.Polyline(
        style=f"fill:none;stroke:{segment_color};stroke-width:{segment_width};opacity:{segment_opacity}",
        stroke_linecap=pen.stroke_linecap,  # type: ignore
        points=points,  # type: ignore
    )

def rect(block: SceneGlyphItemBlock) -> svg.Element:
    """
    Highlighted text.
    """
    value = block.item.value
    color = "rgb" + str(tuple(remarkable_palette[value.color]))
    rectangle = value.rectangles[0]
    return svg.Rect(
        x=rectangle.x,
        y=rectangle.y,
        width=rectangle.w,
        height=rectangle.h,
        fill=color,
        fill_opacity=0.3,
    )

def text(block: RootTextBlock) -> svg.Element:
    """
    Text is split on newlines and using TSpans and their 'dy' attribute,
    lines are emulated using a spacing value of 1.2em.
    """
    text = "".join([i[1] for i in block.value.items.items()])  # type: ignore
    lines = text.splitlines()

    return svg.G(
        style="font: 50px serif",
        elements=[
            svg.Text(
                x=block.value.pos_x,
                y=block.value.pos_y,
                elements=[svg.TSpan(dy="1.2em", text=line) for line in lines],  # type: ignore
            )
        ],
    )
KMIJPH commented 1 month ago

PPS: It seems like remarkable uses some sort of smoothing on the strokes. A moving average (window size=5) on the stroke points comes close to their result.

KMIJPH commented 1 month ago

PPPS: I'm using the lastest main of rmscene here