require 'cairo' require 'date' # Convert milimeter to postscript points def mm2pt(mm) (mm.to_f*360.0)/127.0 end # Convert a weekday number to a string (sunday is 0) $weekdays = ['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø'] $months = ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'] # A collection of events # You can add an event like # events = Events.new # date = Date.new(2008, 12, 31) # events[date, "New years eve") # You can now get a list of events for a given day by calling # anotherdate = Date.new(2009, 12, 31) # e = events[anotherdate] # Which will return the string from all events (separated by comma) # matching the day and month, but not the year. class Events # Internal class describing a single event class Event def initialize(date, str) @date = date @str = str end attr_accessor :date, :str end # Create a new list of events def initialize @events = [] end # Look up descriptions of events matching the day and month of the # given date. If multiple events exists for that day, they are # separated by comma def [](date) events = [] for e in @events if e.date.month == date.month and e.date.mday == date.mday events << e.str end end events.join(', ') end # Add an event to the list def []=(date, description) e = Event.new(date, description) @events << e end end # An abstraction for Cairo class GraphicsContext # Initialize the graphics context def initialize(cr) @cr = cr end # Set the color. The supported colors are :black, :white, :lightgray # and :darkgray def color=(color) case color when :black then r = g = b = 0.0 when :white then r = g = b = 1.0 when :lightgray then r = g = b = 0.8 when :darkgray then r = g = b = 0.5 else raise 'Unsupported color' end @cr.set_source_rgb(r, g, b) end # Set the linewidth. The supported sizes are :thin and :thick def line_width=(width) case width when :thin then w = 0.15 when :thick then w = 1.0 else raise 'Unsupported line width' end @cr.set_line_width(w) end # Set the font size def font_size=(size) @cr.set_font_size(size) end # Set the font options (:normal, :bold, :italic) def font_options(*options) weight = Cairo::FONT_WEIGHT_NORMAL slant = Cairo::FONT_SLANT_NORMAL for o in options case o when :bold then weight = Cairo::FONT_WEIGHT_BOLD when :italic then slant = Cairo::FONT_SLANT_ITALIC end end @cr.select_font_face('sans-serif', slant, weight) end # Draw a line def line(x1, y1, x2, y2) @cr.move_to(x1, y1) @cr.line_to(x2, y2) @cr.stroke end # Draw a rectangle def rectangle(x1, y1, x2, y2) @cr.rectangle(x1, y1, x2 - x1, y2 - y1) @cr.fill end # Display some text, and return the x-position where the following # text will be placed. def text(x, y, str, angle = 0.0) radians = angle*Math::PI/180.0 w, h, a = text_size(str) @cr.move_to(x, y) @cr.rotate(radians) @cr.show_text(str) @cr.rotate(-radians) return x + w + a end # Return the dimensions of some text def text_size(str) e = @cr.text_extents(str) return e.width, e.height, e.x_advance - e.width end # Return information about a font def font_extents e = @cr.font_extents return e.ascent, e.descent, e.height end end class Page # Draw an empty page (this will be overriden by subclasses) def draw(gc, width, height, margin_top, margin_bottom, margin_left, margin_right) end end # The front page of the calendar class FrontPage < Page # Initialize with the year to display on the front page def initialize(title, year) @title = title @year = year end def draw(gc, width, height, margin_top, margin_botom, margin_left, margin_right) # Find the position and size gc.font_size = 1 w, h, a = gc.text_size(@title + @year.to_s) gc.font_size = (width - margin_left - margin_right)/w w, h, a = gc.text_size(@title + @year.to_s) x = margin_left y = height/2.0 - h # Show the text gc.color = :black x = gc.text(x, y, @title) gc.color = :darkgray gc.text(x, y, @year.to_s) end end class NotePage < Page def draw(gc, width, height, margin_top, margin_bottom, margin_left, margin_right) line_height = (height - margin_top - margin_bottom)/32.0 # Draw the thick line in the top gc.color = :black gc.line_width = :thick x1 = margin_left x2 = width - margin_right y = margin_top + line_height gc.line(x1, y, x2, y) # Draw the rest of the lines for line in 1..32 # Draw a thin horizontal line x1 = margin_left x2 = width - margin_right y = margin_top + line*line_height gc.color = :black gc.line_width = :thin gc.line(x1, y, x2, y) end end end class MonthPage < Page def initialize(year, month, events) super() # Initialize the parent class @year = year @month = month @days = [] @events = events first = Date.new(@year, @month, 1) last = Date.new(@year, @month, -1) first.upto(last) do |date| @days << date end end @@max_date_width = nil @@max_wday_width = nil def draw(gc, width, height, margin_top, margin_bottom, margin_left, margin_right) line_height = (height - margin_top - margin_bottom)/32.0 # Draw the shaded box in weekends gc.color = :lightgray for day in @days # Only draw a box if it's sunday (0) or saturday (6) if day.wday == 0 or day.wday == 6 x1 = margin_left y1 = margin_top + (day.mday)*line_height x2 = width - margin_right y2 = y1 + line_height gc.rectangle(x1, y1, x2, y2) end end # Calculate the max width of the date gc.font_size = line_height*0.8 if @@max_date_width.nil? @@max_date_width = 0 for i in 1..31 w, h, a = gc.text_size(day.mday.to_s) @@max_date_width = w + a if w + a > @@max_date_width end end # Calculate the max width of weekdays if @@max_wday_width.nil? @@max_wday_width = 0 for d in $weekdays w, h, a = gc.text_size(' ' + d + ' ') @@max_wday_width = w + a if w + a > @@max_wday_width end end # Draw the month and the year gc.color = :black gc.line_width = :thick x1 = margin_left x2 = width - margin_right y = margin_top + line_height gc.line(x1, y, x2, y) gc.font_options(:bold) a, d, h = gc.font_extents gc.text(x1, y - d*2, $months[@month-1] + " #{@year}") # Draw the lines and dates gc.font_options(:normal) for day in @days # Draw a thin horizontal line x1 = margin_left x2 = width - margin_right y = margin_top + (day.mday + 1)*line_height gc.color = :black gc.line_width = :thin gc.line(x1, y, x2, y) # Draw the date and weekday gc.font_size = line_height*0.8 w, h, a = gc.text_size(day.mday.to_s) x = margin_left + @@max_date_width - w y = y - line_height/2.0 + h/2.0 x = gc.text(x, y, day.mday.to_s) gc.text(x , y, ' ' + $weekdays[day.wday]) # Draw any events on that day event = @events[day] if event x = margin_left + @@max_date_width + @@max_wday_width gc.font_size = line_height*0.5 gc.text(x, y, event) end # Draw the week number on mondays if day.wday == 1 text = day.cweek.to_s gc.font_size = line_height*0.8 w, h, a = gc.text_size(text) gc.color = :darkgray gc.text(width - margin_right - w, y, text) end end end end class LeftMonthPage < Page def initialize(year, month, events) super() # Initialize the parent class @year = year @month = month @days = [] @events = events first = Date.new(@year, @month, 1) last = Date.new(@year, @month, -1) first.upto(last) do |date| @days << date end end @@max_date_width = nil @@max_wday_width = nil def draw(gc, width, height, margin_top, margin_bottom, margin_left, margin_right) line_height = (height - margin_top - margin_bottom)/32.0 # Draw the shaded box in weekends gc.color = :lightgray for day in @days # Only draw a box if it's sunday (0) or saturday (6) if day.wday == 0 or day.wday == 6 x1 = margin_left y1 = margin_top + (day.mday)*line_height x2 = width - margin_right y2 = y1 + line_height gc.rectangle(x1, y1, x2, y2) end end # Calculate the max width of the date gc.font_size = line_height*0.8 if @@max_date_width.nil? @@max_date_width = 0 for i in 1..31 w, h, a = gc.text_size(day.mday.to_s) @@max_date_width = w + a if w + a > @@max_date_width end end # Calculate the max width of weekdays if @@max_wday_width.nil? @@max_wday_width = 0 for d in $weekdays w, h, a = gc.text_size(' ' + d + ' ') @@max_wday_width = w + a if w + a > @@max_wday_width end end # Draw the month and the year gc.color = :black gc.line_width = :thick x1 = margin_left x2 = width - margin_right y = margin_top + line_height gc.line(x1, y, x2, y) gc.font_options(:bold) a, d, h = gc.font_extents gc.text(x1, y - d*2, $months[@month-1] + " #{@year}") # Draw the lines and dates gc.font_options(:normal) for day in @days # Draw a thin horizontal line x1 = margin_left x2 = width - margin_right y = margin_top + (day.mday + 1)*line_height gc.color = :black gc.line_width = :thin gc.line(x1, y, x2, y) # Draw the date and weekday gc.font_size = line_height*0.8 w, h, a = gc.text_size(day.mday.to_s) x = margin_left + @@max_date_width - w y = y - line_height/2.0 + h/2.0 x = gc.text(x, y, day.mday.to_s) gc.text(x , y, ' ' + $weekdays[day.wday]) # Draw any events on that day event = @events[day] if event x = margin_left + @@max_date_width + @@max_wday_width gc.font_size = line_height*0.5 gc.text(x, y, event) end end end end class RightMonthPage < Page def initialize(year, month) super() # Initialize the parent class @year = year @month = month @days = [] first = Date.new(@year, @month, 1) last = Date.new(@year, @month, -1) first.upto(last) do |date| @days << date end end def draw(gc, width, height, margin_top, margin_bottom, margin_left, margin_right) line_height = (height - margin_top - margin_bottom)/32.0 # Draw the shaded box in weekends gc.color = :lightgray for day in @days # Only draw a box if it's sunday (0) or saturday (6) if day.wday == 0 or day.wday == 6 x1 = margin_left y1 = margin_top + (day.mday)*line_height x2 = width - margin_right y2 = y1 + line_height gc.rectangle(x1, y1, x2, y2) end end # Draw the thick line on the top gc.color = :black gc.line_width = :thick x1 = margin_left x2 = width - margin_right y = margin_top + line_height gc.line(x1, y, x2, y) # Draw the lines and dates gc.font_options(:normal) for day in @days # Draw a thin horizontal line x1 = margin_left x2 = width - margin_right y = margin_top + (day.mday + 1)*line_height gc.color = :black gc.line_width = :thin gc.line(x1, y, x2, y) # Draw the week number on mondays if day.wday == 1 text = day.cweek.to_s gc.font_size = line_height*0.8 w, h, a = gc.text_size(text) gc.color = :darkgray gc.text(width - margin_right - 1.5*w, y, text) end end # Draw the tab with the month name to the right h = height/12.0 x1 = width - (2.0/3.0)*margin_right y1 = h*(@month - 1) x2 = width y2 = y1 + h if @month%2 == 1 gc.color = :darkgray else gc.color = :white end gc.rectangle(x1, y1, x2, y2) gc.color = :black gc.line_width = :thin gc.line(x1, y2, x1, height) text = $months[@month - 1][0..2] gc.color = :black gc.font_size = (1.0/3.0)*margin_right tw, th, a = gc.text_size(text) x = x1 + (2.0/3.0)*margin_right/2.0 - th/2.0 y = y1 + h/2.0 - tw/2.0 gc.text(x, y, text, 90.0) end end # A booklet with 8 pages printed on one A4 paper class Booklet # Create a new booklet with the specified dimensions (in millimeter) def initialize(width, height, margin_top, margin_bottom, margin_inner, margin_outer) @w = width @h = height @mt = margin_top @mb = margin_bottom @mi = margin_inner @mo = margin_outer @pages = [] end # Add a page to the booklet def add(page) @pages << page end # Save to booklet to a file def save(filename) # Reorder the pages pages = reorder # Calculate the center of the A4 paper cx = mm2pt(210)/2.0 + mm2pt(1) cy = mm2pt(297)/2.0 # Create a pdf surface with the size of an A4 paper pdf = Cairo::PDFSurface.new(filename, mm2pt(210.0), mm2pt(297.0)) cr = Cairo::Context.new(pdf) gc = GraphicsContext.new(cr) cr.translate(cx, cy) for n in 0..(pages.size/8)-1 # Draw page n, n + 1, n + 4 and n + 5 on this page cr.save cr.translate(-@w, -@h) if n == 0 # If it is the back page we only use outer margins pages[8*n + 0].draw(gc, @w, @h, @mt, @mb, @mo, @mo) else pages[8*n + 0].draw(gc, @w, @h, @mt, @mb, @mo, @mi) end cr.translate(@w, 0) if n == 0 # If it is the front page we only use outer margins pages[8*n + 1].draw(gc, @w, @h, @mt, @mb, @mo, @mo) else pages[8*n + 1].draw(gc, @w, @h, @mt, @mb, @mi, @mo) end cr.translate(-@w, @h) pages[8*n + 4].draw(gc, @w, @h, @mt, @mb, @mo, @mi) cr.translate(@w, 0) pages[8*n + 5].draw(gc, @w, @h, @mt, @mb, @mi, @mo) cr.restore cr.set_line_width(0.15) cr.move_to(-@w - 10, 0) cr.line_to(+@w + 10, 0) cr.move_to(-@w - 10, -@h) cr.line_to(+@w + 10, -@h) cr.move_to(-@w - 10, @h) cr.line_to(+@w + 10, @h) cr.move_to(-@w, -@h - 10) cr.line_to(-@w, +@h + 10) cr.move_to(+@w, -@h - 10) cr.line_to(+@w, +@h + 10) cr.stroke cr.show_page # Draw page n + 2, n + 3, n + 6 and n + 7 on the next page # (the backside of the first page with a duplex printer) cr.save cr.translate(-@w, -@h) pages[8*n + 2].draw(gc, @w, @h, @mt, @mb, @mo, @mi) cr.translate(@w, 0) pages[8*n + 3].draw(gc, @w, @h, @mt, @mb, @mi, @mo) cr.translate(-@w, @h) pages[8*n + 6].draw(gc, @w, @h, @mt, @mb, @mo, @mi) cr.translate(@w, 0) pages[8*n + 7].draw(gc, @w, @h, @mt, @mb, @mi, @mo) cr.restore cr.set_line_width(0.5) cr.move_to(-@w - 10, 0) cr.line_to(+@w + 10, 0) cr.move_to(-@w - 10, -@h) cr.line_to(+@w + 10, -@h) cr.move_to(-@w - 10, @h) cr.line_to(+@w + 10, @h) cr.move_to(-@w, -@h - 10) cr.line_to(-@w, +@h + 10) cr.move_to(+@w, -@h - 10) cr.line_to(+@w, +@h + 10) cr.stroke cr.show_page end end private # Add blank pages to the end, so the number of pages is divisible # with n. The number of pages added is returned def add_blanks(n) blank_pages = @pages.size%n blank_pages = n - blank_pages unless blank_pages == 0 blank_pages.times { add(Page.new) } return blank_pages end # Return an array of reordered pages for the booklet def reorder # Add blank pages blanks = add_blanks(8) # Reorder the pages pages = [] first = 0 last = @pages.size - 1 while first < last pages << @pages[last] pages << @pages[first] pages << @pages[first + 1] pages << @pages[last - 1] first += 2 last -= 2 end # Remove the blank pages again blanks.times { @pages.pop } # Return the reordered pages return pages end end