Skip to content

Commit fd408a8

Browse files
committed
Get annotations working
1 parent 89b5cc3 commit fd408a8

File tree

4 files changed

+224
-57
lines changed

4 files changed

+224
-57
lines changed

rmrl/annotation.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright 2021 Ben Rush
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
from __future__ import annotations
17+
18+
class Point:
19+
def __init__(self, x: float, y: float):
20+
self.x = x
21+
self.y = y
22+
23+
def toList(self) -> list:
24+
return [self.x, self.y]
25+
26+
class Rect:
27+
"""
28+
From PDF spec:
29+
a specific array object used to describe locations on a page and
30+
bounding boxes for a variety of objects and written as an array
31+
of four numbers giving the coordinates of a pair of diagonally
32+
opposite corners, typically in the form [ll.x, ll.y, ur.x, ur.x]
33+
"""
34+
35+
def __init__(self, ll: Point, ur: Point):
36+
self.ll = ll
37+
self.ur = ur
38+
39+
def intersects(self, rectB: Rect) -> bool:
40+
# To check if either rectangle is actually a line
41+
# For example : l1 ={-1,0} r1={1,1} l2={0,-1} r2={0,1}
42+
43+
if (self.ll.x == self.ur.x or self.ll.y == self.ur.y or rectB.ll.x == rectB.ur.x or rectB.ll.y == rectB.ur.y):
44+
# the line cannot have positive overlap
45+
return False
46+
47+
48+
# If one rectangle is on left side of other
49+
if(self.ll.x >= rectB.ur.y or rectB.ll.x >= self.ur.y):
50+
return False
51+
52+
# If one rectangle is above other
53+
if(self.ur.y <= rectB.ll.y or rectB.ur.y <= self.ll.y):
54+
return False
55+
56+
return True
57+
58+
def union(self, rectB: Rect) -> Rect:
59+
ll = Point(min(self.ll.x, rectB.ll.x),
60+
min(self.ll.y, rectB.ll.y))
61+
ur = Point(max(self.ur.x, rectB.ur.x),
62+
max(self.ur.y, rectB.ur.y))
63+
return Rect(ll, ur)
64+
65+
def toList(self) -> list:
66+
return [self.ll.x, self.ll.y, self.ur.x, self.ur.y]
67+
68+
class QuadPoints:
69+
"""
70+
From PDF spec:
71+
An array of 8 x n numbers specifying the coordinates of n quadrilaterals
72+
in default user space. Each quadrilateral shall encompass a word or group
73+
of contiguous words in the text underlying the annotation. The coordinates
74+
for each quadrilateral shall be given in the order x1, y1, x2, y2, x3, y3, x4, y4
75+
specifying the quadrilateral's four vertices in counterclockwise order
76+
starting with the lower left. The text shall be oriented with respect to the
77+
edge connecting points (x1, y1) with (x2, y2).
78+
"""
79+
80+
points: list[Point]
81+
82+
def __init__(self, points: list[Point]):
83+
self.points = points
84+
85+
def append(self, quadpoints: QuadPoints) -> QuadPoints:
86+
return QuadPoints(self.points + quadpoints.points)
87+
88+
def toList(self) -> list:
89+
return [c for p in points for c in p.toList()]
90+
91+
92+
@staticmethod
93+
def fromRect(rect: Rect):
94+
"""
95+
Assumes that the rect is aligned with the text. Will return incorrect
96+
results otherwise
97+
"""
98+
# Needs to be in this order to account for rotations applied later?
99+
# ll.x, ur.y, ur.x, ur.y, ll.x, ll.y, ur.x, ll.y
100+
quadpoints = [Point(rect.ll.x, rect.ur.y),
101+
Point(rect.ur.x, rect.ur.y),
102+
Point(rect.ll.x, rect.ll.y),
103+
Point(rect.ur.x, rect.ll.y)]
104+
return QuadPoints(quadpoints)
105+
106+
class Annotation():
107+
annotype: str
108+
rect: Rect
109+
quadpoints: QuadPoints
110+
111+
def __init__(self, annotype: str, rect: Rect, quadpoints: list = None):
112+
self.annotype = annotype
113+
self.rect = rect
114+
if quadpoints:
115+
self.quadpoints = quadpoints
116+
else:
117+
self.quadpoints = QuadPoints.fromRect(rect)
118+
119+
def united(self, annot: Annotation) -> Annotation:
120+
if self.annotype != annot.annotype:
121+
raise Exception("Cannot merge annotations with different types")
122+
123+
return Annotation(self.annotype,
124+
self.rect.union(annot.rect),
125+
self.quadpoints.append(annot.quadpoints))
126+
127+
def intersects(self, annot: Annotation) -> bool:
128+
return self.rect.intersects(annot.rect)

rmrl/document.py

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from . import lines, pens
2525
from .constants import DISPLAY, PDFHEIGHT, PDFWIDTH, PTPERPX, TEMPLATE_PATH
2626

27+
from typing import List, Tuple
2728

2829
log = logging.getLogger(__name__)
2930

@@ -172,27 +173,27 @@ def __init__(self, page, name=None):
172173
# PDF layers are ever implemented.
173174
self.annot_paths = []
174175

175-
def get_grouped_annotations(self):
176-
# return: (LayerName, [(AnnotType, minX, minY, maxX, maxY)])
176+
def get_grouped_annotations(self) -> Tuple[str, list]:
177+
# return: (LayerName, [Annotations])
177178

178179
# Compare all the annot_paths to each other. If any overlap,
179180
# they will be grouped together. This is done recursively.
180181
def grouping_func(pathset):
181182
newset = []
182183

183184
for p in pathset:
184-
annotype = p[0]
185-
path = p[1]
185+
annotype = p.annotype
186+
#path = p[1] #returns (xmin, ymin, xmax, ymax)
186187
did_fit = False
187188
for i, g in enumerate(newset):
188-
gannotype = g[0]
189-
group = g[1]
189+
gannotype = g.annotype
190+
#group = g[1]
190191
# Only compare annotations of the same type
191192
if gannotype != annotype:
192193
continue
193-
if path.intersects(group):
194+
if p.intersects(g):
194195
did_fit = True
195-
newset[i] = (annotype, group.united(path))
196+
newset[i] = g.united(p) #left off here, need to build united and quadpoints
196197
break
197198
if did_fit:
198199
continue
@@ -207,22 +208,7 @@ def grouping_func(pathset):
207208
return newset
208209

209210
grouped = grouping_func(self.annot_paths)
210-
211-
# Get the bounding rect of each group, which sets the PDF
212-
# annotation geometry.
213-
annot_rects = []
214-
for p in grouped:
215-
annotype = p[0]
216-
path = p[1]
217-
rect = path.boundingRect()
218-
annot = (annotype,
219-
float(rect.x()),
220-
float(rect.y()),
221-
float(rect.x() + rect.width()),
222-
float(rect.y() + rect.height()))
223-
annot_rects.append(annot)
224-
225-
return (self.name, annot_rects)
211+
return (self.name, grouped)
226212

227213
def paint_strokes(self, canvas, vector):
228214
for stroke in self.strokes:

rmrl/pens/highlighter.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,63 @@
1515
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1616

1717
from .generic import GenericPen
18+
from reportlab.graphics.shapes import Rect
19+
from reportlab.pdfgen.pathobject import PDFPathObject
20+
from ..annotation import Annotation, Point, Rect, QuadPoints
1821

1922
class HighlighterPen(GenericPen):
2023
def __init__(self, *args, **kwargs):
2124
super().__init__(*args, **kwargs)
2225
self.layer = kwargs.get('layer')
23-
self.annotate = False #TODO bool(int(QSettings().value(
26+
self.annotate = True#False #TODO bool(int(QSettings().value(
2427
# 'pane/notebooks/export_pdf_annotate')))
2528

2629
def paint_stroke(self, canvas, stroke):
2730
canvas.saveState()
2831
canvas.setLineCap(2) # Square
2932
canvas.setLineJoin(1) # Round
3033
#canvas.setDash ?? for solid line
31-
canvas.setStrokeColor((1.000, 0.914, 0.290), alpha=0.392)
34+
yellow = (1.000, 0.914, 0.290)
35+
canvas.setStrokeColor(yellow, alpha=0.392)
3236
canvas.setLineWidth(stroke.width)
3337

3438
path = canvas.beginPath()
3539
path.moveTo(stroke.segments[0].x, stroke.segments[0].y)
40+
41+
x0 = stroke.segments[0].x
42+
y0 = stroke.segments[0].y
43+
44+
ll = Point(x0, y0)
45+
ur = Point(x0, y0)
46+
3647
for segment in stroke.segments[1:]:
3748
path.lineTo(segment.x, segment.y)
38-
canvas.drawPath(path, stroke=1, fill=0)
39-
canvas.restoreState()
49+
50+
# Do some basic vector math to rotate the line width
51+
# perpendicular to this segment
52+
53+
x1 = segment.x
54+
y1 = segment.y
55+
width = segment.width
56+
57+
l = [x1-x0, y1-y0]
58+
v0 = -l[1]/l[0]
59+
scale = (1+v0**2)**0.5
60+
orthogonal = [v0/scale, 1/scale]
61+
62+
xmin = x0-width/2*orthogonal[0]
63+
ymin = y0-width/2*orthogonal[1]
64+
xmax = x1+width/2*orthogonal[0]
65+
ymax = y1+width/2*orthogonal[1]
66+
67+
ll = Point(min(ll.x, xmin), min(ll.y, ymin))
68+
ur = Point(max(ur.x, xmax), max(ur.y, ymax))
69+
70+
x0 = x1
71+
y0 = y1
4072

4173
if self.annotate:
42-
assert False
43-
# Create outline of the path. Annotations that are close to
44-
# each other get groups. This is determined by overlapping
45-
# paths. In order to fuzz this, we'll double the normal
46-
# width and extend the end caps.
47-
self.setWidthF(self.widthF() * 2)
48-
self.setCapStyle(Qt.SquareCap)
49-
opath = QPainterPathStroker(self).createStroke(path)
50-
# The annotation type is carried all the way through. This
51-
# is the type specified in the PDF spec.
52-
self.layer.annot_paths.append(('Highlight', opath))
74+
self.layer.annot_paths.append(Annotation("Highlight", Rect(ll, ur)))
75+
76+
canvas.drawPath(path, stroke=1, fill=0)
77+
canvas.restoreState()

rmrl/render.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from . import document, sources
2929
from .constants import PDFHEIGHT, PDFWIDTH, PTPERPX, SPOOL_MAX
30+
from typing import Tuple, List
3031

3132

3233
log = logging.getLogger(__name__)
@@ -87,7 +88,7 @@ def render(source, *,
8788
# about 500 pages could use up to 3 GB of RAM. Create them by
8889
# iteration so they get released by garbage collector.
8990
changed_pages = []
90-
annotations = []
91+
annotations = [] # [pages[layers[(layer, [Annotations])]]]
9192
for i in range(0, len(pages)):
9293
page = document.DocumentPage(source, pages[i], i)
9394
if source.exists(page.rmpath):
@@ -388,23 +389,36 @@ def do_apply_ocg(basepage, rmpage, i, uses_base_pdf, ocgprop, annotations):
388389

389390
return ocgorderinner
390391

392+
def invert_coords(point) -> Tuple[float]:
393+
print(point)
394+
x = (point.x * PTPERPX)
395+
y = PDFHEIGHT - (point.y * PTPERPX)
396+
return (x, y)
391397

392398
def apply_annotations(rmpage, page_annot, ocgorderinner):
399+
# page_annot = layers[(layer, [Annotations])]
393400
for k, layer_a in enumerate(page_annot):
401+
# layer_a = (layer, [Annotations])
394402
layerannots = layer_a[1]
395403
for a in layerannots:
396404
# PDF origin is in bottom-left, so invert all
397405
# y-coordinates.
398-
author = 'RCU' #self.model.device_info['rcuname']
406+
author = 'reMarkable' #self.model.device_info['rcuname']
407+
408+
x1, y1 = invert_coords(a.rect.ll)
409+
x2, y2 = invert_coords(a.rect.ur)
410+
411+
w = x2-x1
412+
h = y1-y2
413+
print(a.quadpoints.points)
414+
qp = [c for p in map(invert_coords, a.quadpoints.points) for c in p]
415+
399416
pdf_a = PdfDict(Type=PdfName('Annot'),
400-
Rect=PdfArray([
401-
(a[1] * PTPERPX),
402-
PDFHEIGHT - (a[2] * PTPERPX),
403-
(a[3] * PTPERPX),
404-
PDFHEIGHT - (a[4] * PTPERPX)]),
417+
Rect=PdfArray([x1, y1, x2, y2]),
418+
QuadPoints=PdfArray(qp),
405419
T=author,
406420
ANN='pdfmark',
407-
Subtype=PdfName(a[0]),
421+
Subtype=PdfName(a.annotype),
408422
P=rmpage)
409423
# Set to indirect because it makes a cleaner PDF
410424
# output.
@@ -566,24 +580,23 @@ def merge_pages(basepage, rmpage, changed_page, expand_pages):
566580
if '/Annots' in rmpage:
567581
for a, annot in enumerate(rmpage.Annots):
568582
rect = annot.Rect
569-
rmpage.Annots[a].Rect = PdfArray([
570-
rect[1],
571-
PDFWIDTH - rect[0],
572-
rect[3],
573-
PDFWIDTH - rect[2]])
583+
rmpage.Annots[a].Rect = PdfArray(rotate_annot_points(rect))
584+
585+
qp = annot.QuadPoints
586+
rmpage.Annots[a].QuadPoints = PdfArray(rotate_annot_points(qp))
574587

575588
annot_adjust = [0, 0]
576589

577590
if '/Annots' in rmpage:
578591
for a, annot in enumerate(rmpage.Annots):
579592
rect = annot.Rect
580-
newrect = PdfArray([
581-
rect[0] * scale + annot_adjust[0],
582-
rect[1] * scale + annot_adjust[1],
583-
rect[2] * scale + annot_adjust[0],
584-
rect[3] * scale + annot_adjust[1]])
593+
newrect = PdfArray(scale_annot_points(rect, scale, annot_adjust))
585594
rmpage.Annots[a].Rect = newrect
586595

596+
qp = annot.QuadPoints
597+
newqp = PdfArray(scale_annot_points(qp, scale, annot_adjust))
598+
rmpage.Annots[a].QuadPoints = newqp
599+
587600
# Gives the basepage the rmpage as a new object
588601
np.render()
589602

@@ -592,3 +605,18 @@ def merge_pages(basepage, rmpage, changed_page, expand_pages):
592605
if not '/Annots' in basepage:
593606
basepage.Annots = PdfArray()
594607
basepage.Annots += rmpage.Annots
608+
609+
def rotate_annot_points(points: list) -> list:
610+
rotated = []
611+
for n in range(0,len(points),2):
612+
rotated.append(points[n+1])
613+
rotated.append(PDFWIDTH-points[n])
614+
615+
return rotated
616+
617+
def scale_annot_points(points: list, scale:float, adjust: list) -> list:
618+
scaled = []
619+
for i, p in enumerate(points):
620+
scaled.append(p*scale + adjust[i%2])
621+
622+
return scaled

0 commit comments

Comments
 (0)