1 /*
2 Drawpile - a collaborative drawing program.
3
4 Copyright (C) 2014-2019 Calle Laakkonen
5
6 Drawpile is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Drawpile is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Drawpile. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "filter.h"
21
22 #include "../libshared/record/reader.h"
23 #include "../libshared/record/writer.h"
24 #include "../libshared/net/undo.h"
25 #include "../libshared/net/brushes.h"
26 #include "../libshared/net/recording.h"
27
28 #include <QDebug>
29 #include <QVector>
30 #include <QHash>
31 #include <QSet>
32
33 namespace recording {
34
35 namespace {
36
37 static const uchar UNDOABLE = (1<<7);
38 static const uchar REMOVED = (1<<6);
39
40 struct FilterIndex {
41 // message type (protocol)
42 uchar type;
43
44 // message context ID
45 uchar ctxid;
46
47 // Filtering flags
48 uchar flags;
49
50 // Message position in the recording file
51 qint64 offset;
52 };
53
54 struct State {
55 // A FilterIndex entry for each message in the recording
56 QVector<FilterIndex> index;
57
58 // User join/leave message indices
59 // (used for removing looky-loos)
60 QHash<int, QList<int>> userjoins;
61
62 // Replacement messages for specific indices
63 // (currently used just for squishing)
64 QHash<int, protocol::NullableMessageRef> replacements;
65
66 // List of users who have participated in the session somehow
67 // (other than just loggin in/out)
68 QSet<int> users_seen;
69 };
70
mark_delete(FilterIndex & i)71 static inline void mark_delete(FilterIndex &i) {
72 i.flags |= REMOVED;
73 }
74
undostate(const FilterIndex & i)75 static inline protocol::MessageUndoState undostate(const FilterIndex &i) {
76 return protocol::MessageUndoState(i.flags & 0x03);
77 }
78
isUndoable(const FilterIndex & i)79 static inline bool isUndoable(const FilterIndex &i) {
80 return i.flags & UNDOABLE;
81 }
82
isDeleted(const FilterIndex & i)83 static inline bool isDeleted(const FilterIndex &i) {
84 return i.flags & (REMOVED | protocol::UNDONE);
85 }
86
87 // Basic filtering (including Undo)
filterMessage(const FilterOptions & options,State & state,protocol::MessagePtr msg,qint64 offset)88 static void filterMessage(const FilterOptions &options, State &state, protocol::MessagePtr msg, qint64 offset)
89 {
90 // Put this message in the index
91 state.index.append(FilterIndex {
92 uchar(msg->type()),
93 msg->contextId(),
94 msg->isUndoable() ? UNDOABLE : uchar(0),
95 offset
96 });
97
98 // Filter out select message types
99 switch(msg->type()) {
100 using namespace protocol;
101 case MSG_CHAT:
102 if(options.removeChat) {
103 mark_delete(state.index.last());
104 return;
105 }
106 break;
107
108 case MSG_USER_JOIN:
109 case MSG_USER_LEAVE:
110 state.userjoins[msg->contextId()].append(state.index.size()-1);
111 return;
112
113 case MSG_INTERVAL:
114 if(options.removeDelays) {
115 mark_delete(state.index.last());
116 return;
117 }
118 break;
119
120 case MSG_LASERTRAIL:
121 case MSG_MOVEPOINTER:
122 if(options.removeLasers) {
123 mark_delete(state.index.last());
124 return;
125 }
126 break;
127
128 case MSG_MARKER:
129 if(options.removeMarkers) {
130 mark_delete(state.index.last());
131 return;
132 }
133 break;
134
135 case MSG_FILTERED:
136 // Always filter out FILTERED messages
137 mark_delete(state.index.last());
138 return;
139
140 default: break;
141 }
142
143 // Perform undo
144 if(msg->type() == protocol::MSG_UNDO && options.removeUndone) {
145 // Normally, performing an undo will implicitly delete the undo action itself,
146 // but it can be restored by a redo. Therefore, we must explicitly flag the
147 // undo messages for deletion to be sure they are gone.
148 mark_delete(state.index.last());
149
150 // Perform an undo. This is a stripped down version of handleUndo from StateTracker
151 const protocol::Undo &cmd = msg.cast<protocol::Undo>();
152
153 const uchar ctxid = cmd.contextId();
154
155 // Step 1. Find undo or redo point
156 int pos = state.index.size();
157 int upCount = 0;
158
159 if(cmd.isRedo()) {
160 // Find the start of the undo sequence (oldest undone UndoPoint)
161 int redostart = pos;
162 while(--pos>=0 && upCount <= protocol::UNDO_DEPTH_LIMIT) {
163 const FilterIndex u = state.index[pos];
164 if(u.type == protocol::MSG_UNDOPOINT) {
165 ++upCount;
166 if(u.ctxid == ctxid) {
167 if(undostate(u) != protocol::DONE)
168 redostart = pos;
169 else
170 break;
171 }
172 }
173 }
174
175 if(redostart == state.index.size()) {
176 qDebug() << "nothing to redo for user" << cmd.contextId();
177 mark_delete(state.index.last());
178 return;
179 }
180 pos = redostart;
181
182 } else {
183 // Search for undoable actions from the end of the
184 // command stream towards the beginning
185 while(--pos>=0 && upCount <= protocol::UNDO_DEPTH_LIMIT) {
186 const FilterIndex u = state.index[pos];
187
188 if(u.type == protocol::MSG_UNDOPOINT) {
189 ++upCount;
190 if(u.ctxid == ctxid && undostate(u) == protocol::DONE)
191 break;
192 }
193 }
194 }
195
196 if(upCount > protocol::UNDO_DEPTH_LIMIT) {
197 qDebug() << "user" << cmd.contextId() << "cannot undo/redo beyond history limit";
198 mark_delete(state.index.last());
199 return;
200 }
201
202 // Step 2 is not needed here
203
204 // Step 3. (Un)mark all actions by the user as undone
205 if(cmd.isRedo()) {
206 int i=pos;
207 int sequence=2;
208 while(i<state.index.size()) {
209 FilterIndex &u = state.index[i];
210 if(u.ctxid == ctxid) {
211 if(u.type == protocol::MSG_UNDOPOINT && undostate(u) != protocol::GONE)
212 if(--sequence==0)
213 break;
214
215 // GONE messages cannot be redone
216 if(undostate(u) == protocol::UNDONE)
217 u.flags &= ~protocol::UNDONE;
218 }
219 ++i;
220 }
221 } else {
222 for(int i=pos;i<state.index.size();++i) {
223 FilterIndex &u = state.index[i];
224 if(u.ctxid == ctxid && isUndoable(u))
225 u.flags |= protocol::UNDONE;
226 }
227 }
228 // Steps 4 is not needed here.
229 }
230
231 if(msg->contextId()>0)
232 state.users_seen.insert(msg->contextId());
233
234 return;
235 }
236
237 // Remove Join/Leave messages of those users who didn't do anything
238 // TODO this could be smarter. A user can log in&out many times, but
239 // not draw something every time. Perhaps this could preseve the
240 // outermost login/logout messages in that case?
filterLookyloos(State & state)241 static void filterLookyloos(State &state)
242 {
243 for(const int id : state.userjoins.keys()) {
244 if(!state.users_seen.contains(id)) {
245 for(const int idx : state.userjoins[id]) {
246 mark_delete(state.index[idx]);
247 }
248 }
249 }
250 }
251
252 /**
253 * @brief Remove adjacent undo points by the same user
254 *
255 * This removes all duplicate undo points that appear _next to each other_ in
256 * the message stream. Such undo points are left over when actions are undone or
257 * silenced.
258 *
259 * @param state
260 */
filterAdjacentUndoPoints(State & state)261 static void filterAdjacentUndoPoints(State &state)
262 {
263 FilterIndex prev {0, 0, 0, 0};
264
265 for(int i=0;i<state.index.size();++i) {
266 FilterIndex &fi = state.index[i];
267
268 if(isDeleted(fi) || fi.type == protocol::MSG_INTERVAL)
269 continue;
270
271 if(
272 fi.type == protocol::MSG_UNDOPOINT && prev.type == protocol::MSG_UNDOPOINT &&
273 fi.ctxid == prev.ctxid
274 ) {
275 mark_delete(fi);
276 }
277
278 prev = fi;
279 }
280 }
281
isDabMessage(uchar msgtype)282 static inline bool isDabMessage(uchar msgtype) {
283 return msgtype == protocol::MSG_DRAWDABS_CLASSIC ||
284 msgtype == protocol::MSG_DRAWDABS_PIXEL ||
285 msgtype == protocol::MSG_DRAWDABS_PIXEL_SQUARE
286 ;
287
288 }
289
squishStrokes(State & state,Reader & recording,QString * errorMessage)290 static bool squishStrokes(State &state, Reader &recording, QString *errorMessage)
291 {
292 protocol::NullableMessageRef sequenceStartMessage;
293 FilterIndex sequenceStart {0, 0, 0, 0};
294 int sequenceStartIndex = 0;
295
296 for(int i=0;i<state.index.size();++i) {
297 FilterIndex &fi = state.index[i];
298
299 if(isDeleted(fi))
300 continue;
301
302 // Encountering any non-dab message ends a squishable sequence
303 if(!isDabMessage(fi.type)) {
304 sequenceStartMessage = nullptr;
305 continue;
306 }
307
308 // If we have an open sequence already, the new message type
309 // and context ID must match
310 if(!sequenceStartMessage.isNull()) {
311 if(fi.type != sequenceStart.type || fi.ctxid != sequenceStart.ctxid) {
312 sequenceStartMessage = nullptr;
313 continue;
314 }
315 }
316
317 // At this point this message is either the start of a new sequence
318 // OR a potential continuation of the current one
319
320 recording.seekTo(i, fi.offset);
321 MessageRecord msgrecord = recording.readNext();
322 if(msgrecord.status != MessageRecord::OK) {
323 qWarning("Error reading message #%d at offset %lld", i, fi.offset);
324 if(errorMessage)
325 *errorMessage = "File read error";
326 return false;
327 }
328
329 if(!isDabMessage(msgrecord.message->type())) {
330 qWarning("BUG: Inconsistency when reading recording. Expected a dab message at offset %lld, got %s", fi.offset, qPrintable(msgrecord.message->messageName()));
331 if(errorMessage)
332 *errorMessage = "Internal Application Error in squishStrokes function";
333 return false;
334 }
335
336 // If we have an open sequence, try squishing.
337 if(!sequenceStartMessage.isNull() && sequenceStartMessage.cast<protocol::DrawDabs>().extend(msgrecord.message.cast<protocol::DrawDabs>())) {
338
339 // Squish succeeded. Store the extended message.
340 if(!state.replacements.contains(sequenceStartIndex))
341 state.replacements[sequenceStartIndex] = sequenceStartMessage;
342
343 mark_delete(fi);
344
345 } else {
346 // Did not squish. This is a start of a new potentially squishable sequence
347 sequenceStart = fi;
348 sequenceStartMessage = msgrecord.message;
349 sequenceStartIndex = i;
350 }
351 }
352
353 return true;
354 }
355
doFilterRecording(const FilterOptions & options,State & state,Reader & recording,QString * errorMessage)356 static bool doFilterRecording(const FilterOptions &options, State &state, Reader &recording, QString *errorMessage)
357 {
358 // Read input file and perform basic filtering
359 // This constructs the filtering index
360 while(true) {
361 MessageRecord msg = recording.readNext();
362 if(msg.status == MessageRecord::END_OF_RECORDING)
363 break;
364 if(msg.status == MessageRecord::INVALID) {
365 qWarning() << "skipping invalid message type" << msg.invalid_type;
366 continue;
367 }
368
369 filterMessage(options, state, protocol::MessagePtr::fromNullable(msg.message), recording.currentPosition());
370 }
371
372 // More complicated filtering that uses the index
373
374 if(options.removeLookyLoos)
375 filterLookyloos(state);
376
377 if(options.squishStrokes) {
378 if(!squishStrokes(state, recording, errorMessage))
379 return false;
380 }
381
382 filterAdjacentUndoPoints(state);
383
384 return true;
385 }
386
387 }
388
filterRecording(const QString & input,const QString & outputfile,const FilterOptions & options,QString * errorMessage)389 bool filterRecording(const QString &input, const QString &outputfile, const FilterOptions &options, QString *errorMessage)
390 {
391 // Step 1. Open input and output files
392 Reader reader(input);
393 Compatibility readOk = reader.open();
394 if(readOk != COMPATIBLE && readOk != MINOR_INCOMPATIBILITY) {
395 qWarning() << "Cannot open recording for filtering. Error code" << readOk;
396 if(errorMessage)
397 *errorMessage = reader.errorString();
398 return false;
399 }
400
401 Writer writer(outputfile);
402 if(!writer.open()) {
403 qWarning() << "Cannot open record filter output file";
404 if(errorMessage)
405 *errorMessage = writer.errorString();
406 return false;
407 }
408
409 // Step 2. Mark messages for removal and replacement
410 State state;
411 if(!doFilterRecording(options, state, reader, errorMessage))
412 return false;
413
414 // Step 3. Copy remaining messages to output file
415 reader.rewind();
416
417 writer.writeHeader();
418
419 QByteArray buffer;
420 while(reader.readNextToBuffer(buffer)) {
421 const int pos = reader.currentIndex();
422
423 // Copy (or replace) original message, unless marked for deletion
424 if(!isDeleted(state.index[pos])) {
425
426 if(state.replacements.contains(pos))
427 writer.writeMessage(*state.replacements[pos]);
428 else
429 writer.writeFromBuffer(buffer);
430 }
431 }
432
433 writer.close();
434
435 return true;
436 }
437
438 }
439