rmscene

repository·main·Indexed 41 days ago

https://github.com/ricklupton/rmscene

A Python library for parsing reMarkable v6 .rm files (software version 3), specializing in the extraction of text and drawn lines. It provides tools for reading and writing blocks, reconstructing the hierarchical SceneTree, and managing CRDT data sequences.

Tokens
8.5K
Snippets
38
Records
41
Agent score
100%

What's inside rmscene

  1. Overview of rmscene

    main

    rmscene is a Python library designed to read v6 files from reMarkable tablets (specifically those running software version 3).

    Its primary capability is extracting text data alongside drawn lines. While it is specialized for text extraction, it can also read various other data types contained within reMarkable files.

    If your goal is to convert .rm files into other formats like SVG, PDF, or Markdown, you should use rmc, which uses this library as its engine.

  2. How blocks and subblocks work

    main

    The TaggedBlockWriter uses context managers to handle the hierarchical structure of .rm files. Data written inside these blocks is buffered and then written out with the appropriate length and header information once the block is exited.

    Top-level Blocks

    Use write_block(block_type, min_version, current_version) to create a top-level block. This method ensures that all writes within the with scope are accumulated and then written with a length prefix and versioning metadata.

    Subblocks

    Use write_subblock(index) to create a subblock associated with a specific index. This is commonly used for complex types like strings or LWW (Last-Write-Wins) values. The subblock is written with a TagType.Length4 tag and a length prefix.

    Note: You cannot nest a top-level block inside another top-level block; attempting to do so will raise an UnexpectedBlockError.

    # Example of nesting subblocks within a top-level block
    with writer.write_block(block_type=1, min_version=1, current_version=1):
        with writer.write_subblock(index=10):
            writer.write_bool(1, True)
  3. How blocks and the SceneTree relate

    main

    The reMarkable file format is a stream of independent Block objects. However, these blocks describe a hierarchical structure.

    • SceneTreeBlock defines the existence of nodes and their parent-child relationships.
    • TreeNodeBlock provides metadata (like labels and visibility) for existing nodes.
    • SceneItemBlock (and its subclasses like SceneLineItemBlock) attaches actual content (like drawings or text) to specific nodes in the tree.

    read_tree automates the process of calling build_tree, which iterates through all blocks and assembles them into a single SceneTree object.

  4. Understand CrdtId and LwwValue

    main

    The library uses CrdtId and LwwValue to handle Conflict-free Replicated Data Type (CRDT) logic used by reMarkable.

    • CrdtId: A dataclass representing an identifier or timestamp, consisting of part1 (uint8) and part2 (varuint).
    • LwwValue: A generic container for a 'Last-Write-Wins' value. It pairs a timestamp (of type CrdtId) with a value of type _T. This is used to resolve conflicts by picking the value with the highest timestamp.
    from rmscene.tagged_block_common import CrdtId, LwwValue
    
    # Creating a CRDT ID
    my_id = CrdtId(part1=1, part2=100)
    
    # Creating a Last-Write-Wins value
    my_lww = LwwValue(timestamp=my_id, value="some data")
  5. Initialize TaggedBlockReader for remarkable .rm files

    main

    To read the structure of remarkable v6 files, instantiate TaggedBlockReader with a binary file stream and call read_header() as the first operation.

    import io
    from rmscene.tagged_block_reader import TaggedBlockReader
    
    with open('file.rm', 'rb') as f:
        reader = TaggedBlockReader(f)
        reader.read_header()
        # Proceed to read blocks or values
    from rmscene.tagged_block_reader import TaggedBlockReader
    
    # Assuming 'data' is a binary file-like object
    reader = TaggedBlockReader(data)
    reader.read_header()
  6. Handle UnexpectedBlockError during stream parsing

    main

    When using DataStream.read_tag(), an UnexpectedBlockError is raised if the stream does not contain the expected index or TagType. This is useful for validating that the parser is correctly aligned with the file structure.

    If you want to check for a tag without advancing the stream or raising an error, use check_tag() instead.

    try:
        index, tag_type = stream.read_tag(expected_index=5, expected_type=TagType.ID)
    except UnexpectedBlockError as e:
        print(f"Stream misalignment: {e}")
  7. Read top-level blocks using read_block()

    main

    The read_block() method is a context manager used to iterate through top-level blocks in a remarkable v6 file. It returns a MainBlockInfo object containing metadata about the block (offset, size, type, and version).

    Key behaviors:

    • It yields None when no more blocks are available (EOF).
    • It ensures that all data within the block's declared size is consumed before exiting the context. If the reader exits the block early, it raises a BlockOverflowError. If data remains unread, it logs a warning and consumes the excess to maintain alignment.
    for block_info in reader.read_block():
        if block_info is None:
            break
        print(f"Block Type: {block_info.block_type}, Size: {block_info.size}")
        # Process block content here
    for block_info in reader.read_block():
        if block_info is None:
            break
        # Use block_info.block_type, block_info.min_version, etc.
  8. Access CrdtSequenceItem metadata

    main

    To access the underlying CrdtSequenceItem objects (including their IDs and positional metadata) rather than just the values, use the .sequence_items() method on a CrdtSequence instance.

    items = sequence.sequence_items()
    for item in items:
        print(f"ID: {item.item_id}, Value: {item.value}")
  9. Iterate through leaf items with SceneTree.walk()

    main

    The walk() method provides an iterator that traverses the entire scene hierarchy and yields only the leaf items. It skips si.Group objects, effectively flattening the tree so you can process individual content items (like text or other non-group items) without manually handling the recursive group structure.

    for item in tree.walk():
        # 'item' is a si.SceneItem that is not a si.Group
        process(item)
  10. Write blocks to a file using write_blocks

    main

    To serialize a collection of Block objects back into a reMarkable file format, use write_blocks. You can pass an optional options dictionary to specify the target version (e.g., {'version': '3.4'}).

    from rmscene.scene_stream import write_blocks
    
    blocks = [...] # A list of Block objects
    with open('output.rm', 'wb') as f:
        write_blocks(f, blocks, options={'version': '3.4'})
  11. Use DataStream to read/write reMarkable v6 files

    main

    The DataStream class provides a high-level interface for interacting with reMarkable .lines files (version 6). It wraps a binary stream and provides methods for reading/writing standard types (bool, uint, float), varuints, and specific reMarkable structures like CrdtId and tags.

    Key Workflow:

    1. Initialize DataStream with a binary IO object.
    2. Call read_header() or write_header() as the first operation.
    3. Use read_tag() or write_tag() to navigate the tagged block structure.
    4. Use type-specific methods (e.g., read_uint32(), read_varuint()) to consume the data.
    from io import BytesIO
    from rmscene.tagged_block_common import DataStream, TagType
    
    # Reading a file
    with open("example.lines", "rb") as f:
        stream = DataStream(f)
        stream.read_header()
        # Example: reading a tag with index 10 and type Byte4
        index, tag_type = stream.read_tag(10, TagType.Byte4)
        value = stream.read_uint32()
    
    # Writing a file
    output = BytesIO()
    stream = DataStream(output)
    stream.write_header()
    stream.write_tag(10, TagType.Byte4)
    stream.write_uint32(12345)
  12. Write LWW (Last-Write-Wins) values

    main

    For CRDT-based data, use the write_lww_* methods. These methods automatically wrap the value in a subblock that includes the timestamp (as a tagged ID) and the value itself.

    Supported LWW types:

    • write_lww_bool(index, value)
    • write_lww_byte(index, value)
    • write_lww_float(index, value)
    • write_lww_id(index, value)
    • write_lww_string(index, value)
    # Assuming value is an instance of LwwValue
    writer.write_lww_bool(5, my_lww_bool_value)