First Commit
[orfont.git] / font_db.rb
blob30c3f750a8045ae8d6b6384e0bea3dbcfb19b600
1 #!/usr/bin/ruby
3 # == Synopsis
4 # Create and access a database of fonts.
6 # == Usage
7 # ruby font_db.rb
9 # ==Author
10 # Tim Dresser
12 require 'singleton'
13 require 'sqlite3'
14 require 'font.rb'
16 # Create and access a database of fonts.
17 # The instance is accessed using FontDB.instance()
19 class FontDB
20   # Ensures that only one can exist.
21   include Singleton
23   # Constants for use in a few functions.
25   # FIXME: 
26   # Where should they go?
27   UP = true
28   DOWN = false
29   NO_LIMIT = -1
31   # FIXME: 
32   # Get this stuff from a textfile or something.
33   FONT_DIRECTORIES = ['/usr/share/fonts','C:\WINDOWS\Fonts']
34   FONT_TYPES = ["ttf","otf"]
35   
36   # Add these tags based on font metadata.
37   AUTO_TAGS = %w(bold serif italic sans-serif 
38                 sans\ serif hairline modern slab\ serif script)
40   # The associated data cannot coexist. The right side is removed.
41   AUTO_TAGS_EXCLUSIVE = {"sans-serif" => "serif", "sans serif" => "serif"}
43   # The associated data should be combined. Left side becomes right side.
44   AUTO_TAGS_COMBINE = {"sans-serif" => "sans serif"}
46   # Columns to search through for tags.
47   AUTO_TAGS_COLUMNS= %w(family subfamily name description)
49   def initialize()
50     @db = SQLite3::Database.new('database.db')
51   end
53   # Gets data out of the database.
54   # For example
55   #
56   # FontDB.instance.get("name","id",DOWN, 5, 10)
57   #
58   # will display the names, sorted by decreasing ID starting at #10, and 
59   # going to number 15
60   #
61   def get(columns, order_by='name', sort_dir = UP, limit = NO_LIMIT, offset = 0)
62     if sort_dir = UP then
63       dir = "ASC"
64     else
65       dir = "DESC"
66     end
67     return @db.execute(%{
68       SELECT #{[columns].flatten.join(", ")} 
69       FROM fonts 
70       ORDER BY #{order_by} #{dir} 
71       LIMIT #{limit} 
72       OFFSET #{offset}
73     })
74   end
76   # Creates the required tables.
77   #
78   def create_tables()
79     if !FontDB.instance.tables_exist? then
80       @db.execute_batch(%{
81         CREATE TABLE fonts 
82           (
83             id              INTEGER PRIMARY KEY,
84             path            VARCHAR(255) UNIQUE ON CONFLICT IGNORE,
85             rating          INTEGER,
86             copyright       VARCHAR(2048),
87             family          VARCHAR(255),
88             subfamily       VARCHAR(255),
89             name            VARCHAR(255),
90             foundry         VARCHAR(255),
91             description     VARCHAR(2048),
92             license         VARCHAR(255),
93             defaulttext     VARCHAR(255)
94           );
96         CREATE TABLE tags
97           (
98             id            INTEGER PRIMARY KEY,
99             name      VARCHAR(255) UNIQUE ON CONFLICT IGNORE
100           );
102         CREATE TABLE tags_fonts
103           (
104             tag_id        INTEGER,
105             font_id       INTEGER,
106             PRIMARY KEY (tag_id, font_id) ON CONFLICT IGNORE
107             
108           );
110         CREATE TABLE sets
111           (
112             id            INTEGER PRIMARY KEY,
113             name      VARCHAR(255) UNIQUE ON CONFLICT IGNORE
114           );
116         CREATE TABLE sets_fonts
117           (
118             set_id INTEGER,
119             font_id INTEGER,
120             PRIMARY KEY (set_id, font_id) ON CONFLICT IGNORE
121           );
122         })
123       return true
124     else
125       return false
126     end
127   end
129   # Removes all tables from the database.
130   #
131   def drop_tables()
132     if FontDB.instance.tables_exist? then
133       @db.execute_batch(%{
134         DROP TABLE fonts;
135         DROP TABLE tags;
136         DROP TABLE tags_fonts;
137         DROP TABLE sets;
138         DROP TABLE sets_fonts;
139       })
140       return true
141     else
142       return false
143     end
144   end
146   # Adds a font to the database.
147   # FIXME:
148   # This should auto_tag_font.
149   #
150   def add_font(path)
151     font_data = [path] + Font.meta(path)
152     @db.execute(%{
153       INSERT INTO fonts 
154       (
155         path, copyright, family, subfamily,
156         name, foundry, description, license, defaulttext
157       ) 
158       VALUES (
159         '#{font_data.collect{|x|x.to_s}.join(%{','})}'
160       )
161     })
162   end
164   # Populates the table based on system directories.
165   #
166   def populate()
167     Font.find(FONT_DIRECTORIES, FONT_TYPES).each do |path|
168       add_font(path)
169     end
170   end
172   # FIXME:: add in sets.
173   #
174   def to_s
175     return @db.execute(%{
176       SELECT * FROM fonts
177     }).collect do |meta|
178        ("-------")+"\n" + meta.join("\n") + "\n" + read_tags(meta[0]).join(", ")
179     end.join("\n")
180   end
182   # Has create_tables been called?
183   #
184   def tables_exist?
185     return !@db.execute(%{
186       SELECT name FROM sqlite_master WHERE type = 'table'
187     }).empty?
188   end
189   
190   private
192   # A set of functions for acting on sets or tags.
193   
194   # Add an array of groups to an array of font ids.
195   #
196   def add_groups(font_ids, groups, type)
197     [groups].flatten.each do |group|
198       # Add the group to the global table of groups.
199       @db.execute(%{
200         INSERT INTO #{type}s (name)
201         VALUES ('#{group}')
202       })
203       [font_ids].flatten.each do |font_id|
204         # Add an association between the font and the group.
205         @db.execute(%{
206           INSERT INTO #{type}s_fonts 
207           (
208             #{type}_id, 
209             font_id
210           )
211           VALUES 
212           (
213             (SELECT id FROM #{type}s WHERE name = '#{group}'),
214             #{font_id}
215           )
216         })
217       end
218     end
219   end
221   # Removes groups from the given fonts.
222   #
223   def remove_groups(font_ids, groups, type)
224     [groups].flatten.each do |group|
225       [font_ids].flatten.each do |font_id|
226         @db.execute(%{
227           DELETE FROM #{type}s_fonts
228           WHERE font_id = '#{font_id}'
229           AND #{type}_id = 
230           (
231             SELECT id FROM #{type}s  
232             WHERE name = '#{group}'
233           )
234         })
235       end
236     end
237   end 
239   # Combines groups named old_names into one named new_name.
240   #
241   def combine_groups(new_name, old_names, type)
242     [old_names].flatten.each do |old_name|
243       ids = tag_members(old_name)      
244       remove_tags(ids, old_names)
245       add_tags(ids, new_name)
246     end
247   end
249   # Returns all groups associated with a font ID.
250   #
251   def read_groups(font_id, type)
252     return @db.execute(%{
253       SELECT name FROM #{type}s
254       INNER JOIN #{type}s_fonts
255       ON #{type}s.id = #{type}s_fonts.tag_id
256       WHERE #{type}s_fonts.font_id = '#{font_id}'
257     })
258   end
260   # Returns the font IDs of all members of group.
261   #
262   def group_members(group, type)
263     return @db.execute(%{
264       SELECT font_id FROM #{type}s_fonts
265       INNER JOIN #{type}s
266       ON #{type}s_fonts.tag_id = #{type}s.id
267       WHERE #{type}s.name = '#{group}'
268     })
269   end
270   
271   # Returns a list of all groups.
272   #
273   def groups(type)
274     return @db.execute(%{
275       SELECT name FROM #{type}s
276     })
277   end
279   public 
281   # Adds the tags to the given fonts.
282   #
283   def add_tags(font_ids, tags)
284     add_groups(font_ids, tags, "tag")
285   end
287   # Adds the sets to the given fonts.
288   #
289   def add_sets(font_ids, set)
290     add_groups(font_ids, tags, "set")
291   end
293   # Removes the tags from the given fonts.
294   #
295   def remove_tags(font_ids, tags)
296     remove_groups(font_ids, tags, "tag")
297   end
299   # Removes the sets from the given fonts.
300   #
301   def remove_sets(font_ids, tags)
302     remove_groups(font_ids, tags, "set")
303   end
304   
305   # Changes all old_tags to new_tag.
306   #
307   def combine_tags(new_tag, *old_tags)
308     combine_groups(new_tag, old_tags, "tag")
309   end
310   
311   # Changes all old_sets to new_set.
312   #
313   def combine_sets(new_set, *old_sets)
314     combine_groups(new_set, old_sets, "set")
315   end
317   # Lists all of the given font's tags.
318   #
319   def read_tags(font_id)
320     read_groups(font_id, "tag")
321   end
323   # Lists all of the given font's sets.
324   #
325   def read_sets(font_id)
326     read_groups(font_id, "set")
327   end
329   # Lists all of the tag's members.
330   #
331   def tag_members(tag_name)
332     group_members(tag_name, "tag")
333   end
334   
335   # Lists all of the set's members.
336   #
337   def set_members(set_name)
338     group_members(set_name, "set")
339   end
340   
341   # Lists all tags.
342   def tags()
343     groups("tag")
344   end
346   # Lists all sets.
347   def sets()
348     groups("set")
349   end
351   # Automatically tags all fonts.
352   # FIXME: should constants be in here?
353   #
354   def auto_tag(auto_tags, auto_tags_exclusive, 
355                     auto_tags_combine, auto_tags_search_columns)
356     auto_tags.each do |tag|
357       # Search through all columns given for tags in the auto_tag list.
358       all_columns = auto_tags_search_columns.join(" LIKE '%#{tag}%' OR ")
359       ids = @db.execute(%{
360         SELECT id FROM fonts
361         WHERE #{all_columns} 
362         LIKE '%#{tag}%'
363       })
364       add_tags(ids, tag)
365     end
366     
367     auto_tags_combine.each_key do |tag_keep|
368       combine_tags(tag_keep, auto_tags_combine[tag_keep])
369     end
370     
371     auto_tags_exclusive.each_key do |tag_keep|
372       ids = tag_members(tag_keep) & tag_members(auto_tags_exclusive[tag_keep])
373       remove_tags(ids, (auto_tags_exclusive[tag_keep]))
374     end
375   end
377   # auto_tag one font.
378   #
379   def auto_tag_font(id, auto_tags, auto_tags_exclusive, 
380                     auto_tags_combine, auto_tags_search_columns)
381     tags=[]
382     auto_tags.each do |tag|
383       # Search through all columns given for tags in the auto_tag list
384       all_columns = auto_tags_search_columns.join(" LIKE '%#{tag}%' OR ")
385       if !@db.execute(%{
386         SELECT id FROM fonts 
387         WHERE (id = #{id}) 
388         AND (#{all_columns} LIKE '%#{tag}%')
389       }).empty? then
390         tags.push(tag)
391       end
392       add_tags(id, tags)
393     end
394    
395     # FIXME:
396     # This shouldn't be global. 
397     auto_tags_combine.each_key do |tag_keep|
398       combine_tags(tag_keep, auto_tags_combine[tag_keep])
399     end
400     
401     auto_tags_exclusive.each_key do |tag_keep|
402       ids = tag_members(tag_keep) & tag_members(auto_tags_exclusive[tag_keep])
403       remove_tags(ids, (auto_tags_exclusive[tag_keep]))
404     end
405   end
408 db = FontDB.instance
409 # FIXME:
410 # Should be a transaction to do all starting stuff.
411 db.create_tables
412 # FIXME? 
413 # Should it do a full scan every time?
414 db.populate
415 db.auto_tag(FontDB::AUTO_TAGS, FontDB::AUTO_TAGS_EXCLUSIVE, 
416                       FontDB::AUTO_TAGS_COMBINE, FontDB::AUTO_TAGS_COLUMNS)
417 puts db