rmc icon indicating copy to clipboard operation
rmc copied to clipboard

use svg.py

Open ghost opened this issue 1 year ago • 2 comments

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
            )
        ],
    )

ghost avatar Oct 20 '24 15:10 ghost

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.

ghost avatar Oct 20 '24 15:10 ghost

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

ghost avatar Oct 20 '24 15:10 ghost