FEATURE: Compress Data
CLEANUP: Separating out much of the concern of the large pyPenNotes.py file into SaveRestore.py and UserDrawingArea.py patch from haakeyar Thanks a lot! --- Full Ticket Message --- UserDrawingArea.py is a widget that the user can draw on and you can receive tha strokes that the user has drawn. SaveRestore contains classes for saving and loading the data. It is split into two closes. A base class takes care of things in common for all file formats, while a subclass implements the actual file format. This way, we could easily implement other file formats, for example a text format where only parts of the file are loaded, to improve loading speed, or ability to save to an sqlite file. In the base class, I have implemented a simple compression of the notes. Points closer than QUALITY_LOSS (currently set at 5) pixels are merged. This compressed a test note file with 77% and you can barely see the difference. I have attached the original file, the compressed file and a a file with two notes file, the compressed first and the original last (open this file in the original pyPenNotes and switch between the notes to see the difference). There are also other ways to compress the notes even more (no need for more than two points in a straight line), but I have not implemented that (yet). Maybe it would be better to move the compression to UserDrawingArea - it would have both good and bad sides. pyPenNotes.py still has too much responsibility in my opinion - it both displays the window and coordinates SaveRestore and UserDrawingArea, but I haven't done anything about that (yet). If you want to discuss any of the changes, feel free to contact me on IRC or mail me at my nick at gmail dot com if you want to discuss the changes. git-svn-id: http://www.neo1973-germany.de/svn@74 46df4e5c-bc4e-4628-a0fc-830ba316316d
This commit is contained in:
parent
a4d3e62796
commit
c78e13b4da
2 changed files with 356 additions and 0 deletions
185
pyPenNotes/trunk/src/SaveRestore.py
Normal file
185
pyPenNotes/trunk/src/SaveRestore.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
"""
|
||||
* saveRestore.py - pyPenNotes - Save and restore the notes
|
||||
*
|
||||
* (C) 2008 by Kristian Mueller <kristian-m@kristian-m.de>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pyPenNotes import COLOR_LIST
|
||||
|
||||
class PenNote:
|
||||
def __init__(self, bg_color="#000000000000", strokes=[]):
|
||||
self.bg_color = bg_color
|
||||
self.strokes = strokes[:] # Copy to make sure we don't run into strange bugs due to mutable objects
|
||||
|
||||
|
||||
class BaseSaveRestore:
|
||||
"""Abstract class for saving and restoring notes."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Changes that are not yet saved.
|
||||
self.changes = {}
|
||||
|
||||
# Quality loss in the compression (remove unneeded points). Measured in pixels.
|
||||
self.quality_loss = 0
|
||||
|
||||
def revert_changes(self):
|
||||
self.changes = {}
|
||||
|
||||
def get_note(self, index):
|
||||
"""
|
||||
Abstract. Get a note by it's index.
|
||||
Returns a SaveRestore.PenNote
|
||||
"""
|
||||
pass
|
||||
|
||||
def in_line(self, point1, point2, point3):
|
||||
"""Check if the three points are in line, making the point in the middle unneeded."""
|
||||
|
||||
# Check if point2 is equal to or at most 'self.quality_loss' pixels different than point1
|
||||
if abs(point1[0] - point2[0]) < self.quality_loss \
|
||||
and abs(point1[1] - point2[1]) < self.quality_loss:
|
||||
return True
|
||||
|
||||
# Todo: Detect more unneeded points by looking for straight lines etc.
|
||||
|
||||
def compress_note(self, note):
|
||||
"""
|
||||
Compress a note by removing unneeded points.
|
||||
|
||||
'note': The note to compress
|
||||
"""
|
||||
count = 0
|
||||
for stroke in note.strokes:
|
||||
for index, point in enumerate(stroke[2]):
|
||||
while len(stroke[2]) > (index + 2) \
|
||||
and self.in_line(stroke[2][index], stroke[2][index+1], stroke[2][index+2]):
|
||||
del stroke[2][index+1]
|
||||
count += 1
|
||||
print "Deleted %i points from a note." % count
|
||||
return note
|
||||
|
||||
|
||||
def set_note(self, index, penNote):
|
||||
self.changes[index] = self.compress_note(penNote)
|
||||
|
||||
def save(self):
|
||||
"""Abstract. Save all changes."""
|
||||
pass
|
||||
|
||||
class BasicFile(BaseSaveRestore):
|
||||
"""Save to a basic file. Load everything into memory on startup."""
|
||||
|
||||
def __init__(self, file_name="~/.penNotes.strokes_data"):
|
||||
"""
|
||||
Initialize and load all the data into memory.
|
||||
'file_name': The name of the file where the data is stored
|
||||
"""
|
||||
BaseSaveRestore.__init__(self)
|
||||
|
||||
self.file_name = os.path.expanduser(file_name)
|
||||
self.pen_notes = [] # Not really necessary, as it will be defined in load() anyway, but keeping it here too so that __init__ sort of contains an overview of all data attributes
|
||||
|
||||
self.load()
|
||||
|
||||
def get_note(self, index):
|
||||
if self.changes.has_key(index):
|
||||
return self.changes[index]
|
||||
elif len(self.pen_notes) > index:
|
||||
return self.pen_notes[index]
|
||||
else:
|
||||
return PenNote()
|
||||
|
||||
def load(self):
|
||||
"""Load data from the data file."""
|
||||
## throw away the old stuff :-(
|
||||
self.pen_notes = []
|
||||
|
||||
count = 0
|
||||
|
||||
if os.path.exists(self.file_name):
|
||||
file_fd = open(self.file_name, 'r')
|
||||
try:
|
||||
for line in file_fd.read().split('\n'):
|
||||
comma_values = line.split(",")
|
||||
if len(comma_values) >= 2: ## at least 1 coord
|
||||
if len(line.split(";")[1].rstrip()) <= 0:
|
||||
continue
|
||||
if not len(self.pen_notes):
|
||||
raise Exception("Bad data file: %s" % self.file_name)
|
||||
fg_color = COLOR_LIST[int(comma_values[0])]
|
||||
bg_color = COLOR_LIST[int(comma_values[1])]
|
||||
thickness = int(comma_values[2].split(";")[0])
|
||||
|
||||
self.pen_notes[-1].bg_color = bg_color
|
||||
self.pen_notes[-1].strokes.append((fg_color, thickness, []))
|
||||
|
||||
for coord in line.split("; ")[1].split(" "):
|
||||
coords = coord.split(",")
|
||||
if len(coords) <= 1:
|
||||
continue
|
||||
|
||||
self.pen_notes[-1].strokes[-1][2].append((int(coords[0]), int(coords[1])))
|
||||
else:
|
||||
if len(line) >= 1:
|
||||
count += 1
|
||||
self.pen_notes.append(PenNote())
|
||||
finally:
|
||||
file_fd.close()
|
||||
else:
|
||||
print "No notebook file found - using an empty one."
|
||||
|
||||
print "Loaded %i notes from %s." % (count, self.file_name)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
save data to file - format is:
|
||||
Note 001
|
||||
fg, bg, thickness; x,y x,y x,y
|
||||
fg, bg, thickness; x,y x,y x,y
|
||||
Note 002
|
||||
fg, bg, thickness; x,y x,y x,y
|
||||
"""
|
||||
file_fd = open(self.file_name, 'w')
|
||||
count = 0
|
||||
|
||||
# Move changes from self.changes to self.pen_notes
|
||||
for index, note in self.changes.iteritems():
|
||||
while len(self.pen_notes) <= index: # The user has created a new note. Create new notes until we reach index
|
||||
self.pen_notes.append(PenNote())
|
||||
self.pen_notes[index] = note
|
||||
|
||||
|
||||
for note in self.pen_notes:
|
||||
bg_color = note.bg_color
|
||||
count += 1
|
||||
file_fd.write("Note %4.4d\n" %count)
|
||||
|
||||
for piece in note.strokes:
|
||||
fg_color = piece[0]
|
||||
thickness = piece[1]
|
||||
line = "%d, %d, %d; " %(COLOR_LIST.index(fg_color), COLOR_LIST.index(bg_color), thickness)
|
||||
if len(piece[2]) >= 1:
|
||||
for coord in piece[2]:
|
||||
line += "%d,%d " % coord
|
||||
file_fd.write("%s\n" %line)
|
||||
|
||||
file_fd.close()
|
||||
|
||||
print "Saved %i notes to %s." % (count, self.file_name)
|
171
pyPenNotes/trunk/src/UserDrawingArea.py
Normal file
171
pyPenNotes/trunk/src/UserDrawingArea.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/python
|
||||
"""
|
||||
* userDrawingArea.py - pyPenNotes - An area a user can draw on
|
||||
*
|
||||
* (C) 2008 by Kristian Mueller <kristian-m@kristian-m.de>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
"""
|
||||
import gtk
|
||||
import gobject
|
||||
import sys
|
||||
|
||||
class UserDrawingArea(gtk.DrawingArea):
|
||||
def __init__(self):
|
||||
gtk.DrawingArea.__init__(self)
|
||||
|
||||
self.fg_color = self.get_colormap().alloc_color("#0000FF")
|
||||
self.bg_color = self.get_colormap().alloc_color("#000000")
|
||||
self.line_width = 5
|
||||
|
||||
self.dragging = False
|
||||
|
||||
# Each stroke is a tuple: (fg color, width, [(x,y)])
|
||||
self.strokes = []
|
||||
|
||||
self.add_events(gtk.gdk.BUTTON_MOTION_MASK | \
|
||||
gtk.gdk.BUTTON_PRESS_MASK | \
|
||||
gtk.gdk.BUTTON_RELEASE_MASK)
|
||||
self.connect("realize", self.realize);
|
||||
self.connect("button-press-event", self.mouse_press)
|
||||
self.connect("button-release-event", self.mouse_up)
|
||||
self.connect("motion-notify-event", self.mouse_move)
|
||||
self.connect("expose-event", lambda widget,event: self.redraw())
|
||||
|
||||
def realize(self, event):
|
||||
"""Called when the widget is first displayed."""
|
||||
self.gc = self.get_style().fg_gc[gtk.STATE_NORMAL]
|
||||
self.gc.set_line_attributes(self.line_width, \
|
||||
gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, \
|
||||
gtk.gdk.CAP_ROUND)
|
||||
self.redraw()
|
||||
|
||||
def set_bg_color(self, bg_color):
|
||||
"""
|
||||
Set the bg_color of the area.
|
||||
|
||||
'bg_color': Anything that can be the first argument to gtk.gdk.Colormap.alloc_color
|
||||
"""
|
||||
self.bg_color = self.get_colormap().alloc_color(bg_color)
|
||||
self.redraw()
|
||||
def get_bg_color(self):
|
||||
"""Get the bg_color of the area. The format is lowercase hex with four digits per color."""
|
||||
return self.bg_color.to_string()
|
||||
|
||||
def set_fg_color(self, bg_color):
|
||||
"""
|
||||
Set the fg_color of the area.
|
||||
|
||||
'bg_color': Anything that can be the first argument to gtk.gdk.Colormap.alloc_color
|
||||
"""
|
||||
self.fg_color = self.get_colormap().alloc_color(bg_color)
|
||||
self.redraw()
|
||||
def get_fg_color(self):
|
||||
"""Get the fg_color of the area. The format is lowercase hex with four digits per color."""
|
||||
return self.fg_color.to_string()
|
||||
|
||||
def clear(self):
|
||||
"""Clear all strokes from the area."""
|
||||
del self.strokes[:]
|
||||
self.current_stroke_points = None
|
||||
self.redraw()
|
||||
|
||||
def get_strokes(self):
|
||||
"""Returns a list of the strokes. The stroke is a tuple of (fg_color, stroke_width, [(x,y)]), where fg_color is lowercase hex with 4 digits per color."""
|
||||
return [(fg_color.to_string(), stroke_width, points) \
|
||||
for (fg_color, stroke_width, points) in self.strokes] # Convert the fg_color to a hex string
|
||||
|
||||
def set_strokes(self, strokes):
|
||||
"""
|
||||
Set strokes and draw them on the screen.
|
||||
'strokes': A tuple of (fg_color, stroke_width, [(x,y)]), where fg_color is lowercase hex with 4 digits per color.
|
||||
"""
|
||||
self.strokes = [(self.get_colormap().alloc_color(fg_color), stroke_width, points) \
|
||||
for (fg_color, stroke_width, points) in strokes] # Allocate fg_color
|
||||
self.redraw()
|
||||
|
||||
def new_stroke(self, x, y):
|
||||
"""Create a new stroke starting at (x, y)"""
|
||||
self.strokes.append((self.fg_color, self.line_width, [(x,y)]))
|
||||
self.current_stroke_points = self.strokes[-1][2]
|
||||
self.update_gc()
|
||||
self.add_point_to_stroke(x, y)
|
||||
|
||||
def add_point_to_stroke(self, x, y):
|
||||
"""Add a point to the last stroke."""
|
||||
self.current_stroke_points.append((x,y))
|
||||
self.window.draw_lines(self.gc, self.current_stroke_points[-2:])
|
||||
|
||||
def clear_draw_screen(self, event):
|
||||
"""Overdraw the screen using background color."""
|
||||
fg_backup = self.gc.foreground
|
||||
self.gc.foreground = self.gc.background
|
||||
self.window.draw_rectangle(self.gc, True, 0, 0, -1, -1)
|
||||
self.gc.foreground = fg_backup
|
||||
|
||||
def update_gc(self):
|
||||
"""Private. Update self.gc from self.fg_color, self.bg_color and self.line_width."""
|
||||
self.gc.foreground = self.fg_color
|
||||
self.gc.background = self.bg_color
|
||||
self.gc.line_width= self.line_width
|
||||
|
||||
def redraw(self):
|
||||
"""Redraw screen using the notes content."""
|
||||
## step 1 - update the gc in case we have changed the background or something like that
|
||||
self.update_gc()
|
||||
|
||||
## step 2 - clear screen <-- will cause a flicker!
|
||||
self.clear_draw_screen(None)
|
||||
|
||||
## step 3 - redraw all the lines
|
||||
fg_backup = self.gc.foreground
|
||||
for stroke in self.strokes:
|
||||
self.gc.foreground = stroke[0]
|
||||
self.gc.line_width = stroke[1]
|
||||
self.window.draw_lines(self.gc, stroke[2])
|
||||
self.gc.line_width = self.line_width
|
||||
self.gc.foreground = fg_backup
|
||||
def undo(self):
|
||||
if len(self.strokes) >= 1:
|
||||
self.strokes.pop()
|
||||
if len(self.strokes):
|
||||
self.current_stroke_points = self.strokes[-1][2]
|
||||
self.redraw()
|
||||
|
||||
def mouse_move(self, widget, event):
|
||||
if(self.dragging):
|
||||
self.add_point_to_stroke(int(event.x), int(event.y))
|
||||
|
||||
def mouse_press(self, widget, event):
|
||||
self.dragging = True
|
||||
self.new_stroke(int(event.x), int(event.y))
|
||||
|
||||
def mouse_up(self, widget, event):
|
||||
self.dragging = False
|
||||
|
||||
if __name__ == "__main__": # Show a sample UserDrawingArea
|
||||
area = UserDrawingArea()
|
||||
|
||||
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||||
window.set_default_size(480, 640)
|
||||
window.set_title("UserDrawingArea")
|
||||
window.connect("destroy", lambda w: gtk.main_quit())
|
||||
window.add(area)
|
||||
window.show_all()
|
||||
gtk.main()
|
||||
|
||||
print "Strokes:"
|
||||
print area.get_strokes()
|
Loading…
Reference in a new issue