Skip to content

Commit 9dbbfc1

Browse files
committed
* switch to OpenGL rendering
* remove intensity scaling (is this a useful feature?) * add ability to peek left/right with arrow keys * add settings for highlighter * add settings for added/removed color * add background to render canvas * add new version notification
1 parent cd42063 commit 9dbbfc1

14 files changed

Lines changed: 599 additions & 83 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## [0.1.0] - 2025-08-10
2+
* switch to OpenGL rendering
3+
* remove intensity scaling (is this a useful feature?)
4+
* add ability to peek left/right with arrow keys
5+
* add settings for highlighter
6+
* add settings for added/removed color
7+
* add background to render canvas
8+
19
## [0.0.2] - 2025-08-07
210
* fix commit method of PersistentDict
311
* initialize PersistentDict to APPDATA instead of desktop

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ Redliner allows for quickly identifying changes in text and visual documents by
2121
## Todo
2222
- ### V1
2323
- [X] Upload to GitHub
24-
- [ ] Finish initial documentation
2524
- [X] Develop installation / distribution procedure
25+
- [X] Switch to OpenGL
26+
- [X] Add update check button / built-in update installation
27+
- [ ] Make new version notif only pop up once
28+
- [ ] Finish initial documentation
2629
- [ ] Implement dynamic canvas
2730
- [ ] Implement export document composition
2831
- ### V2
2932
- [ ] Implement annotation tools
3033
- [ ] Implement text-aware search and PDF generation
3134
- [ ] Set up Linux and MacOS build scripts
32-
- [ ] Add update check button / built-in update installation
35+
- [ ] Add background rasterization for faster page loads
3336

3437

3538
## Extending Redliner for your company / workflow

redliner/common/constants.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,24 @@
22
import numpy as np
33
import re
44

5+
from common.common import resource_path
6+
57
PREVIEW_DPI = 10
68
PREVIEW_SIZE = qtc.QSize(128, 128)
79
GRAYSCALE_WEIGHTS = np.array([0.2989, 0.5870, 0.1140])
810
VERSION_PATTERN = re.compile(r'\[(\d+\.\d+\.\d+)\]')
9-
REMOTE_CHANGELOG = r"https://raw.githubusercontent.com/CJett/Redliner/refs/heads/main/CHANGELOG.md"
11+
REMOTE_CHANGELOG = r"https://raw.githubusercontent.com/CJett/Redliner/refs/heads/main/CHANGELOG.md"
12+
REMOTE = r"https://github.com/CJett/Redliner"
13+
14+
15+
with open (resource_path("diff.frag")) as f:
16+
DIFF_FRAG = f.read()
17+
18+
with open(resource_path("diff.vert")) as f:
19+
DIFF_VERT = f.read()
20+
21+
with open(resource_path("bg.frag")) as f:
22+
BG_FRAG = f.read()
23+
24+
with open(resource_path("bg.vert")) as f:
25+
BG_VERT = f.read()

redliner/core/doc_man.py

Lines changed: 143 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,101 @@
33
from redliner.core.ui import DocPreview
44
from redliner.extensions.fetcher import FETCHER_TYPES
55
from redliner.extensions.source_doc import SrcDoc
6-
from .diff import diff
7-
import numpy as np
86
from PyQt6 import QtCore as qtc, QtGui as qtg
97

8+
from .render import Renderer, RenderPage
9+
10+
11+
12+
def rgb_to_hex(red: int, green: int, blue: int, alpha: int | None = None) -> str:
13+
return f"#{hex(red)[2:].zfill(2)}{hex(green)[2:].zfill(2)}{hex(blue)[2:].zfill(2)} {alpha if alpha is not None else ''}"
14+
15+
def hex_to_rgb(val:str) -> tuple:
16+
val = val.strip().lower()
17+
if val[0] == "#":
18+
val = val[1:]
19+
r1, r2, g1, g2, b1, b2 = val
20+
return (int(r1+r2,16), int(g1+g2,16), int(b1+b2,16))
21+
22+
class ColorButton(qtw.QPushButton):
23+
signalColorChanged = qtc.pyqtSignal(str)
24+
25+
def __init__(self, hex):
26+
super().__init__()
27+
self.hx = hex
28+
self.setStyleSheet(f"background-color:{self.hx}")
29+
self.setText(self.hx)
30+
self.clicked.connect(self.color_pick)
31+
32+
def color_pick(self):
33+
color_pick = qtw.QColorDialog.getColor(qtg.QColor(self.hx))
34+
r, g, b, a = color_pick.getRgb()
35+
self.hx = rgb_to_hex(r, g, b)
36+
self.setStyleSheet(f"background-color:{self.hx}")
37+
self.setText(self.hx)
38+
self.signalColorChanged.emit(self.hx)
39+
40+
41+
class SettingsWidget(qtw.QWidget):
42+
signalSettingsChanged = qtc.pyqtSignal()
43+
def __init__(self, items:list, width:int):
44+
super().__init__()
45+
self.pd = PersistentDict()
46+
self._l = qtw.QVBoxLayout(self)
47+
self._l.setContentsMargins(0,0,0,0)
48+
self._l.setSpacing(2)
49+
self.setFixedWidth(width)
50+
tgt_l = self._l
51+
for row in items:
52+
if len(row) == 1:
53+
gb = qtw.QGroupBox(row[0])
54+
gb_l = qtw.QVBoxLayout(gb)
55+
self._l.addWidget(gb)
56+
tgt_l = gb_l
57+
else:
58+
_type, key, name, *args = row
59+
val = self.pd[key]
60+
_r = qtw.QWidget()
61+
_l = qtw.QHBoxLayout(_r)
62+
_l.setContentsMargins(0,0,0,0)
63+
_l.setSpacing(2)
64+
lb = qtw.QLabel(name)
65+
_l.addWidget(lb)
66+
lb.setSizePolicy(qtw.QSizePolicy.Policy.Fixed, qtw.QSizePolicy.Policy.Fixed)
67+
if _type == "bool":
68+
w = qtw.QCheckBox()
69+
w.setChecked(val)
70+
w.stateChanged.connect(lambda *_, _k=key, _w=w: self.set(_k, _w.isChecked()))
71+
if _type == "spin":
72+
w = qtw.QSpinBox()
73+
w.setRange(*args)
74+
w.setValue(val)
75+
w.valueChanged.connect(lambda *_, _k=key, _w=w: self.set(_k, _w.value()))
76+
if _type == "color":
77+
w = ColorButton(val)
78+
w.signalColorChanged.connect(lambda *_, _k=key, _w=w: self.set(_k, _w.hx))
79+
_l.addWidget(w)
80+
tgt_l.addWidget(_r)
81+
self._l.addStretch()
82+
83+
def set(self, key, value):
84+
self.pd[key] = value
85+
self.signalSettingsChanged.emit()
86+
87+
def __getitem__(self, key):
88+
return self.pd[key]
89+
1090
class DocMan(qtw.QWidget):
1191
def __init__(self):
1292
super().__init__()
1393
self.pd = PersistentDict()
14-
self.pd.default("scale_lo", 0)
15-
self.pd.default("scale_hi", 255)
94+
self.pd.default("highlighter_en", True)
95+
self.pd.default("highlighter_size", 4)
96+
self.pd.default("highlighter_sensitivity", 100)
1697
self.pd.default("dpi", 72)
98+
self.pd.default("removed_color", "#D0A000")
99+
self.pd.default("added_color", "#00A0D0")
100+
self.pd.default("highlighter_color", "#F0F000")
17101
self.lhs = None
18102
self.rhs = None
19103
self.click_side = "L"
@@ -40,30 +124,20 @@ def __init__(self):
40124
lhl.addWidget(self.lhp)
41125
rhl.addWidget(self.rhp)
42126

43-
w_settings = qtw.QWidget()
44-
_l.addWidget(w_settings)
45-
l_s = qtw.QGridLayout(w_settings)
46-
self.sb_dpi = qtw.QSpinBox()
47-
self.sb_dpi.setRange(0, 10000)
48-
self.sb_dpi.setValue(self.pd["dpi"])
49-
self.sb_dpi.valueChanged.connect(self.regen)
50-
l_s.addWidget(qtw.QLabel("DPI"), 0, 0)
51-
l_s.addWidget(self.sb_dpi, 0, 1)
52-
self.sb_lo = qtw.QSpinBox()
53-
self.sb_lo.setRange(0, 254)
54-
self.sb_lo.setValue(self.pd["scale_lo"])
55-
self.sb_lo.valueChanged.connect(self.regen)
56-
l_s.addWidget(qtw.QLabel("Lower Thresh"), 1, 0)
57-
l_s.addWidget(self.sb_lo, 1, 1)
58-
59-
self.sb_hi = qtw.QSpinBox()
60-
self.sb_hi.setRange(1, 255)
61-
self.sb_hi.setValue(self.pd["scale_hi"])
62-
self.sb_hi.valueChanged.connect(self.regen)
63-
l_s.addWidget(qtw.QLabel("Upper Thresh"), 2, 0)
64-
l_s.addWidget(self.sb_hi, 2, 1)
65-
l_s.setRowStretch(3, 1)
66-
w_settings.setFixedWidth(196)
127+
self.settings = SettingsWidget([
128+
["Render"],
129+
["spin", "dpi", "DPI", 0, 10000],
130+
["color", "removed_color", "Removed Color"],
131+
["color", "added_color", "Added Color"],
132+
["Highlighter"],
133+
["bool", "highlighter_en", "Enabled"],
134+
["spin", "highlighter_size", "Size (px)", 0, 128],
135+
["spin", "highlighter_sensitivity", "Sensitivity", 0, 100],
136+
["color", "highlighter_color", "Highlight Color"]
137+
], 196)
138+
self.settings.signalSettingsChanged.connect(self.regen)
139+
_l.addWidget(self.settings)
140+
self.renderer = Renderer()
67141
self.preview = qtw.QLabel()
68142
self.preview.setScaledContents(True)
69143
_l.addWidget(self.preview)
@@ -104,41 +178,50 @@ def doc_ready(self, doc: SrcDoc):
104178
self.rhp.set_doc(doc)
105179

106180
def regen(self):
107-
scale_lo = self.sb_lo.value()
108-
scale_hi = self.sb_hi.value()
109-
dpi = self.sb_dpi.value()
110-
if scale_hi <= scale_lo:
111-
if scale_lo != self.pd["scale_lo"]:
112-
# low scale was changed
113-
scale_hi = scale_lo + 1
114-
self.sb_hi.setValue(scale_hi)
115-
else:
116-
scale_lo = scale_hi - 1
117-
self.sb_lo.setValue(scale_lo)
118-
self.pd.update({
119-
"dpi": dpi,
120-
"scale_lo": scale_lo,
121-
"scale_hi": scale_hi
122-
})
181+
123182
lh_page = self.lhp.selectedIndexes()
124-
lh_data = None
125-
rh_data = None
183+
rh_page = self.rhp.selectedIndexes()
184+
render_page = RenderPage()
126185
if lh_page:
127186
lh_page = lh_page[0].row()
128-
lh_data = self.lhs.raster(lh_page, dpi)
129-
rh_page = self.rhp.selectedIndexes()
187+
render_page.lhs = self.lhs.page(lh_page, self.settings["dpi"])
130188
if rh_page:
131189
rh_page = rh_page[0].row()
132-
rh_data = self.rhs.raster(rh_page, dpi)
133-
if lh_data is not None:
134-
if rh_data is not None:
135-
im = diff(lh_data, rh_data, scale_lo, scale_hi)
136-
else:
137-
im = lh_data
138-
elif rh_data is not None:
139-
im = rh_data
140-
else:
141-
im = np.full((256, 256, 3), (0, 0, 255), dtype=np.uint8)
142-
image = qtg.QImage(im, im.shape[1], im.shape[0], im.strides[0], qtg.QImage.Format.Format_RGB888)
143-
self.preview.setPixmap(qtg.QPixmap.fromImage(image))
190+
render_page.rhs = self.rhs.page(rh_page, self.settings["dpi"])
191+
self.renderer.set_page(render_page)
192+
self.redraw()
193+
194+
def resizeEvent(self, a0):
195+
super().resizeEvent(a0)
196+
self.redraw()
197+
198+
def redraw(self, lh=True, rh=True):
199+
200+
px = self.renderer.render(hex_to_rgb(self.settings["added_color"]),
201+
hex_to_rgb(self.settings["removed_color"]),
202+
hex_to_rgb(self.settings["highlighter_color"]),
203+
self.settings["highlighter_en"],
204+
1-self.settings["highlighter_sensitivity"]/100,
205+
self.settings["highlighter_size"],
206+
0,
207+
0,
208+
1,
209+
0,
210+
lh,
211+
rh,
212+
self.preview.width(),
213+
self.preview.height())
214+
self.preview.setPixmap(qtg.QPixmap.fromImage(px))
144215
self.preview.setMinimumSize(qtc.QSize(64, 64))
216+
217+
def keyPressEvent(self, a0):
218+
if a0.key() == qtc.Qt.Key.Key_Left:
219+
self.redraw(rh=False)
220+
if a0.key() == qtc.Qt.Key.Key_Right:
221+
self.redraw(lh=False)
222+
super().keyPressEvent(a0)
223+
224+
def keyReleaseEvent(self, a0):
225+
self.redraw()
226+
227+

0 commit comments

Comments
 (0)