MacRuby는 Laurent Sansonetti란 프랑스 출신 프로그래머가 애플에서 거의 혼자 작업하고 있는데 이번 버전에서는 ruby 1.9에서 사용하는 YARV란 가상 머신 대신에 LLVM이란 오픈소스 가상 머신을 바탕으로 돌아가는데 이 때문에 이제는 ruby 프로그램을 컴파일 하는 것이 가능해졌습니다! 따라서 소스 프로그램을 보이지 않고도 빨리 실행될 수 있는 프로그램을 만들수 있게 되겠습니다.
내부에 cocoa의 객체들이 포함되어 있기 때문에 macruby의 성과를 바로 ruby쪽에서 가져오는 것은 어려울지도 모르겠지만 mac 이외의 운영체계에서 사용할 수 있는 ruby의 변종이 또 만들어질지도 모르겠네요.
Ruby와 cocoa를 동시에 사용할 수 있으면 무척 재밌을것 같은데 일상의 직장생활에 파묻혀 프로그래밍과는 점점 멀어지고 있는 요즘입니다.
어떤 툴이거나 보통 thumbnail은 원본 파일을 읽어들이고 작은 크기의 thumbnail 이미지를 만든 다음 원본 파일을 thumbnail에 그려넣고 저장하는 순서로 이루어집니다. OSX에서 thumbnail을 만드는 방법은 크게 Cocoa의 NSImage를 사용하는 것과 Quartz의 CoreImage를 사용하는 방법이 있습니다. NSImage가 더 나중의 방법이고 내부적으로는 아마 core image를 사용하는 것 같습니다.
먼저 원본이미지를 읽어들입니다.
img = NSImage.alloc.initWithContentsOfFile("파일이름")
이제 작은 크기의 thumbnail을 만듭니다. 엄청나게 긴 함수이름을 가지고 있습니다.
:pixelsWide=>200.0, :pixelsHigh=>150.0,
:bitsPerSample=>8, :samplesPerPixel=>4, :hasAlpha=>true, :isPlanar=>false,
:colorSpaceName=>NSDeviceRGBColorSpace, :bytesPerRow=>0, :bitsPerPixel=>0)
다음 2행은 graphics context를 thumbnail의 것으로 만들고 원본 이미지를 thumbnail 크기로 그려넣습니다. Graphics context를 thumbnail의 것으로 지정하면 image에서의 그림 그리기가 thumbnail에 이루어집니다.
img.drawInRect([0.0, 0.0, 200.0, 150.0], :fromRect=>NSZeroRect, :operation=>NSCompositeCopy, :fraction=>1.0)
이제 그려진 thumbnail을 파일로 저장합시다.
jpg.writeToFile("썸네일 파일이름", :atomically=>false)
다음은 그 결과물입니다.
그림이 작아지기는 했습니다만 영 거칠고 화질이 좋지 않습니다. 건물벽도 이상하게 보이네요.
이미지의 크기를 확인해 봅시다.
=> #<NSSize width=836.484069824219 height=627.363037109375>
원본은 3648, 2736 크기의 사진입니다. Cocoa에서는 픽셀단위를 사용하지 않고 출력 device, window, view에 따라 다른 단위를 사용합니다. 우리는 비트맵 단위로 다루고 싶으므로 다음과 같은 방법을 써서 크기를 원본으로 맞추어 줍니다.
img.setSize([img_bitmap.pixelsWide, img_bitmap.pixelsHigh])
이후 결과물은 다음과 같습니다.
거의 차이가 없네요. 그림을 그릴때 화질을 조절하는 옵션이 있습니다. 속도는 조금 느리더라도 더 좋은 화질을 얻도록 합시다.
마지막 결과물...
이제야 볼만한 그림이 나온것 같네요.
NSBitmapImageRep 인스턴스의 메소드 representationUsingType은 JPEG이외에 TIFF, BMP, GIF, PNG 그리고 JPEG2000까지 지원해줍니다. 그리고 properties 인자에 이미지의 옵션을 지정해 줄 수 있습니다. 예를 들어 JPEG의 화질을 95%로 하고 싶다면 인자로 { NSImageCompressionFactor => 0.95 }를 주면 됩니다.
다음은 전체 소스입니다.
NSGraphicsContext.saveGraphicsState
img = NSImage.alloc.initWithContentsOfFile("test.jpg")
img_bitmap = img.bestRepresentationForDevice(nil)
img.setSize([img_bitmap.pixelsWide, img_bitmap.pixelsHigh]);
thumbrect = [0.0, 0.0, 200.0, 150.0]
thumb = NSBitmapImageRep.alloc.initWithBitmapDataPlanes(nil,
:pixelsWide=>thumbrect[2], :pixelsHigh=>thumbrect[3],
:bitsPerSample=>8, :samplesPerPixel=>4, :hasAlpha=>true, :isPlanar=>false,
:colorSpaceName=>NSDeviceRGBColorSpace, :bytesPerRow=>0, :bitsPerPixel=>0)
NSGraphicsContext.setCurrentContext(NSGraphicsContext.graphicsContextWithBitmapImageRep(thumb))
NSGraphicsContext.currentContext.setImageInterpolation(NSImageInterpolationHigh)
img.drawInRect(thumbrect, :fromRect=>NSZeroRect, :operation=>NSCompositeCopy, :fraction=>1.0)
jpg = thumb.representationUsingType(NSJPEGFileType, :properties=>nil)
jpg.writeToFile("test_200.jpg", :atomically=>false)
NSGraphicsContext.restoreGraphicsState
end
다음은 그 결과물... 태국 모래사장에서 발견한 꼬마게인데 모래로 열심히 작은 공을 만들고 있었습니다. 카메라로 연속 촬영한 것을 일부분 잘라서 만들어 보았습니다. 2MB(헉!)가 조금 넘는 용량이지만 원래 색 정보가 많지 않아서인지 생각보다 잘 나온것 같네요.
엉망인 소스는 정말 옛날스럽게 클래스 정의도 없이 함수2개 호출하고 gets함수를 써서사용자 입력받고 사용자 입력 오류검사도 없습니다. 그냥 읽어보시면 이해가 되리라 생각하고 백업겸 올려놓습니다. 현재 프로그램은 P1234567.JPG 형식의 파일이라고 가정하고 있으니 필요에 따라 고쳐쓰시고... (엉?!) 아니면 그냥 변환할 파일 이름을 Array에 담아서 process 함수에 넘겨주시면 될겁니다.
gifmaker.rb |
다음은 각 라이브러리를 받은 곳의 주소입니다.
- FreeType : http://download.savannah.gnu.org/releases/freetype/
- libpng : http://sourceforge.net/project/showfiles.php?group_id=5624
- libjpeg : ftp://ftp.uu.net/graphics/jpeg/jpegsrc.v6b.tar.gz
make전 libtool, MACOSX_DEPLOYMENT_TARGET 잡아줘야 하고 install시 manual 설치되지 않는다고 오류발생하지만 설치에는 지장없습니다
- libtiff : ftp://ftp.remotesensing.org/libtiff/
- ghostscript fonts : http://sourceforge.net/projects/gs-fonts/
- ImageMagick : ftp://ftp.imagemagick.org/pub/ImageMagick/
CPPFLAGS, LGFLAGS 설정해야 합니다
ImageMagick, Rmagick, gruff까지 한번에 시험해 보기 위해 gruff의 홈페이지에 있는 간단한 프로그램을 다음과 같이 실행시켜 봅니다.
require 'rubygems'결과는
require 'gruff'
g = Gruff::Line.new
g.title = 'Gruff 시험'
g.data("사과", [1,2,3,4,4,3])
g.data("오렌지", [4,8,7,9,8,9])
g.data("수박", [2,3,1,5,6,8])
g.data("복숭아", [9,9,10,8,7,9])
g.labels = { 0=>'2006', 2=>'2007', 4=>'2008'}
g.font = "/System/Library/Fonts/AppleGothic.ttf"
g.write('gruff_test_result.png')
실제 움직이는 GIF 만들기는 다음에 도전...
- XCode 실행시킨 후 New Project에서 User Templates 밑의 MacRuby Application을 선택합니다. 이름은 임의로 rubyGL이라고 주었습니다.
- Classes 아래에 Add > New File을 선택하고 Other > Empty File을 선택해 myopengl.rb란 이름을 줍니다.
- 다음과 같이 코드를 입력합니다.
- 프로젝트를 저장합니다.
- MainMenu.nib를 더블클릭하여 interface builder를 실행시킵니다.
- 윈도우 위에 openGL View를 놓고 크기를 적절하게 조절합니다.
- openGL view의 class를 MyOpenGLView로 지정합니다.
- Interface builder에서 interface를 저장합니다.
- Xcode에서 Build and Go를 실행합니다.
지난번에 말씀드린 Holux M-241…
시간 간격(1, 5, 10, 15, 30, 60, 120초) 혹은 지정 거리(50, 100, 150, 300, 500, 1000m)별로 위치를 저장하고 총 13만개정도의 위치를 저장할 수 있으니 36시간 400초(13만초) – 180일 13시간 20분(15600000초) 정도의 시간, 혹은 6500Km – 130000Km의 거리를 저장할 수 있는 셈입니다.
BT747 프로그램을 사용하여 맥에서 로그 데이터를 받아 구글 어스에서 받는 것까지는 성공했지만 막상 전체 거리, 속도 등의 자료를 볼 수 없어서 이를 계산하는 프로그램을 짜 보았습니다. 프로그램은 XML의 형식을 가지는 KML 파일을 읽어들여 저장되어 있는 위도, 경도와 시간을 사용하여 이동거리 및 속도를 기록하고 이것을 기초로 간단한 그래프를 보여줍니다.
프로그램은 GPS 자료의 위도, 경도 차이를 기준으로 거리를 계산합니다. 위도, 경도를 계산하는 방법은 Haversine 방법과 Vincenty 방법 이 알려져 있는데 Vincenty 방법은 지구를 타원으로 측정하여 1m 정도까지 정확하게 계산할 수 있는 모양입니다. 집에서 직장까지의 거리를 Vincenty 방법과 Haversine 방법으로 계산한 결과는 각각 9.212Km, 9.206Km로 대략 60m 정도 차이가 나지만 어차피 GPS가 크게 정확하지 않으리라 생각하고 Haversine 방법으로 거리를 계산했습니다.
다음은 지난 금요일 자전거를 타고 출퇴근하면서 10초 간격으로 GPS를 기록한 것을 그래프로 나타낸 것입니다. 퇴근때는 GPS를 켜자마자 바로 달려서 초반 부분이 기록되어 있지 않습니다.
전체 프로그램의 소스입니다. 그래프는 rubyCocoa를 이용했기 때문에 Leopard이상의 OSX에서 실행해야 합니다. kml파일 목록을 인자로 실행시키면 기본적으로 GPS 기록이 1시간 이상 차이날 때마다 새로운 그래프를 만들어줍니다. KML 파일 이외의 형식도 마지막 부분의 소스를 조금만 수정하면 사용할 수 있도록 만들어져 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
require 'rexml/document' require 'time' require 'osx/cocoa' # require 'rubygems' # require 'units' # great resources about distance calculations on web # http://williams.best.vwh.net/avform.htm # http://www.movable-type.co.uk/scripts/latlong.html # http://www.movable-type.co.uk/scripts/latlong-vincenty.html # http://ajax.suaccess.org/rubyisms-in-rails/converting-between-degrees-and-radians/ include Math include REXML SUMMARY_KO = { :dist=>"전체 거리 : %.2f Km", :time=>"시간 : %d분 %d초", :max_v=>"최고 속도 : %.2f Km/시", :avg_v=>"평균 속도 : %.2f Km/시" } SUMMARY_EN = { :dist=>"Total Distance : %.2f Km", :time=>"Time : %d:%d", :max_v=>"Max Velocity : %.2f Km/hr", :avg_v=>"Avg Velocity : %.2f Km/hr" } SUMMARY = SUMMARY_KO class Numeric def to_rad self*Math::PI/180 end # add_unit_conversions(:angle => { :radians => 1, :degrees => Math::PI/180 }) # add_unit_aliases(:angle => { :degrees => [:degree], :radians => [:radian] }) end def distVincenty(lat1, lon1, lat2, lon2) a, b = 6378137.0, 6356752.3142 f = 1/298.257223563; # WGS-84 ellipsiod l = (lon2-lon1).to_rad u1 = atan((1-f) * tan(lat1.to_rad)) u2 = atan((1-f) * tan(lat2.to_rad)) sinU1, cosU1 = sin(u1), cos(u1) sinU2, cosU2 = sin(u2), cos(u2) lambda, lambdaP = l, 2*Math::PI iterLimit = 19 while ((lambda-lambdaP).abs > 1e-12 and iterLimit>0) do sinLambda, cosLambda = sin(lambda), cos(lambda) sinSigma = sqrt((cosU2*sinLambda) ** 2 + (cosU1*sinU2-sinU1*cosU2*cosLambda) ** 2) return 0 if (sinSigma==0) # co-incident points cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda sigma = atan2(sinSigma, cosSigma) sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma cosSqAlpha = 1 - sinAlpha ** 2 cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha # if (isNaN(cos2SigmaM)) cos2SigmaM = 0 # equatorial line: cosSqAlpha=0 (§6) c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha)) lambdaP = lambda lambda = l + (1-c) * f * sinAlpha * \ (sigma + c*sinSigma*(cos2SigmaM+c*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM))) iterLimit -= 1 end return nil if (iterLimit==0) # formula failed to converge uSq = cosSqAlpha * (a*a - b*b) / (b*b) biga = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq))) bigb = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq))) deltaSigma = bigb*sinSigma*(cos2SigmaM+bigb/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM) - \ bigb/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM))) b*biga*(sigma-deltaSigma) end def distHaversine(lat1, lon1, lat2, lon2) # R = 6371 dLat = (lat2-lat1).to_rad dLon = (lon2-lon1).to_rad a = sin(dLat/2) * sin(dLat/2) + cos(lat1.to_rad) * cos(lat2.to_rad) * sin(dLon/2)**2 c = 2 * atan2(sqrt(a), sqrt(1-a)) 6371 * c * 1000; end def velocity(dist, t_int) (dist / 1000) / t_int * 3600 end def gps_calc(doc, path, loc_f, h_f, t_f, cut_interval = 60 * 5) result = [] rhash = { :data => [] } pre_lat, pre_lon, pre_h, pre_t = 0, 0, 0, 0 start_time, total_dist, total_points, time_int, max_v = nil, 0, 0, 0, 0 doc.elements.each(path) do |p| lat, lon = loc_f.call(p) h = h_f.call(p) t = t_f.call(p) start_time = t if start_time.nil? if pre_lat != 0 then time_int = t - pre_t if time_int <= cut_interval then dist = distHaversine(lat, lon, pre_lat, pre_lon) total_dist += dist total_points += 1 v = velocity(dist, time_int) max_v = v if v > max_v rhash[:data] << [t - start_time, dist, v] end end if time_int > cut_interval then avg_v = velocity(total_dist, pre_t - start_time) rhash[:start_time] = start_time rhash[:total_points]= total_points rhash[:total_dist] = total_dist / 1000 rhash[:time_int] = pre_t - start_time rhash[:avg_v] = avg_v rhash[:max_v] = max_v result << rhash rhash = { :data => [] } total_dist, total_points, max_v, time_int = 0, 0, 0, 0 pre_lat, pre_lon, start_time = 0, 0, nil else pre_lat, pre_lon, pre_h, pre_t = lat, lon, h, t end end if pre_lat != 0 then avg_v = velocity(total_dist, pre_t - start_time) rhash[:start_time] = start_time rhash[:total_points]= total_points rhash[:total_dist] = total_dist / 1000 rhash[:time_int] = pre_t - start_time rhash[:avg_v] = avg_v rhash[:max_v] = max_v result << rhash end result end def mark_x(gps_data, value, para, emphasis=false) position = value * para[:graph_width] / gps_data[:total_dist] + para[:margin] vstring = value.integer? ? value.to_s : "%.2f" % value label = OSX::NSString.alloc.initWithString(vstring) font_dict = emphasis ? para[:em_font_dict] : para[:font_dict] size = label.sizeWithAttributes(font_dict) label.drawAtPoint_withAttributes([position-size.width/2, para[:zero].y-size.height-para[:font_margin]], font_dict) OSX::NSRectFill([position, para[:zero].y-para[:font_margin], 1, para[:font_margin]]) end def mark_y(gps_data, value, para, emphasis=false) position = value * para[:graph_height] / (gps_data[:max_v] * 1) + para[:margin] vstring = value.integer? ? value.to_s : "%.2f" % value label = OSX::NSString.alloc.initWithString(vstring) font_dict = emphasis ? para[:em_font_dict] : para[:font_dict] size = label.sizeWithAttributes(font_dict) label.drawAtPoint_withAttributes([para[:margin]-size.width-para[:font_margin], position-size.height/2], font_dict) OSX::NSRectFill([para[:zero].x-para[:font_margin], position, para[:font_margin], 1]) end def make_graph(gps_data, para={}) default = { :width=>700, :height=>500, :mark_int=>60*10, :format=>OSX::NSPNGFileType, :margin=>50, :font_size=>12, :font_margin=>3 } dist_scales = [[1, 2, 5, 10, 20, 50, 100], [5, 10, 20, 50, 100, 200, 1000]] default.update(para) canvas = OSX::NSBitmapImageRep.alloc.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(nil, default[:width], default[:height], 8, 4, true, false, OSX::NSDeviceRGBColorSpace, 0, 0) context = OSX::NSGraphicsContext.graphicsContextWithBitmapImageRep(canvas) OSX::NSGraphicsContext.setCurrentContext(context) # font white = OSX::NSColor.whiteColor white.set yellow = OSX::NSColor.yellowColor font = OSX::NSFont.fontWithName_size('Helvetica', default[:font_size]) font_dict = OSX::NSMutableDictionary.alloc.init font_dict.setObject_forKey(font, OSX::NSFontAttributeName) font_dict.setObject_forKey(white, OSX::NSForegroundColorAttributeName) default[:font_dict] = font_dict em_font = OSX::NSFont.boldSystemFontOfSize(default[:font_size]+2) em_font_dict = OSX::NSMutableDictionary.alloc.init em_font_dict.setObject_forKey(em_font, OSX::NSFontAttributeName) em_font_dict.setObject_forKey(OSX::NSColor.yellowColor, OSX::NSForegroundColorAttributeName) default[:em_font_dict] = em_font_dict # background gradient gradient = OSX::NSGradient.alloc.initWithStartingColor_endingColor(OSX::NSColor.blueColor, OSX::NSColor.blackColor) gradient.drawInRect_angle([0, 0, default[:width], default[:height]], 90) # lines default[:zero] = OSX::NSMakePoint(default[:margin], default[:margin]) default[:x_end] = OSX::NSMakePoint(default[:width]-default[:margin], default[:margin]) default[:y_end] = OSX::NSMakePoint(default[:margin], default[:height]-default[:margin]) path = OSX::NSBezierPath.bezierPath path.moveToPoint(default[:x_end]) path.lineToPoint(default[:zero]) path.lineToPoint(default[:y_end]) path.stroke # labels # dist_label = OSX::NSString.alloc.initWithString('Km') # dist_size = dist_label.sizeWithAttributes(font_dict) # dist_label.drawAtPoint_withAttributes([default[:x_end].x+default[:font_margin], default[:x_end].y-dist_size.height-default[:font_margin]], font_dict) # velo_label = OSX::NSString.alloc.initWithString('Km/Hr') # velo_size = velo_label.sizeWithAttributes(font_dict) # velo_label.drawAtPoint_withAttributes([default[:y_end].x-velo_size.width-default[:font_margin], default[:y_end].y+default[:font_margin]], font_dict) # default[:graph_width] = default[:width] - default[:margin] * 2 default[:graph_height] = default[:height] - default[:margin] * 2 # determine x scale units scale_index = 0 (dist_scales[1].size-1).downto(0) { |i| scale_index=i; break if gps_data[:total_dist] > dist_scales[1][i] } scale = dist_scales[0][scale_index] # draw x legends # mark_x(gps_data, gps_data[:total_dist], default, true) dist = scale while (dist < gps_data[:total_dist]) do mark_x(gps_data, dist, default) dist += scale end # mark_y(gps_data, gps_data[:max_v], default) velo = 10 while (velo < gps_data[:max_v]) do mark_y(gps_data, velo, default) velo += 10 end # draw graph graph = nil total = 0 gps_data[:data].each do |time_int, dist, velo| total += dist point_x = (total / 1000) * default[:graph_width] / gps_data[:total_dist] + default[:margin] point_y = velo * default[:graph_height] / (gps_data[:max_v] * 1) + default[:margin] if graph.nil? then graph = OSX::NSBezierPath.bezierPath graph.moveToPoint(OSX::NSMakePoint(point_x, point_y)) else graph.lineToPoint(OSX::NSMakePoint(point_x, point_y)) end if (default[:mark_int] != 0) and (time_int % default[:mark_int] == 0) then mark_label = OSX::NSString.alloc.initWithString("%d:%02d" % [time_int / 60, time_int % 60]) mark_size = mark_label.sizeWithAttributes(default[:font_dict]) mark_label.drawAtPoint_withAttributes([point_x-mark_size.width/2, point_y+default[:font_margin]], default[:font_dict]) mark = OSX::NSBezierPath.bezierPathWithOvalInRect([point_x-2, point_y-2, 4, 4]) yellow.set mark.stroke white.set end end graph.stroke # summary sum_y = default[:height] - default[:margin] / 2 dist_label = OSX::NSString.alloc.initWithString(SUMMARY[:dist] % gps_data[:total_dist]) dist_size = dist_label.sizeWithAttributes(default[:em_font_dict]) dist_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-dist_size.width, sum_y-dist_size.height], default[:em_font_dict]) sum_y -= dist_size.height + default[:font_margin] time_label = OSX::NSString.alloc.initWithString(SUMMARY[:time] % [gps_data[:time_int]/60, gps_data[:time_int]%60]) time_size = time_label.sizeWithAttributes(default[:em_font_dict]) time_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-time_size.width, sum_y-time_size.height], default[:em_font_dict]) sum_y -= time_size.height + default[:font_margin] maxv_label = OSX::NSString.alloc.initWithString(SUMMARY[:max_v] % gps_data[:max_v]) maxv_size = maxv_label.sizeWithAttributes(default[:em_font_dict]) maxv_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-maxv_size.width, sum_y-maxv_size.height], default[:em_font_dict]) sum_y -= maxv_size.height + default[:font_margin] avgv_label = OSX::NSString.alloc.initWithString(SUMMARY[:avg_v] % gps_data[:avg_v]) avgv_size = avgv_label.sizeWithAttributes(default[:em_font_dict]) avgv_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-avgv_size.width, sum_y-avgv_size.height], default[:em_font_dict]) canvas.representationUsingType_properties(default[:format], nil).rubyString end if ARGV.size > 0 then ARGV.each do |a| doc = Document.new(open(a)) loc_f = proc { |e| e.elements['Point'].elements['coordinates'].text.split(',').collect { |s| s.to_f}[0..2] } h_f = proc { |e| e.elements['Point'].elements['coordinates'].text.split(',').collect { |s| s.to_f}[2] } t_f = proc { |e| Time.parse(e.elements['TimeStamp'].elements['when'].text).localtime } r = gps_calc(doc, 'kml/Document/Folder/Folder/Placemark', loc_f, h_f, t_f, 60*60) r.each do |result| open("graph_#{result[:start_time].strftime('%Y%m%d%H%M')}.png", 'wb') { |f| f.write(make_graph(result))} end end end |
구현은 간단합니다.
require 'xmlrpc/client'
server = XMLRPC::Client.new2('http://사용자아이디.tistory.com/api')
post = { "title"=>제목문자열, "categories"=>[카테고리 배열], "description"=>블로그내용,
"mt_keywords"=>쉼표로 구분한 태그 문자열 }
result = server.call('metaWeblog.newPost', 블로그id, 아이디, 패스워드, post, true)
마지막 2행을 적절히 수정해가면서 반복적으로 호출하면 블로그가 옮겨지는 것이죠. 생성 날짜에 대한 처리가 빠져있습니다만 일단 호출하면 제대로 포스팅이 됩니다.
그런데 문제는 같이 포함된 파일들입니다. 이론적으로는 블로그 내용을 스캔해서 img 태그가 블로그 내부의 파일을 링크하면 이 이미지를 티스토리에 옮겨주면 됩니다. 이때 사용할 수 있는 호출 함수가 metaWeblog.newMediaObject 함수입니다.
호출은 다음과 같은 방법으로 할 수 있습니다.
require base64
bincontent = open(파일이름) { |f| f.read }
enccontent = Base64.encode64.gsub(/\n/, '')
attach = { "name"=>파일이름, "type"=>MIME 타입('image/jpeg'), "bits"=>enccontent }
result = server.call('metaWeblog.newMediaObject', 블로그id, 아이디, 패스워드, attach)
위와 같이 호출하면 서버에서 만들어진 파일에 대한 링크가 반환됩니다. 원래 블로그의 링크를 이것으로 바꾸어주면 되겠죠.
현재까지 알아낸 바로는 티스토리에서는 base64인코딩의 디코딩을 시행하지 않고 그대로 저장하는것 같습니다. 따라서 이미지로 인식되지 않고 블로그에서 이를 볼수가 없습니다. 티스토리측에 버그 리포트를 했는데 어떻게 처리될지 모르겠네요.
* UPDATE
티스토리및 태터툴즈와 이야기를 해 본 결과 제가 한 방식이 맞기는 맞습니다만, 애초 스펙에 문제가 좀 있는 것 같습니다. 하여튼 다음과 같은 방식으로 ruby에서 media를 포스팅하면 되겠습니다.
require base64
bincontent = open(파일이름) { |f| f.read }
attach = { "name"=>파일이름, "type"=>MIME 타입('image/jpeg'), "bits"=>XMLRPC::Base64.new(bincontent) }
result = server.call('metaWeblog.newMediaObject', 블로그id, 아이디, 패스워드, attach)
아직까지 루비와 레일스를 취미삼아 공부하다 보니 루비를 다른 언어들과 차별되게 하는 것들이 클래스와 클래스와 객체를 동적으로 확장하는 것과 블록이 아닌가 하는 생각이 들었습니다. 거창한 제목을 붙여 보았습니다만 먼저 클래스와 객체의 동적인 확장과 관련된 것을 공부삼아 간단히 정리해 보겠습니다. 내용은 틀린것이 있으면 수정하고 필요하다면 보완하도록 하겠습니다.
모든것은 객체
루비에서 모든 것은 객체입니다. 다른 객체지향언어들과 같이 모든 객체는 클래스를 가집니다. 객체에는 각 인스턴스 변수가 저장되어 있고 클래스에는 메소드가 저장됩니다. 클래스의 인스턴스에 메소드가 호출되면 인스턴스의 클래스를 찾아서 해당 클래스와 상위 클래스 들에서 메소드를 찾아서 실행시키게됩니다. 메소드를 호출할때는 기본적으로 객체를 지정해야하며 이를 생략하면 self가 포함된 것으로 간주됩니다. self는 상황에 따라 다른 값을 가지게 되는데 보통 irb에서는 main이란 Object가 됩니다.
싱글톤 클래스
각 객체에는 그 객체만을 위한 메소드나 클래스를 지정할 수 있습니다. 이를 싱글톤 메소드 혹은 클래스라고 부르는데 이때 루비는 그 객체만의 싱글톤 클래스란 무명 클래스를 만들어 객체와 부착시킵니다. 싱글톤 클래스는 객체와 객체의 클래스 사이에 위치하여 객체만의 메소드를 추가하거나 클래스의 메소드를 오버라이드 할수 있도록 해줍니다.
클래스 역시 객체입니다. 루비에서 모든 클래스들은 기본 클래스 Class의 인스턴스입니다. 위에서 언급한 객체와 클래스의 관계를 생각할 때 각 객체 클래스의 기본 클래스 Class에 메소드를 저장하면 모든 클래스가 같은 클래스 메소드를 가지게 됩니다. 이를 방지하게 위해 클래스 메소드들은 앞서 이야기한 싱글톤 클래스를 이용하여 구현됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class String self.module_eval do define_method :foo do puts "inside foo" end end (class << self; self; end).module_eval do define_method :bar do puts "inside bar" end end end "string".foo # => "inside foo" String.bar # => "inside bar" |
인스턴스 변수, 클래스 변수, 클래스 인스턴스 변수
각 객체는 내부의 인스턴스 변수를 가질 수 있으며 클래스는 클래스 변수를 가질 수 있습니다. 루비에서는 클래스 인스턴스 변수란 것이 추가됩니다. 클래스 메소드가 아닌 메소드의 정의에서 @가 붙는 변수는 보통의 인스턴스 변수가 되지만 클래스의 정의나 클래스 메소드의 정의에 @가 붙는 변수가 클래스 인스턴스 변수가 됩니다.
클래스 인스턴스 변수는 객체의 메소드에서 접근할 수 없으므로 보통의 경우 클래스 변수를 사용하면 되지만, 클래스에서 상속이 일어나는 경우 클래스 변수는 자손과 공유하게 되므로 자손과 따로 클래스 전용의 변수를 사용하고자 한다면 클래스 인스턴스 변수를 사용해야 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Base @@var1 = nil class << self attr_accessor :var2 end def self.var1 p @@var1 end end class A < Base @@var1 = 'A' self.var2 = 'B' end class B < Base @@var1 = 'B' self.var = 'B' end >> A.var1 # 'B' >> B.var1 # 'B' >> A.var2 # 'A' >> B.var2 # 'B' |
참고
- Programming Ruby: Pragmatic programmer’s guide, 2nd edition
- The Ruby Way, Second Edition
- http://www.rubycentral.com/faq/rubyfaq-8.html
- http://ola-bini.blogspot.com/2006/09/ruby-singleton-class.html
1 2 3 4 5 6 7 8 |
puts "Method name: " meth_name = gets puts "Line of code: " code = gets string = %[def #{meth_name}\n #{code}\n end] eval(string) eval(meth_name) |
위의 간단한 예에서 함수이름과 코드를 입력받아 함수를 정의하고 실행한다.

gifmaker.rb