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:
kriss 2008-04-24 19:33:46 +00:00
parent a4d3e62796
commit c78e13b4da
2 changed files with 356 additions and 0 deletions

View 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)

View 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()