Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ readme = "README.md"

[tool.poetry.dependencies]
python = "^3.7"
colour = "^0.1.5"
pdfrw = "^0.4"
reportlab = "^3.5.59"
svglib = "^1.0.1"
Expand Down
32 changes: 23 additions & 9 deletions rmrl/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,22 @@
import zipfile

from . import render
from .constants import VERSION
from .constants import VERSION, HIGHLIGHT_DEFAULT_COLOR
from .render import InvalidColor
from .sources import ZipSource

def main():
parser = argparse.ArgumentParser(description="Render a PDF file from a Remarkable document.")
parser = argparse.ArgumentParser(description="Render a PDF file from a Remarkable document.",
epilog='The colors may be specified as hex strings ("#AABBCC", "#ABC") or well-known names ("black", "red"). If no gray color is given, the program will use an average of the white and black colors. A fixed amount of transparency will be applied to the color given for the highlighter.')
parser.add_argument('input', help="Filename of zip file, or root-level unpacked file of document. Use '-' to read zip file from stdin.")
parser.add_argument('output', nargs='?', default='', help="Filename where PDF file should be written. Omit to write to stdout.")
parser.add_argument('--alpha', default=0.3, help="Opacity for template background (0 for no background).")
parser.add_argument('--no-expand', action='store_true', help="Don't expand pages to margins on device.")
parser.add_argument('--only-annotated', action='store_true', help="Only render pages with annotations.")
parser.add_argument('--black', default='black', help='Color for "black" pen.')
parser.add_argument('--white', default='white', help='Color for "white" pen.')
parser.add_argument('--gray', '--grey', default=None, help='Color for "gray" pen.')
parser.add_argument('--highlight', '--hilight', '--hl', default=HIGHLIGHT_DEFAULT_COLOR, help='Color for the highlighter.')
parser.add_argument('--version', action='version', version=VERSION)
args = parser.parse_args()

Expand All @@ -41,13 +47,21 @@ def main():
else:
fout = sys.stdout.buffer

stream = render(source,
template_alpha=float(args.alpha),
expand_pages=not args.no_expand,
only_annotated=args.only_annotated)
fout.write(stream.read())
fout.close()
return 0
try:
stream = render(source,
template_alpha=float(args.alpha),
expand_pages=not args.no_expand,
only_annotated=args.only_annotated,
black=args.black,
white=args.white,
gray=args.gray,
highlight=args.highlight)
fout.write(stream.read())
fout.close()
return 0
except InvalidColor as e:
print(str(e), file=sys.stderr)
return 1

if __name__ == '__main__':
sys.exit(main())
3 changes: 3 additions & 0 deletions rmrl/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@
TEMPLATE_PATH = xdg_data_home() / 'rmrl' / 'templates'

VERSION = pkg_resources.get_distribution('rmrl').version

HIGHLIGHT_DEFAULT_COLOR = '#FFE949'
HIGHLIGHT_ALPHA = 0.392
21 changes: 12 additions & 9 deletions rmrl/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@

class DocumentPage:
# A single page in a document
def __init__(self, source, pid, pagenum):
def __init__(self, source, pid, pagenum, colors):
# Page 0 is the first page!
self.source = source
self.num = pagenum
self.colors = colors

# On disk, these files are named by a UUID
self.rmpath = f'{{ID}}/{pid}.rm'
Expand Down Expand Up @@ -103,7 +104,7 @@ def load_layers(self):
except:
name = 'Layer ' + str(i + 1)

layer = DocumentPageLayer(self, name=name)
layer = DocumentPageLayer(self, name=name, colors=self.colors)
layer.strokes = layerstrokes
self.layers.append(layer)

Expand Down Expand Up @@ -152,17 +153,15 @@ def render_to_painter(self, canvas, vector, template_alpha):
class DocumentPageLayer:
pen_widths = []

def __init__(self, page, name=None):
def __init__(self, page, colors, name=None):
self.page = page
self.name = name

self.colors = [
#QSettings().value('pane/notebooks/export_pdf_blackink'),
#QSettings().value('pane/notebooks/export_pdf_grayink'),
#QSettings().value('pane/notebooks/export_pdf_whiteink')
(0, 0, 0),
(0.5, 0.5, 0.5),
(1, 1, 1)
colors.black.rgb,
colors.gray.rgb,
colors.white.rgb,
colors.highlight.rgb,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namedtuples allow position-based indexing, so as long as we get it in the right order, we could just pass self.colors = colors here, and then do colors[i].rgb in the pen itself.

I'm not completely sold on this idea. I does decrease the amount of fussing needed here, but it also locks our color structure to the details of the lines files from remarkable. I could go either way here, so I'll let you make the decision.

]

# Set this from the calling func
Expand Down Expand Up @@ -233,6 +232,10 @@ def paint_strokes(self, canvas, vector):
log.error("Unknown pen code %d" % pen)
penclass = pens.GenericPen

# Hack to get the right color for the highlighter.
if penclass == pens.HighlighterPen:
color = -1

qpen = penclass(vector=vector,
layer=self,
color=self.colors[color])
Expand Down
3 changes: 2 additions & 1 deletion rmrl/pens/highlighter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from ..constants import HIGHLIGHT_ALPHA
from .generic import GenericPen

class HighlighterPen(GenericPen):
Expand All @@ -28,7 +29,7 @@ def paint_stroke(self, canvas, stroke):
canvas.setLineCap(2) # Square
canvas.setLineJoin(1) # Round
#canvas.setDash ?? for solid line
canvas.setStrokeColor((1.000, 0.914, 0.290), alpha=0.392)
canvas.setStrokeColor(self.color, alpha=HIGHLIGHT_ALPHA)
canvas.setLineWidth(stroke.width)

path = canvas.beginPath()
Expand Down
63 changes: 58 additions & 5 deletions rmrl/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,38 @@
from pathlib import Path
import json
import re
from collections import namedtuple

from colour import Color

from pdfrw import PdfReader, PdfWriter, PageMerge, PdfDict, PdfArray, PdfName, \
IndirectPdfDict, uncompress, compress

from reportlab.pdfgen import canvas

from . import document, sources
from .constants import PDFHEIGHT, PDFWIDTH, PTPERPX, SPOOL_MAX
from .constants import PDFHEIGHT, PDFWIDTH, PTPERPX, SPOOL_MAX, HIGHLIGHT_DEFAULT_COLOR


log = logging.getLogger(__name__)

Colors = namedtuple('Colors', ['black', 'white', 'gray', 'highlight'])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this PenColors, just to avoid confusion with the Color class.


class InvalidColor(Exception):
"""Raised when an invalid string is passed as a color."""
pass


def render(source, *,
progress_cb=lambda x: None,
expand_pages=True,
template_alpha=0.3,
only_annotated=False):
"""
Render a source document as a PDF file.
only_annotated=False,
black='black',
white='white',
gray=None,
highlight=HIGHLIGHT_DEFAULT_COLOR):
"""Render a source document as a PDF file.

source: The reMarkable document to be rendered. This may be
- A filename or pathlib.Path to a zip file containing the
Expand All @@ -59,8 +72,19 @@ def render(source, *,
makes the templates invisible, 1 makes them fully dark.
only_annotated: Boolean value (default False) indicating whether only
pages with annotations should be output.
black: A string giving the color to use as "black" in the document.
Can be a color name or a hex string. Default: 'black'
white: A string giving the color to use as "white" in the document.
See `black` parameter for format. Default: 'white'
gray: A string giving the color to use as "gray" in the document.
See `black` parameter for format. Default: None, which means to
pick an average between the "white" and "black" values.
highlight: A string giving the color to use for the highlighter.
See `black` parameter for format.
"""

colors = parse_colors(black, white, gray, highlight)

vector=True # TODO: Different rendering styles
source = sources.get_source(source)

Expand Down Expand Up @@ -89,7 +113,7 @@ def render(source, *,
changed_pages = []
annotations = []
for i in range(0, len(pages)):
page = document.DocumentPage(source, pages[i], i)
page = document.DocumentPage(source, pages[i], i, colors=colors)
if source.exists(page.rmpath):
changed_pages.append(i)
page.render_to_painter(pdf_canvas, vector, template_alpha)
Expand Down Expand Up @@ -177,6 +201,35 @@ def render(source, *,
return stream


def parse_colors(black, white, gray, highlight):
black_color = parse_color(black, 'black')
white_color = parse_color(white, 'white')
highlight_color = parse_color(highlight, 'highlight')

if gray is not None:
# Use the explicit gray value.
gray_color = parse_color(gray, 'gray')
elif black_color.saturation == 0 or white_color.saturation == 0:
# One or the other of the color endpoints is a shade of gray (or
# white or black). Use average in RGB space. This keeps the hue
# from the saturated endpoint and just lets the other endpoint
# either darken or lighten it.
gray_color = Color(rgb=((b + w) / 2 for b, w in zip(black_color.rgb, white_color.rgb)))
else:
# Both "black" and "white" have color elements to them. Use
# Color.range_to, which more or less averages in HSL space.
gray_color = list(black_color.range_to(white_color, 3))[1]

return Colors(black=black_color, white=white_color, gray=gray_color, highlight=highlight_color)


def parse_color(color_string, name):
try:
return Color(color_string)
except Exception as e:
raise InvalidColor('"{}" color was passed an invalid string: {}'.format(name, str(e)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps raise ... from None here?



def do_apply_ocg(basepage, rmpage, i, uses_base_pdf, ocgprop, annotations):
ocgpage = IndirectPdfDict(
Type=PdfName('OCG'),
Expand Down