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@73 46df4e5c-bc4e-4628-a0fc-830ba316316d
This commit is contained in:
kriss 2008-04-24 18:21:45 +00:00
parent b5d1f2c08c
commit a4d3e62796
3 changed files with 72 additions and 368 deletions

View file

@ -21,77 +21,32 @@
"""
import gtk
import gobject ## for multithreading
import sys ## exit
import os ## file handling
import notesList
from UserDrawingArea import UserDrawingArea
import SaveRestore
COLOR_LIST = ["#0000FF", "#FF0000", "#00FF00", "#FFFF00", "#FFFFFF", "#000000"]
COLOR_LIST = ["#00000000ffff", "#ffff00000000", "#0000ffff0000", "#ffffffff0000", "#ffffffffffff", "#000000000000"]
DEFAULT_BG = 5 ## Black
DEFAULT_FG = 0 ## Blue
DEFAULT_SIZE = 4 ## Diameter 4 Pixel
DATA_FILE = "~/.penNotes.strokes_data"
## A note is defined by its strokes a background and the line thickness.
## Strokes of the same color and thickness are combined in a strokes_list.
## The order of strokes will stay the same.
class PenNote:
def __init__(self):
self.bg_color = DEFAULT_BG
self.last_fg_color = 0
self.last_line_thickness = DEFAULT_SIZE
self.last_stroke = (0, 0)
self.strokes_list = [] # current list of strokes from
# the same color or thickness
# strokes are tuples of src and dest
self.image_list = [] # list of (color, thickness, strokes_list)
self.image_list.append((self.last_fg_color, self.last_line_thickness,
self.strokes_list))
def add_stroke(self, src, dest, color, line_thickness):
if (color != self.last_fg_color) or \
(self.last_line_thickness != line_thickness) \
or self.last_stroke != src:
self.strokes_list = []
self.strokes_list.append((src[0], src[1]))
self.image_list.append((color, line_thickness, self.strokes_list))
self.last_fg_color = color
self.last_line_thickness = line_thickness
self.strokes_list.append((dest[0], dest[1]))
self.last_stroke = dest
def append_point_to_stroke(self, coord):
self.strokes_list.append(coord)
def append_new_point(self, coord, color, thickness):
self.strokes_list = []
self.strokes_list.append(coord)
self.image_list.append((color, thickness, self.strokes_list))
self.last_fg_color = color
self.last_line_thickness = thickness
def clear(self):
self.__init__()
QUALITY_LOSS = 5 ## Quality loss when removing unneeded points. Measured in pixels.
class pyPenNotes:
## init the class
def __init__(self):
self.state = "pre-init"
self.current_note_number = 0
self.size_num = DEFAULT_SIZE
self.fg_color = DEFAULT_FG
self.bg_color = DEFAULT_BG
self.pen_notes = []
self.current_note = PenNote()
self.pen_notes.append(self.current_note)
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_default_size(480, 640)
self.window.set_title("pyPenNotes")
self.window.connect("destroy", lambda w: gtk.main_quit())
self.window.connect("destroy", self.end_program)
self.window.show()
self.more_options_visible = False
self.state = "init-done"
self.save_restore = SaveRestore.BasicFile(DATA_FILE)
self.save_restore.quality_loss = QUALITY_LOSS
def update_ui(self):
if self.state == "init-done":
@ -109,7 +64,8 @@ class pyPenNotes:
main_toolbar.set_style(gtk.TOOLBAR_ICONS);
self.sub_toolbar = gtk.Toolbar()
self.sub_toolbar.set_style(gtk.TOOLBAR_ICONS);
self.area = gtk.DrawingArea()
self.area = UserDrawingArea()
self.area.line_width = DEFAULT_SIZE
self.table = gtk.Table(2,2)
self.hruler = gtk.HRuler()
self.vruler = gtk.VRuler()
@ -122,10 +78,10 @@ class pyPenNotes:
size_evnt_box = gtk.EventBox()
self.size_number_entry = gtk.Label()
size_evnt_box.add(self.size_number_entry)
self.size_number_entry.set_text("%2.2dpx" % self.size_num)
self.size_number_entry.set_text("%2.2dpx" % DEFAULT_SIZE)
self.size_number_entry.modify_fg(gtk.STATE_NORMAL, \
self.size_number_entry.get_colormap().alloc_color(\
COLOR_LIST[self.fg_color]))
COLOR_LIST[DEFAULT_FG]))
self.size_number_entry.set_width_chars(4) # max: "99 px"
size_evnt_box.set_events(gtk.gdk.BUTTON_PRESS_MASK)
size_evnt_box.connect("button_press_event", self.fg_color_select)
@ -136,7 +92,7 @@ class pyPenNotes:
self.note_number_entry.set_text("%4.4d" % (self.current_note_number+1))
self.note_number_entry.modify_fg(gtk.STATE_NORMAL, \
self.size_number_entry.get_colormap().\
alloc_color(COLOR_LIST[self.bg_color]))
alloc_color(COLOR_LIST[DEFAULT_BG]))
self.note_number_entry.set_width_chars(4)
note_evnt_box.set_events(gtk.gdk.BUTTON_PRESS_MASK)
note_evnt_box.connect("button_press_event", self.bg_color_select)
@ -187,9 +143,9 @@ class pyPenNotes:
# Undo
self.sub_toolbar.insert(create_toolbutton("gtk-undo", self.undo, True), -1)
# Revert to saved
self.sub_toolbar.insert(create_toolbutton("gtk-revert-to-saved", self.load, True), -1)
self.sub_toolbar.insert(create_toolbutton("gtk-revert-to-saved", self.revert_to_saved, True), -1)
# Save
self.sub_toolbar.insert(create_toolbutton("gtk-save",self.save, True),-1)
self.sub_toolbar.insert(create_toolbutton("gtk-save", self.save, True),-1)
# Quit
self.sub_toolbar.insert(create_toolbutton("gtk-quit", self.end_program, True), -1)
@ -204,58 +160,15 @@ class pyPenNotes:
vbox.pack_start(self.sub_toolbar, False, False, 0)
vbox.add(self.table);
self.window.add(vbox)
self.area.set_events(gtk.gdk.POINTER_MOTION_MASK |
gtk.gdk.POINTER_MOTION_HINT_MASK )
self.area.connect("expose-event", self.area_expose_cb)
def motion_notify(ruler, event):
return ruler.emit("motion_notify_event", event)
def add_pixel(widget, event):
if self.clicked:
pos = widget.get_pointer()
# print "blub <%s/%s>" % (widget.get_pointer()[0], \
# widget.get_pointer()[1])
backup_fb = self.gc.foreground
try:
self.gc.foreground = widget.window.get_colormap().alloc_color(\
COLOR_LIST[self.fg_color])
if self.last == (0, 0):
self.last = pos
self.gc.set_line_attributes(self.size_num, \
gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, \
gtk.gdk.CAP_ROUND)
widget.window.draw_line(self.gc, self.last[0], \
self.last[1], pos[0], pos[1])
finally:
self.gc.foreground = backup_fb
self.current_note.add_stroke((self.last[0], self.last[1]),
(pos[0], pos[1]), self.fg_color, self.size_num)
self.last = pos
self.area.add_events(gtk.gdk.POINTER_MOTION_MASK |
gtk.gdk.POINTER_MOTION_HINT_MASK )
self.area.connect_object("motion_notify_event", motion_notify,
self.hruler)
self.area.connect_object("motion_notify_event", motion_notify,
self.vruler)
self.clicked = False
self.last = (0, 0)
def click(widget, event):
self.clicked = True
add_pixel(widget, event)
def unclick(widget, event):
self.last = (0, 0);
self.clicked = False
self.area.add_events(gtk.gdk.BUTTON_MOTION_MASK | \
gtk.gdk.BUTTON_PRESS_MASK | \
gtk.gdk.BUTTON_RELEASE_MASK)
self.area.connect("button-press-event", click)
self.area.connect("button-release-event", unclick)
self.area.connect("motion-notify-event", add_pixel)
def size_allocate_cb(wid, allocation):
x, y, w, h = allocation
l,u,p,m = self.hruler.get_range()
@ -270,19 +183,8 @@ class pyPenNotes:
self.hruler.hide()
self.vruler.hide()
self.update_area()
## lets update the screen so our load can redraw to something
self.area_expose_cb(self.area, None)
self.load(None)
###########################################################################
## GTK stuff ##############################################################
###########################################################################
def delete_event(self, widget, event, data=None):
self.save(None)
gtk.main_quit()
return False
###########################################################################
@ -306,44 +208,52 @@ class pyPenNotes:
## change brush size--
def prev_size(self, event):
self.size_num /= 2
if self.size_num <= 0:
self.size_num = 1
self.size_number_entry.set_text("%2.2d px" % self.size_num)
self.area.line_width /= 2
if self.area.line_width <= 0:
self.area.line_width = 1
self.size_number_entry.set_text("%2.2d px" % self.area.line_width)
## change brush size++
def next_size(self, event):
self.size_num *= 2
if self.size_num > 99:
self.size_num = 99
self.size_number_entry.set_text("%2.2d px" % self.size_num)
self.area.line_width *= 2
if self.area.line_width > 99:
self.area.line_width = 99
self.size_number_entry.set_text("%2.2d px" % self.area.line_width)
def load_changes_from_area(self):
"""Load changes made to the area, so that they can be saved."""
self.save_restore.set_note(self.current_note_number, \
SaveRestore.PenNote(self.area.get_bg_color(), self.area.get_strokes()))
def update_area(self):
"""Load notes with self.current_note_number and display them on the UserDrawingArea"""
note = self.save_restore.get_note(self.current_note_number)
self.area.set_bg_color(note.bg_color)
self.area.set_strokes(note.strokes)
self.note_number_entry.modify_fg(gtk.STATE_NORMAL, \
self.size_number_entry.get_colormap().\
alloc_color(self.area.get_bg_color()))
## prev note
def prev_note(self, event):
"""Select the previous note."""
self.load_changes_from_area()
if self.current_note_number > 0:
self.current_note_number -= 1
self.note_number_entry.set_text("%4.4d"%(self.current_note_number+1))
self.current_note = self.pen_notes[self.current_note_number]
self.redraw()
self.update_area()
## new or next note
def next_note(self, event):
"""Select the next or a new note."""
self.load_changes_from_area()
self.current_note_number += 1
self.note_number_entry.set_text("%4.4d" % (self.current_note_number+1))
if len(self.pen_notes) <= self.current_note_number:
print "creating a new Note..."
self.current_note = PenNote()
self.current_note.bg_color = self.bg_color
self.pen_notes.append(self.current_note)
self.current_note = self.pen_notes[self.current_note_number]
self.redraw()
self.update_area()
## show more options (a second toolbar)
def more_options(self, event):
"""Show more options. (A second toolbar and the rulers.)"""
if self.more_options_visible:
self.sub_toolbar.hide()
self.hruler.hide()
@ -356,107 +266,19 @@ class pyPenNotes:
self.more_options_visible = True
def undo(self, event):
if len(self.current_note.image_list) >= 1:
self.current_note.image_list.pop()
self.redraw()
self.area.undo()
def revert_to_saved(self, event):
self.save_restore.revert_changes()
self.update_area()
def save(self, event):
self.load_changes_from_area()
self.save_restore.save()
def end_program(self, event):
self.save(None)
sys.exit()
###########################################################################
## save and restore - as kind-of-CSV-file #################################
###########################################################################
## load from file - format is:
## fg, bg, thickness; x,y x,y x,y
## every line not containing at last two ',' declare a new note
def load(self, event):
## throw away the old stuff :-(
self.pen_notes = []
count = 0
data_file_name = os.path.expanduser(DATA_FILE)
if os.path.exists(data_file_name):
file_fd = open(data_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
fg_color = int(comma_values[0])
bg_color = int(comma_values[1])
self.current_note.bg_color = bg_color
thickness = int(comma_values[2].split(";")[0])
if len(line.split(";")[1].rstrip()) <= 0:
continue
new_stroke = True
for coord in line.split("; ")[1].split(" "):
coords = coord.split(",")
if len(coords) <= 1:
continue
## first point
if new_stroke:
self.current_note.append_new_point(\
(int(coords[0]), \
int(coords[1])), fg_color, thickness)
# self.current_note.strokes_list.append(\
# int(coords[0]), int(coords[1])))
self.current_note.append_point_to_stroke(\
(int(coords[0]), int(coords[1])))
new_stroke = False
else:
if len(line) >= 1:
count += 1
self.current_note = PenNote()
self.pen_notes.append(self.current_note)
finally:
file_fd.close()
else:
print "No notebook file found - using an empty one."
count += 1
self.current_note = PenNote()
self.pen_notes.append(self.current_note)
print "count: %s, current: %s" %(count, self.current_note_number)
if count <= self.current_note_number:
print "Sorry there is no note %4.4d left bringing you to note 0001"\
% (count + 1)
# Count is zero-indexed, while what is displayed to the user is not
self.current_note_number = 0
self.next_note(None) # Update the note number box
self.prev_note(None) #
self.current_note = self.pen_notes[self.current_note_number]
self.redraw()
## save to file - format is:
## fg, bg, thickness; x,y x,y x,y
def save(self, event):
file_fd = open(os.path.expanduser(DATA_FILE), 'w')
count = 0
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.image_list:
fg_color = piece[0]
thickness = piece[1]
line = "%d, %d, %d; " %(fg_color, bg_color, thickness)
if len(piece[2]) >= 1:
for coord in piece[2]:
line += "%d,%d " % coord
file_fd.write("%s\n" %line)
gtk.main_quit()
@ -466,78 +288,32 @@ class pyPenNotes:
## increment foreground color - select new pen color
def fg_color_select(self, event, blub):
self.fg_color += 1
if self.fg_color >= len(COLOR_LIST):
self.fg_color = 0 ## cycle around
index = COLOR_LIST.index(self.area.get_fg_color())
index += 1
if index >= len(COLOR_LIST):
index = 0 ## cycle around
self.area.set_fg_color(COLOR_LIST[index]);
self.size_number_entry.modify_fg(gtk.STATE_NORMAL, \
self.size_number_entry.get_colormap().\
alloc_color(COLOR_LIST[self.fg_color]))
alloc_color(COLOR_LIST[index]))
## increment background color and redraw screen using the new one
## increment background color
def bg_color_select(self, event, blub):
self.bg_color += 1
if self.bg_color >= len(COLOR_LIST):
self.bg_color = 0 ## cycle around
index = COLOR_LIST.index(self.area.get_bg_color())
index += 1
if index >= len(COLOR_LIST):
index = 0 ## cycle around
self.area.set_bg_color(COLOR_LIST[index]);
## show color as fontcolor for note number
self.note_number_entry.modify_fg(gtk.STATE_NORMAL, \
self.size_number_entry.get_colormap().\
alloc_color(COLOR_LIST[self.bg_color]))
self.current_note.bg_color = self.bg_color
self.redraw()
## overdraw the screen using background color
def clear_draw_screen(self, event):
fg_backup = self.gc.foreground
self.gc.foreground = self.area.window.get_colormap()\
.alloc_color(COLOR_LIST[self.current_note.bg_color])
self.area.window.draw_rectangle(self.gc, True, 0, 0, -1, -1)
self.gc.foreground = fg_backup
alloc_color(COLOR_LIST[index]))
## trow away the content of the note
def clear_note(self, event):
self.current_note.clear()
self.current_note.bg_color = self.bg_color
self.redraw()
## redraw screen using the notes content
def redraw(self):
## step 1 - clear screen <-- will cause a flicker!
bg = self.current_note.bg_color
self.clear_draw_screen(None)
fg_backup = self.gc.foreground
self.gc.foreground = self.area.window.get_colormap().\
alloc_color(COLOR_LIST[bg])
self.gc.foreground = fg_backup
## step 2 - paint all pieces of the same color
for piece in self.current_note.image_list:
color = piece[0]
line_thickness = piece[1]
## step 3 - draw all lines of one color
if len(piece[2]) >= 1:
fg_backup = self.gc.foreground
self.gc.foreground = self.area.window.get_colormap().\
alloc_color(COLOR_LIST[color])
self.gc.set_line_attributes(line_thickness, \
gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, \
gtk.gdk.CAP_ROUND)
self.area.window.draw_lines(self.gc, piece[2])
self.gc.foreground = fg_backup
## is called when ever a redraw is needed
def area_expose_cb(self, area, event):
self.style = self.area.get_style()
self.gc = self.style.fg_gc[gtk.STATE_NORMAL]
self.redraw()
#self.gc.set_background(gtk.gdk.color_parse("#000000"))
#self.gc.set_foreground(gtk.gdk.color_parse("#FFFFFF"))
return True
self.area.clear()
def main():

View file

@ -30,5 +30,5 @@ setup(name='pyPenNotes',
url='http://wiki.openmoko.org/wiki/PyPenNotes',
version='0.3b',
license='GPL_v2',
scripts=['pyPenNotes.py'],
scripts=['pyPenNotes.py', 'SaveRestore.py', 'UserDrawingArea.py'],
)

View file

@ -1,72 +0,0 @@
"""
* write_panel.py - Project LDA - Pen_interface
*
* (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.
"""
COLOR_LIST = ["#0000FF", "#FF0000", "#00FF00", "#FFFF00", "#FFFFFF", "#000000"]
DEFAULT_BG = 5 ## Black
DEFAULT_FG = 0 ## Blue
DEFAULT_SIZE = 4 ## Diameter 4 Pixel
## A note is defined by its strokes a background and the line thickness.
## Strokes of the same color and thickness are combined in a strokes_list.
## The order of strokes will stay the same.
class PenNote:
def __init__(self):
self.bg_color = DEFAULT_BG
self.last_fg_color = 0
self.last_line_thickness = DEFAULT_SIZE
self.last_stroke = (0, 0)
self.strokes_list = [] # current list of strokes from
# the same color or thickness
# strokes are tuples of src and dest
self.image_list = [] # list of (color, thickness, strokes_list)
self.image_list.append((self.last_fg_color, self.last_line_thickness,
self.strokes_list))
def add_stroke(self, src, dest, color, line_thickness):
if (color != self.last_fg_color) or \
(self.last_line_thickness != line_thickness) \
or self.last_stroke != src:
self.strokes_list = []
self.strokes_list.append((src[0], src[1]))
self.image_list.append((color, line_thickness, self.strokes_list))
self.last_fg_color = color
self.last_line_thickness = line_thickness
self.strokes_list.append((dest[0], dest[1]))
self.last_stroke = dest
def append_point_to_stroke(self, coord):
self.strokes_list.append(coord)
def append_new_point(self, coord, color, thickness):
self.strokes_list = []
self.strokes_list.append(coord)
self.image_list.append((color, thickness, self.strokes_list))
self.last_fg_color = color
self.last_line_thickness = thickness
def clear(self):
self.__init__()