rmov 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +19 -0
- data/Manifest +1 -0
- data/README.rdoc +33 -7
- data/Rakefile +1 -1
- data/ext/exporter.c +2 -2
- data/ext/movie.c +83 -64
- data/ext/rmov_ext.h +2 -0
- data/ext/track.c +104 -3
- data/lib/quicktime/movie.rb +74 -2
- data/lib/quicktime/track.rb +10 -0
- data/rmov.gemspec +27 -95
- data/spec/fixtures/dot.png +0 -0
- data/spec/fixtures/settings.st +0 -0
- data/spec/quicktime/exporter_spec.rb +18 -0
- data/spec/quicktime/movie_spec.rb +26 -1
- data/spec/quicktime/track_spec.rb +31 -3
- metadata +7 -4
data/CHANGELOG
CHANGED
@@ -1,9 +1,25 @@
|
|
1
|
+
0.1.5 (July 28, 2009)
|
2
|
+
|
3
|
+
* fixing saving export settings - closes #8
|
4
|
+
|
5
|
+
* improving how movie selection process happens providing more flexibility - closes #4 and #5
|
6
|
+
|
7
|
+
* adding enable_alpha method to enable alpha transparency for compositing - closes #3
|
8
|
+
|
9
|
+
* adding track transformations (rotation, scaling, and translation) - closes #1 and #2
|
10
|
+
|
11
|
+
* adding movie.save method to save a movie in place
|
12
|
+
|
13
|
+
* supporting .pict extension when exporting frame
|
14
|
+
|
15
|
+
|
1
16
|
0.1.4 (October 3rd, 2008)
|
2
17
|
|
3
18
|
* adding support for several export_image formats (PNG, JPEG, TIFF, TGA, BMP, PSD)
|
4
19
|
|
5
20
|
* adding movie.export_image as a generic way to export a frame to multiple formats
|
6
21
|
|
22
|
+
|
7
23
|
0.1.3 (October 3rd, 2008)
|
8
24
|
|
9
25
|
* some support for text tracks
|
@@ -12,6 +28,7 @@
|
|
12
28
|
|
13
29
|
* changing Quicktime module name to QuickTime to match proper casing
|
14
30
|
|
31
|
+
|
15
32
|
0.1.2 (October 3rd, 2008)
|
16
33
|
|
17
34
|
* movie.poster_time and movie.poster_time=(seconds) for getting and setting a movie's poster time
|
@@ -22,12 +39,14 @@
|
|
22
39
|
|
23
40
|
* QuickTime settings dialog comes into the forground properly
|
24
41
|
|
42
|
+
|
25
43
|
0.1.1 (October 3rd, 2008)
|
26
44
|
|
27
45
|
* RubyGems 1.3 compatibility (updated Echoe)
|
28
46
|
|
29
47
|
* fixing inline RDocs so call sequence is handled properly
|
30
48
|
|
49
|
+
|
31
50
|
0.1.0 (October 2nd, 2008)
|
32
51
|
|
33
52
|
* initial release
|
data/Manifest
CHANGED
data/README.rdoc
CHANGED
@@ -17,7 +17,9 @@ And then load it in your project:
|
|
17
17
|
|
18
18
|
== Usage
|
19
19
|
|
20
|
-
|
20
|
+
There are many methods for editing, compositing, and exporting movies. Here are some examples.
|
21
|
+
|
22
|
+
=== Editing
|
21
23
|
|
22
24
|
movie1 = QuickTime::Movie.open("path/to/movie.mov")
|
23
25
|
movie2 = QuickTime::Movie.open("path/to/another_movie.mov")
|
@@ -32,12 +34,33 @@ Use this gem to open QuickTime movies and edit them to your liking.
|
|
32
34
|
# You can insert that part back into the movie at 8 seconds in
|
33
35
|
movie1.insert_movie(movie3, 8)
|
34
36
|
|
35
|
-
|
36
|
-
interface the first time around. The settings can then be saved to
|
37
|
-
a file. After that you can load these settings without interfering
|
38
|
-
the user with the dialog again.
|
37
|
+
=== Compositing
|
39
38
|
|
40
|
-
|
39
|
+
movie = QuickTime::Movie.open("path/to/movie.mov")
|
40
|
+
watermark_movie = QuickTime::Movie.open("path/to/watermark.png")
|
41
|
+
|
42
|
+
# add watermark track onto the entire length of the movie
|
43
|
+
movie.composite_movie(watermark_movie, 0, movie.duration)
|
44
|
+
|
45
|
+
# grab the watermark track
|
46
|
+
watermark = movie.video_tracks.last
|
47
|
+
|
48
|
+
# enable the alpha transparency of the png
|
49
|
+
watermark.enable_alpha
|
50
|
+
|
51
|
+
# make the watermark half the size
|
52
|
+
watermark.scale(0.5, 0.5)
|
53
|
+
|
54
|
+
# offset into lower left corner
|
55
|
+
watermark.translate(10, movie.height - watermark.height - 10)
|
56
|
+
|
57
|
+
=== Exporting
|
58
|
+
|
59
|
+
Usually exporting is done through a user interface the first time.
|
60
|
+
The settings can then be saved to a file. After that you can load
|
61
|
+
these settings without interfering the user with the dialog again.
|
62
|
+
|
63
|
+
exporter = movie.exporter
|
41
64
|
|
42
65
|
# if we already have saved the settings, load those
|
43
66
|
if File.exist? "settings.st"
|
@@ -56,7 +79,10 @@ the user with the dialog again.
|
|
56
79
|
puts "#{percent}% complete"
|
57
80
|
end
|
58
81
|
|
59
|
-
|
82
|
+
|
83
|
+
== Documentation
|
84
|
+
|
85
|
+
See QuickTime::Movie and QuickTime::Track in the RDoc for more information.
|
60
86
|
|
61
87
|
http://rmov.rubyforge.org
|
62
88
|
|
data/Rakefile
CHANGED
@@ -2,7 +2,7 @@ require 'rubygems'
|
|
2
2
|
require 'rake'
|
3
3
|
require 'echoe'
|
4
4
|
|
5
|
-
Echoe.new('rmov', '0.1.
|
5
|
+
Echoe.new('rmov', '0.1.5') do |p|
|
6
6
|
p.summary = "Ruby wrapper for the QuickTime C API."
|
7
7
|
p.description = "Ruby wrapper for the QuickTime C API."
|
8
8
|
p.url = "http://github.com/ryanb/rmov"
|
data/ext/exporter.c
CHANGED
@@ -93,7 +93,7 @@ static VALUE exporter_open_settings_dialog(VALUE obj)
|
|
93
93
|
// Bring this process to the front
|
94
94
|
err = TransformProcessType(¤t_process, kProcessTransformToForegroundApplication);
|
95
95
|
if (err != noErr) {
|
96
|
-
rb_raise(eQuickTime, "Error %d occurred while
|
96
|
+
rb_raise(eQuickTime, "Error %d occurred while bringing this application to the forground.", err);
|
97
97
|
}
|
98
98
|
SetFrontProcess(¤t_process);
|
99
99
|
|
@@ -176,7 +176,7 @@ static VALUE exporter_save_settings(VALUE obj, VALUE filepath)
|
|
176
176
|
if (!file) {
|
177
177
|
rb_raise(eQuickTime, "Unable to open file for saving at %s.", RSTRING(filepath)->ptr);
|
178
178
|
}
|
179
|
-
fwrite(
|
179
|
+
fwrite(*settings, GetHandleSize((Handle)settings), 1, file);
|
180
180
|
fclose(file);
|
181
181
|
|
182
182
|
return Qnil;
|
data/ext/movie.c
CHANGED
@@ -64,27 +64,29 @@ static VALUE movie_load_from_file(VALUE obj, VALUE filepath)
|
|
64
64
|
} else {
|
65
65
|
OSErr err;
|
66
66
|
FSSpec fs;
|
67
|
-
short
|
68
|
-
short
|
67
|
+
short resRefNum = -1;
|
68
|
+
short resId = 0;
|
69
69
|
Movie *movie = ALLOC(Movie);
|
70
70
|
|
71
71
|
err = NativePathNameToFSSpec(RSTRING(filepath)->ptr, &fs, 0);
|
72
72
|
if (err != 0)
|
73
73
|
rb_raise(eQuickTime, "Error %d occurred while reading file at %s", err, RSTRING(filepath)->ptr);
|
74
74
|
|
75
|
-
err = OpenMovieFile(&fs, &
|
75
|
+
err = OpenMovieFile(&fs, &resRefNum, fsRdPerm);
|
76
76
|
if (err != 0)
|
77
77
|
rb_raise(eQuickTime, "Error %d occurred while opening movie at %s", err, RSTRING(filepath)->ptr);
|
78
78
|
|
79
|
-
err = NewMovieFromFile(movie,
|
79
|
+
err = NewMovieFromFile(movie, resRefNum, &resId, 0, newMovieActive, 0);
|
80
80
|
if (err != 0)
|
81
81
|
rb_raise(eQuickTime, "Error %d occurred while loading movie at %s", err, RSTRING(filepath)->ptr);
|
82
82
|
|
83
|
-
err = CloseMovieFile(
|
83
|
+
err = CloseMovieFile(resRefNum);
|
84
84
|
if (err != 0)
|
85
85
|
rb_raise(eQuickTime, "Error %d occurred while closing movie file at %s", err, RSTRING(filepath)->ptr);
|
86
86
|
|
87
87
|
RMOVIE(obj)->movie = *movie;
|
88
|
+
RMOVIE(obj)->filepath = RSTRING(filepath)->ptr;
|
89
|
+
RMOVIE(obj)->resId = resId;
|
88
90
|
|
89
91
|
return obj;
|
90
92
|
}
|
@@ -158,44 +160,32 @@ static VALUE movie_track_count(VALUE obj)
|
|
158
160
|
}
|
159
161
|
|
160
162
|
/*
|
161
|
-
call-seq:
|
163
|
+
call-seq: select(position, duration)
|
162
164
|
|
163
|
-
|
164
|
-
|
165
|
-
You can track the progress of this operation by passing a block to this
|
166
|
-
method. It will be called regularly during the process and pass the
|
167
|
-
percentage complete (0.0 to 1.0) as an argument to the block.
|
165
|
+
Select a portion of a movie. Both position and duration should be
|
166
|
+
floats representing seconds.
|
168
167
|
*/
|
169
|
-
static VALUE
|
168
|
+
static VALUE movie_select(VALUE obj, VALUE position, VALUE duration)
|
170
169
|
{
|
171
|
-
|
172
|
-
SetMovieProgressProc(MOVIE(obj), (MovieProgressUPP)movie_progress_proc, rb_block_proc());
|
173
|
-
|
174
|
-
SetMovieSelection(MOVIE(obj), MOVIE_TIME(obj, position), 0);
|
175
|
-
AddMovieSelection(MOVIE(obj), MOVIE(src));
|
176
|
-
|
177
|
-
if (rb_block_given_p())
|
178
|
-
SetMovieProgressProc(MOVIE(obj), 0, 0);
|
179
|
-
|
170
|
+
SetMovieSelection(MOVIE(obj), MOVIE_TIME(obj, position), MOVIE_TIME(obj, duration));
|
180
171
|
return obj;
|
181
172
|
}
|
182
173
|
|
183
174
|
/*
|
184
|
-
call-seq:
|
175
|
+
call-seq: add_into_selection(movie)
|
176
|
+
|
177
|
+
Adds the tracks of given movie into called movie's current selection.
|
185
178
|
|
186
|
-
Inserts given movie into called movie at given position (in seconds).
|
187
|
-
|
188
179
|
You can track the progress of this operation by passing a block to this
|
189
180
|
method. It will be called regularly during the process and pass the
|
190
181
|
percentage complete (0.0 to 1.0) as an argument to the block.
|
191
182
|
*/
|
192
|
-
static VALUE
|
183
|
+
static VALUE movie_add_into_selection(VALUE obj, VALUE src)
|
193
184
|
{
|
194
185
|
if (rb_block_given_p())
|
195
186
|
SetMovieProgressProc(MOVIE(obj), (MovieProgressUPP)movie_progress_proc, rb_block_proc());
|
196
187
|
|
197
|
-
|
198
|
-
PasteMovieSelection(MOVIE(obj), MOVIE(src));
|
188
|
+
AddMovieSelection(MOVIE(obj), MOVIE(src));
|
199
189
|
|
200
190
|
if (rb_block_given_p())
|
201
191
|
SetMovieProgressProc(MOVIE(obj), 0, 0);
|
@@ -204,20 +194,19 @@ static VALUE movie_insert_movie(VALUE obj, VALUE src, VALUE position)
|
|
204
194
|
}
|
205
195
|
|
206
196
|
/*
|
207
|
-
call-seq:
|
197
|
+
call-seq: insert_into_selection(movie)
|
198
|
+
|
199
|
+
Inserts the given movie into called movie, replacing any current selection.
|
208
200
|
|
209
|
-
Adds given movie to the end of movie which this method is called on.
|
210
|
-
|
211
201
|
You can track the progress of this operation by passing a block to this
|
212
202
|
method. It will be called regularly during the process and pass the
|
213
203
|
percentage complete (0.0 to 1.0) as an argument to the block.
|
214
204
|
*/
|
215
|
-
static VALUE
|
205
|
+
static VALUE movie_insert_into_selection(VALUE obj, VALUE src)
|
216
206
|
{
|
217
207
|
if (rb_block_given_p())
|
218
208
|
SetMovieProgressProc(MOVIE(obj), (MovieProgressUPP)movie_progress_proc, rb_block_proc());
|
219
209
|
|
220
|
-
SetMovieSelection(MOVIE(obj), GetMovieDuration(MOVIE(obj)), 0);
|
221
210
|
PasteMovieSelection(MOVIE(obj), MOVIE(src));
|
222
211
|
|
223
212
|
if (rb_block_given_p())
|
@@ -227,37 +216,22 @@ static VALUE movie_append_movie(VALUE obj, VALUE src)
|
|
227
216
|
}
|
228
217
|
|
229
218
|
/*
|
230
|
-
call-seq:
|
219
|
+
call-seq: clone_selection()
|
231
220
|
|
232
|
-
|
233
|
-
|
234
|
-
*/
|
235
|
-
static VALUE movie_delete_section(VALUE obj, VALUE start, VALUE duration)
|
236
|
-
{
|
237
|
-
SetMovieSelection(MOVIE(obj), MOVIE_TIME(obj, start), MOVIE_TIME(obj, duration));
|
238
|
-
ClearMovieSelection(MOVIE(obj));
|
239
|
-
return obj;
|
240
|
-
}
|
241
|
-
|
242
|
-
/*
|
243
|
-
call-seq: clone_section(start_time, duration) -> movie
|
221
|
+
Returns a new movie from the current selection. Does not modify original
|
222
|
+
movie.
|
244
223
|
|
245
|
-
Returns a new movie in the given section. Does not modify original
|
246
|
-
movie. Both start_time and duration should be floats representing
|
247
|
-
seconds.
|
248
|
-
|
249
224
|
You can track the progress of this operation by passing a block to this
|
250
225
|
method. It will be called regularly during the process and pass the
|
251
226
|
percentage complete (0.0 to 1.0) as an argument to the block.
|
252
227
|
*/
|
253
|
-
static VALUE
|
228
|
+
static VALUE movie_clone_selection(VALUE obj)
|
254
229
|
{
|
255
230
|
VALUE new_movie_obj = rb_obj_alloc(cMovie);
|
256
231
|
|
257
232
|
if (rb_block_given_p())
|
258
233
|
SetMovieProgressProc(MOVIE(obj), (MovieProgressUPP)movie_progress_proc, rb_block_proc());
|
259
234
|
|
260
|
-
SetMovieSelection(MOVIE(obj), MOVIE_TIME(obj, start), MOVIE_TIME(obj, duration));
|
261
235
|
RMOVIE(new_movie_obj)->movie = CopyMovieSelection(MOVIE(obj));
|
262
236
|
|
263
237
|
if (rb_block_given_p())
|
@@ -267,24 +241,22 @@ static VALUE movie_clone_section(VALUE obj, VALUE start, VALUE duration)
|
|
267
241
|
}
|
268
242
|
|
269
243
|
/*
|
270
|
-
call-seq:
|
244
|
+
call-seq: clip_selection()
|
245
|
+
|
246
|
+
Deletes current selection on movie and returns a new movie with that
|
247
|
+
content.
|
271
248
|
|
272
|
-
Deletes given section on movie and returns a new movie with that
|
273
|
-
section. Both start_time and duration should be floats representing
|
274
|
-
seconds.
|
275
|
-
|
276
249
|
You can track the progress of this operation by passing a block to this
|
277
250
|
method. It will be called regularly during the process and pass the
|
278
251
|
percentage complete (0.0 to 1.0) as an argument to the block.
|
279
252
|
*/
|
280
|
-
static VALUE
|
253
|
+
static VALUE movie_clip_selection(VALUE obj)
|
281
254
|
{
|
282
255
|
VALUE new_movie_obj = rb_obj_alloc(cMovie);
|
283
256
|
|
284
257
|
if (rb_block_given_p())
|
285
258
|
SetMovieProgressProc(MOVIE(obj), (MovieProgressUPP)movie_progress_proc, rb_block_proc());
|
286
259
|
|
287
|
-
SetMovieSelection(MOVIE(obj), MOVIE_TIME(obj, start), MOVIE_TIME(obj, duration));
|
288
260
|
RMOVIE(new_movie_obj)->movie = CutMovieSelection(MOVIE(obj));
|
289
261
|
|
290
262
|
if (rb_block_given_p())
|
@@ -293,6 +265,17 @@ static VALUE movie_clip_section(VALUE obj, VALUE start, VALUE duration)
|
|
293
265
|
return new_movie_obj;
|
294
266
|
}
|
295
267
|
|
268
|
+
/*
|
269
|
+
call-seq: delete_selection()
|
270
|
+
|
271
|
+
Removes the portion of the movie which is selected.
|
272
|
+
*/
|
273
|
+
static VALUE movie_delete_selection(VALUE obj)
|
274
|
+
{
|
275
|
+
ClearMovieSelection(MOVIE(obj));
|
276
|
+
return obj;
|
277
|
+
}
|
278
|
+
|
296
279
|
/*
|
297
280
|
call-seq: changed?() -> bool
|
298
281
|
|
@@ -345,6 +328,41 @@ static VALUE movie_flatten(VALUE obj, VALUE filepath)
|
|
345
328
|
return new_movie_obj;
|
346
329
|
}
|
347
330
|
|
331
|
+
|
332
|
+
/*
|
333
|
+
call-seq: save()
|
334
|
+
|
335
|
+
Saves the movie to the current file.
|
336
|
+
*/
|
337
|
+
static VALUE movie_save(VALUE obj)
|
338
|
+
{
|
339
|
+
OSErr err;
|
340
|
+
FSSpec fs;
|
341
|
+
short resRefNum = -1;
|
342
|
+
|
343
|
+
if (!RMOVIE(obj)->filepath || !RMOVIE(obj)->resId) {
|
344
|
+
rb_raise(eQuickTime, "Unable to save movie because it does not have an associated file.");
|
345
|
+
} else {
|
346
|
+
err = NativePathNameToFSSpec(RMOVIE(obj)->filepath, &fs, 0);
|
347
|
+
if (err != 0)
|
348
|
+
rb_raise(eQuickTime, "Error %d occurred while reading file at %s", err, RMOVIE(obj)->filepath);
|
349
|
+
|
350
|
+
err = OpenMovieFile(&fs, &resRefNum, fsWrPerm);
|
351
|
+
if (err != 0)
|
352
|
+
rb_raise(eQuickTime, "Error %d occurred while opening movie at %s", err, RMOVIE(obj)->filepath);
|
353
|
+
|
354
|
+
err = UpdateMovieResource(MOVIE(obj), resRefNum, RMOVIE(obj)->resId, 0);
|
355
|
+
if (err != 0)
|
356
|
+
rb_raise(eQuickTime, "Error %d occurred while saving movie file", err);
|
357
|
+
|
358
|
+
err = CloseMovieFile(resRefNum);
|
359
|
+
if (err != 0)
|
360
|
+
rb_raise(eQuickTime, "Error %d occurred while closing movie file at %s", err, RMOVIE(obj)->filepath);
|
361
|
+
|
362
|
+
return Qnil;
|
363
|
+
}
|
364
|
+
}
|
365
|
+
|
348
366
|
/*
|
349
367
|
call-seq: export_image_type(filepath, time, ostype)
|
350
368
|
|
@@ -440,12 +458,12 @@ void Init_quicktime_movie()
|
|
440
458
|
rb_define_method(cMovie, "time_scale", movie_time_scale, 0);
|
441
459
|
rb_define_method(cMovie, "bounds", movie_bounds, 0);
|
442
460
|
rb_define_method(cMovie, "track_count", movie_track_count, 0);
|
443
|
-
rb_define_method(cMovie, "
|
444
|
-
rb_define_method(cMovie, "
|
445
|
-
rb_define_method(cMovie, "
|
446
|
-
rb_define_method(cMovie, "
|
447
|
-
rb_define_method(cMovie, "
|
448
|
-
rb_define_method(cMovie, "
|
461
|
+
rb_define_method(cMovie, "select", movie_select, 2);
|
462
|
+
rb_define_method(cMovie, "add_into_selection", movie_add_into_selection, 1);
|
463
|
+
rb_define_method(cMovie, "insert_into_selection", movie_insert_into_selection, 1);
|
464
|
+
rb_define_method(cMovie, "clone_selection", movie_clone_selection, 0);
|
465
|
+
rb_define_method(cMovie, "clip_selection", movie_clip_selection, 0);
|
466
|
+
rb_define_method(cMovie, "delete_selection", movie_delete_selection, 0);
|
449
467
|
rb_define_method(cMovie, "changed?", movie_changed, 0);
|
450
468
|
rb_define_method(cMovie, "clear_changed_status", movie_clear_changed_status, 0);
|
451
469
|
rb_define_method(cMovie, "flatten", movie_flatten, 1);
|
@@ -454,4 +472,5 @@ void Init_quicktime_movie()
|
|
454
472
|
rb_define_method(cMovie, "poster_time", movie_get_poster_time, 0);
|
455
473
|
rb_define_method(cMovie, "poster_time=", movie_set_poster_time, 1);
|
456
474
|
rb_define_method(cMovie, "new_track", movie_new_track, 2);
|
475
|
+
rb_define_method(cMovie, "save", movie_save, 0);
|
457
476
|
}
|
data/ext/rmov_ext.h
CHANGED
data/ext/track.c
CHANGED
@@ -191,7 +191,7 @@ static VALUE track_set_offset(VALUE obj, VALUE seconds)
|
|
191
191
|
}
|
192
192
|
|
193
193
|
/*
|
194
|
-
call-seq: new_video_media
|
194
|
+
call-seq: new_video_media()
|
195
195
|
|
196
196
|
Creates a new video media for this track.
|
197
197
|
|
@@ -205,7 +205,7 @@ static VALUE track_new_video_media(VALUE obj)
|
|
205
205
|
}
|
206
206
|
|
207
207
|
/*
|
208
|
-
call-seq: new_audio_media
|
208
|
+
call-seq: new_audio_media()
|
209
209
|
|
210
210
|
Creates a new audio media for this track.
|
211
211
|
|
@@ -219,7 +219,7 @@ static VALUE track_new_audio_media(VALUE obj)
|
|
219
219
|
}
|
220
220
|
|
221
221
|
/*
|
222
|
-
call-seq: new_text_media
|
222
|
+
call-seq: new_text_media()
|
223
223
|
|
224
224
|
Creates a new text media for this track.
|
225
225
|
|
@@ -232,6 +232,101 @@ static VALUE track_new_text_media(VALUE obj)
|
|
232
232
|
return obj;
|
233
233
|
}
|
234
234
|
|
235
|
+
/*
|
236
|
+
call-seq: enable_alpha()
|
237
|
+
|
238
|
+
Enable the straight alpha graphic mode for this track.
|
239
|
+
|
240
|
+
This is best used on an overlayed video track which includes some
|
241
|
+
alpha transparency (such as in a PNG image).
|
242
|
+
*/
|
243
|
+
static VALUE track_enable_alpha(VALUE obj)
|
244
|
+
{
|
245
|
+
MediaSetGraphicsMode(GetMediaHandler(TRACK_MEDIA(obj)), graphicsModeStraightAlpha, 0);
|
246
|
+
return obj;
|
247
|
+
}
|
248
|
+
|
249
|
+
/*
|
250
|
+
call-seq: scale(width, height)
|
251
|
+
|
252
|
+
Scale the track's size by width and height respectively.
|
253
|
+
|
254
|
+
The value passed is a relative float where "1" is the current size.
|
255
|
+
*/
|
256
|
+
static VALUE track_scale(VALUE obj, VALUE width, VALUE height)
|
257
|
+
{
|
258
|
+
MatrixRecord matrix;
|
259
|
+
GetTrackMatrix(TRACK(obj), &matrix);
|
260
|
+
ScaleMatrix(&matrix, FloatToFixed(NUM2DBL(width)), FloatToFixed(NUM2DBL(height)), 0, 0);
|
261
|
+
SetTrackMatrix(TRACK(obj), &matrix);
|
262
|
+
return obj;
|
263
|
+
}
|
264
|
+
|
265
|
+
/*
|
266
|
+
call-seq: translate(x, y)
|
267
|
+
|
268
|
+
Offset a track's position by x and y values respectively.
|
269
|
+
|
270
|
+
Values should be in pixels.
|
271
|
+
*/
|
272
|
+
static VALUE track_translate(VALUE obj, VALUE x, VALUE y)
|
273
|
+
{
|
274
|
+
MatrixRecord matrix;
|
275
|
+
GetTrackMatrix(TRACK(obj), &matrix);
|
276
|
+
TranslateMatrix(&matrix, FloatToFixed(NUM2DBL(x)), FloatToFixed(NUM2DBL(y)));
|
277
|
+
SetTrackMatrix(TRACK(obj), &matrix);
|
278
|
+
return obj;
|
279
|
+
}
|
280
|
+
|
281
|
+
/*
|
282
|
+
call-seq: rotate(degrees)
|
283
|
+
|
284
|
+
Rotate the track by the given number of degrees.
|
285
|
+
*/
|
286
|
+
static VALUE track_rotate(VALUE obj, VALUE degrees)
|
287
|
+
{
|
288
|
+
MatrixRecord matrix;
|
289
|
+
GetTrackMatrix(TRACK(obj), &matrix);
|
290
|
+
RotateMatrix(&matrix, FloatToFixed(NUM2DBL(degrees)), 0, 0);
|
291
|
+
SetTrackMatrix(TRACK(obj), &matrix);
|
292
|
+
return obj;
|
293
|
+
}
|
294
|
+
|
295
|
+
/*
|
296
|
+
call-seq: bounds() -> bounds_hash
|
297
|
+
|
298
|
+
Returns a hash of boundaries. The hash contains four keys: :left, :top,
|
299
|
+
:right, :bottom. Each holds an integer representing the pixel value.
|
300
|
+
*/
|
301
|
+
static VALUE track_bounds(VALUE obj)
|
302
|
+
{
|
303
|
+
VALUE bounds_hash = rb_hash_new();
|
304
|
+
RgnHandle region;
|
305
|
+
Rect bounds;
|
306
|
+
region = GetTrackDisplayBoundsRgn(TRACK(obj));
|
307
|
+
GetRegionBounds(region, &bounds);
|
308
|
+
DisposeRgn(region);
|
309
|
+
rb_hash_aset(bounds_hash, ID2SYM(rb_intern("left")), INT2NUM(bounds.left));
|
310
|
+
rb_hash_aset(bounds_hash, ID2SYM(rb_intern("top")), INT2NUM(bounds.top));
|
311
|
+
rb_hash_aset(bounds_hash, ID2SYM(rb_intern("right")), INT2NUM(bounds.right));
|
312
|
+
rb_hash_aset(bounds_hash, ID2SYM(rb_intern("bottom")), INT2NUM(bounds.bottom));
|
313
|
+
return bounds_hash;
|
314
|
+
}
|
315
|
+
|
316
|
+
/*
|
317
|
+
call-seq: reset_transformations()
|
318
|
+
|
319
|
+
Revert any transformations (scale, translate, rotate) performed on this track.
|
320
|
+
*/
|
321
|
+
static VALUE track_reset_transformations(VALUE obj)
|
322
|
+
{
|
323
|
+
MatrixRecord matrix;
|
324
|
+
GetTrackMatrix(TRACK(obj), &matrix);
|
325
|
+
SetIdentityMatrix(&matrix);
|
326
|
+
SetTrackMatrix(TRACK(obj), &matrix);
|
327
|
+
return obj;
|
328
|
+
}
|
329
|
+
|
235
330
|
void Init_quicktime_track()
|
236
331
|
{
|
237
332
|
VALUE mQuickTime;
|
@@ -255,4 +350,10 @@ void Init_quicktime_track()
|
|
255
350
|
rb_define_method(cTrack, "new_video_media", track_new_video_media, 0);
|
256
351
|
rb_define_method(cTrack, "new_audio_media", track_new_audio_media, 0);
|
257
352
|
rb_define_method(cTrack, "new_text_media", track_new_text_media, 0);
|
353
|
+
rb_define_method(cTrack, "enable_alpha", track_enable_alpha, 0);
|
354
|
+
rb_define_method(cTrack, "scale", track_scale, 2);
|
355
|
+
rb_define_method(cTrack, "translate", track_translate, 2);
|
356
|
+
rb_define_method(cTrack, "rotate", track_rotate, 1);
|
357
|
+
rb_define_method(cTrack, "bounds", track_bounds, 0);
|
358
|
+
rb_define_method(cTrack, "reset_transformations", track_reset_transformations, 0);
|
258
359
|
}
|
data/lib/quicktime/movie.rb
CHANGED
@@ -87,10 +87,10 @@ module QuickTime
|
|
87
87
|
def export_image(filepath, seconds)
|
88
88
|
# TODO support more file types
|
89
89
|
type = case File.extname(filepath).downcase
|
90
|
-
when '.pct'
|
91
|
-
when '.png' then 'PNGf'
|
90
|
+
when '.pct', '.pict' then 'PICT'
|
92
91
|
when '.tif', '.tiff' then 'TIFF'
|
93
92
|
when '.jpg', '.jpeg' then 'JPEG'
|
93
|
+
when '.png' then 'PNGf'
|
94
94
|
when '.tga' then 'TPIC'
|
95
95
|
when '.bmp' then 'BMPf'
|
96
96
|
when '.psd' then '8BPS'
|
@@ -98,5 +98,77 @@ module QuickTime
|
|
98
98
|
end
|
99
99
|
export_image_type(filepath, seconds, type)
|
100
100
|
end
|
101
|
+
|
102
|
+
# Reset selection to beginning
|
103
|
+
def deselect
|
104
|
+
select(0, 0)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Adds the tracks of given movie into called movie. Position will default to
|
108
|
+
# beginning of movie. Duration will default to length of given movie.
|
109
|
+
#
|
110
|
+
# You can track the progress of this operation by passing a block to this
|
111
|
+
# method. It will be called regularly during the process and pass the
|
112
|
+
# percentage complete (0.0 to 1.0) as an argument to the block.
|
113
|
+
def composite_movie(movie, position = 0, duration = 0, &block)
|
114
|
+
select(position, duration)
|
115
|
+
add_into_selection(movie, &block)
|
116
|
+
deselect
|
117
|
+
end
|
118
|
+
|
119
|
+
# Adds given movie to the end of movie which this method is called on.
|
120
|
+
#
|
121
|
+
# You can track the progress of this operation by passing a block to this
|
122
|
+
# method. It will be called regularly during the process and pass the
|
123
|
+
# percentage complete (0.0 to 1.0) as an argument to the block.
|
124
|
+
def append_movie(movie, &block)
|
125
|
+
select(duration, 0)
|
126
|
+
insert_into_selection(movie, &block)
|
127
|
+
deselect
|
128
|
+
end
|
129
|
+
|
130
|
+
# Inserts given movie into called movie. The position defaults to the beginning
|
131
|
+
# of the movie. If a duration is passed, that amount of the movie will be replaced.
|
132
|
+
#
|
133
|
+
# You can track the progress of this operation by passing a block to this
|
134
|
+
# method. It will be called regularly during the process and pass the
|
135
|
+
# percentage complete (0.0 to 1.0) as an argument to the block.
|
136
|
+
def insert_movie(movie, position = 0, duration = 0, &block)
|
137
|
+
select(position, duration)
|
138
|
+
insert_into_selection(movie, &block)
|
139
|
+
deselect
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns a new movie from the specified portion of called movie.
|
143
|
+
#
|
144
|
+
# You can track the progress of this operation by passing a block to this
|
145
|
+
# method. It will be called regularly during the process and pass the
|
146
|
+
# percentage complete (0.0 to 1.0) as an argument to the block.
|
147
|
+
def clone_section(position = 0, duration = 0, &block)
|
148
|
+
select(position, duration)
|
149
|
+
movie = clone_selection(&block)
|
150
|
+
deselect
|
151
|
+
movie
|
152
|
+
end
|
153
|
+
|
154
|
+
# Deletes the specified section on movie and returns a new movie
|
155
|
+
# with that content.
|
156
|
+
#
|
157
|
+
# You can track the progress of this operation by passing a block to this
|
158
|
+
# method. It will be called regularly during the process and pass the
|
159
|
+
# percentage complete (0.0 to 1.0) as an argument to the block.
|
160
|
+
def clip_section(position = 0, duration = 0, &block)
|
161
|
+
select(position, duration)
|
162
|
+
movie = clip_selection(&block)
|
163
|
+
deselect
|
164
|
+
movie
|
165
|
+
end
|
166
|
+
|
167
|
+
# Deletes the specified section on movie.
|
168
|
+
def delete_section(position = 0, duration = 0)
|
169
|
+
select(position, duration)
|
170
|
+
delete_selection
|
171
|
+
deselect
|
172
|
+
end
|
101
173
|
end
|
102
174
|
end
|
data/lib/quicktime/track.rb
CHANGED
@@ -26,5 +26,15 @@ module QuickTime
|
|
26
26
|
def text?
|
27
27
|
media_type == :text
|
28
28
|
end
|
29
|
+
|
30
|
+
# Returns the bounding width of this track in number of pixels.
|
31
|
+
def width
|
32
|
+
bounds[:right] - bounds[:left]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the bounding height of this track in number of pixels.
|
36
|
+
def height
|
37
|
+
bounds[:bottom] - bounds[:top]
|
38
|
+
end
|
29
39
|
end
|
30
40
|
end
|
data/rmov.gemspec
CHANGED
@@ -1,99 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
|
2
|
-
|
3
|
-
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{rmov}
|
5
|
+
s.version = "0.1.5"
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Ryan Bates"]
|
9
|
+
s.date = %q{2009-07-28}
|
10
|
+
s.description = %q{Ruby wrapper for the QuickTime C API.}
|
11
|
+
s.email = %q{ryan (at) railscasts (dot) com}
|
12
|
+
s.extensions = ["ext/extconf.rb"]
|
13
|
+
s.extra_rdoc_files = ["CHANGELOG", "ext/exporter.c", "ext/extconf.rb", "ext/movie.c", "ext/rmov_ext.c", "ext/rmov_ext.h", "ext/track.c", "lib/quicktime/exporter.rb", "lib/quicktime/movie.rb", "lib/quicktime/track.rb", "lib/rmov.rb", "LICENSE", "README.rdoc", "tasks/setup.rake", "tasks/spec.rake", "TODO"]
|
14
|
+
s.files = ["CHANGELOG", "ext/exporter.c", "ext/extconf.rb", "ext/movie.c", "ext/rmov_ext.c", "ext/rmov_ext.h", "ext/track.c", "lib/quicktime/exporter.rb", "lib/quicktime/movie.rb", "lib/quicktime/track.rb", "lib/rmov.rb", "LICENSE", "Manifest", "Rakefile", "README.rdoc", "spec/fixtures/dot.png", "spec/fixtures/settings.st", "spec/quicktime/exporter_spec.rb", "spec/quicktime/movie_spec.rb", "spec/quicktime/track_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "tasks/setup.rake", "tasks/spec.rake", "TODO", "rmov.gemspec"]
|
15
|
+
s.homepage = %q{http://github.com/ryanb/rmov}
|
16
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Rmov", "--main", "README.rdoc"]
|
17
|
+
s.require_paths = ["lib", "ext"]
|
18
|
+
s.rubyforge_project = %q{rmov}
|
19
|
+
s.rubygems_version = %q{1.3.3}
|
20
|
+
s.summary = %q{Ruby wrapper for the QuickTime C API.}
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 3
|
18
25
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
extra_rdoc_files:
|
26
|
-
- CHANGELOG
|
27
|
-
- ext/exporter.c
|
28
|
-
- ext/extconf.rb
|
29
|
-
- ext/movie.c
|
30
|
-
- ext/rmov_ext.c
|
31
|
-
- ext/rmov_ext.h
|
32
|
-
- ext/track.c
|
33
|
-
- lib/quicktime/exporter.rb
|
34
|
-
- lib/quicktime/movie.rb
|
35
|
-
- lib/quicktime/track.rb
|
36
|
-
- lib/rmov.rb
|
37
|
-
- LICENSE
|
38
|
-
- README.rdoc
|
39
|
-
- tasks/setup.rake
|
40
|
-
- tasks/spec.rake
|
41
|
-
- TODO
|
42
|
-
files:
|
43
|
-
- CHANGELOG
|
44
|
-
- ext/exporter.c
|
45
|
-
- ext/extconf.rb
|
46
|
-
- ext/movie.c
|
47
|
-
- ext/rmov_ext.c
|
48
|
-
- ext/rmov_ext.h
|
49
|
-
- ext/track.c
|
50
|
-
- lib/quicktime/exporter.rb
|
51
|
-
- lib/quicktime/movie.rb
|
52
|
-
- lib/quicktime/track.rb
|
53
|
-
- lib/rmov.rb
|
54
|
-
- LICENSE
|
55
|
-
- Manifest
|
56
|
-
- Rakefile
|
57
|
-
- README.rdoc
|
58
|
-
- spec/fixtures/settings.st
|
59
|
-
- spec/quicktime/exporter_spec.rb
|
60
|
-
- spec/quicktime/movie_spec.rb
|
61
|
-
- spec/quicktime/track_spec.rb
|
62
|
-
- spec/spec.opts
|
63
|
-
- spec/spec_helper.rb
|
64
|
-
- tasks/setup.rake
|
65
|
-
- tasks/spec.rake
|
66
|
-
- TODO
|
67
|
-
- rmov.gemspec
|
68
|
-
has_rdoc: true
|
69
|
-
homepage: http://github.com/ryanb/rmov
|
70
|
-
post_install_message:
|
71
|
-
rdoc_options:
|
72
|
-
- --line-numbers
|
73
|
-
- --inline-source
|
74
|
-
- --title
|
75
|
-
- Rmov
|
76
|
-
- --main
|
77
|
-
- README.rdoc
|
78
|
-
require_paths:
|
79
|
-
- lib
|
80
|
-
- ext
|
81
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
-
requirements:
|
83
|
-
- - ">="
|
84
|
-
- !ruby/object:Gem::Version
|
85
|
-
version: "0"
|
86
|
-
version:
|
87
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
-
requirements:
|
89
|
-
- - ">="
|
90
|
-
- !ruby/object:Gem::Version
|
91
|
-
version: "1.2"
|
92
|
-
version:
|
93
|
-
requirements: []
|
94
|
-
|
95
|
-
rubyforge_project: rmov
|
96
|
-
rubygems_version: 1.2.0
|
97
|
-
specification_version: 2
|
98
|
-
summary: Ruby wrapper for the QuickTime C API.
|
99
|
-
test_files: []
|
26
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
27
|
+
else
|
28
|
+
end
|
29
|
+
else
|
30
|
+
end
|
31
|
+
end
|
Binary file
|
data/spec/fixtures/settings.st
CHANGED
Binary file
|
@@ -26,4 +26,22 @@ describe QuickTime::Exporter do
|
|
26
26
|
lambda { @exporter.save_settings('foo/bar/baz') }.should raise_error(QuickTime::Error)
|
27
27
|
end
|
28
28
|
end
|
29
|
+
describe "example.mov" do
|
30
|
+
before(:each) do
|
31
|
+
@movie = QuickTime::Movie.open(File.dirname(__FILE__) + '/../fixtures/example.mov')
|
32
|
+
@exporter = @movie.exporter
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should be able to export from loaded settings.st" do
|
36
|
+
load_path = File.dirname(__FILE__) + '/../fixtures/settings.st'
|
37
|
+
path = File.dirname(__FILE__) + '/../output/loaded_exported_example.mov'
|
38
|
+
File.delete(path) rescue nil
|
39
|
+
|
40
|
+
@exporter.load_settings(load_path)
|
41
|
+
@exporter.export(path)
|
42
|
+
exported_movie = QuickTime::Movie.open(path)
|
43
|
+
exported_movie.duration.should == @movie.duration
|
44
|
+
exported_movie.tracks.size == @movie.tracks.size
|
45
|
+
end
|
46
|
+
end
|
29
47
|
end
|
@@ -111,12 +111,28 @@ describe QuickTime::Movie do
|
|
111
111
|
|
112
112
|
it "flatten should save movie into file" do
|
113
113
|
path = File.dirname(__FILE__) + '/../output/flattened_example.mov'
|
114
|
-
File.delete(path)
|
114
|
+
File.delete(path) if File.exist?(path)
|
115
115
|
@movie.flatten(path)
|
116
116
|
mov = QuickTime::Movie.open(path)
|
117
117
|
mov.duration.should == 3.1
|
118
118
|
end
|
119
119
|
|
120
|
+
it "save should update movie in current file" do
|
121
|
+
path = File.dirname(__FILE__) + '/../output/saved_example.mov'
|
122
|
+
File.delete(path) if File.exist?(path)
|
123
|
+
@movie.flatten(path)
|
124
|
+
mov = QuickTime::Movie.open(path)
|
125
|
+
mov.audio_tracks.each { |t| t.delete } # delete track to demonstrate change
|
126
|
+
mov.save
|
127
|
+
mov2 = QuickTime::Movie.open(path)
|
128
|
+
mov2.audio_tracks.should be_empty
|
129
|
+
end
|
130
|
+
|
131
|
+
it "save should raise exception when saving new movie without filepath" do
|
132
|
+
mov = QuickTime::Movie.empty
|
133
|
+
lambda { mov.save }.should raise_error
|
134
|
+
end
|
135
|
+
|
120
136
|
it "export_pict should output a pict file at a given duration" do
|
121
137
|
path = File.dirname(__FILE__) + '/../output/example.pct'
|
122
138
|
File.delete(path) rescue nil
|
@@ -137,6 +153,15 @@ describe QuickTime::Movie do
|
|
137
153
|
@movie.poster_time = 2.1
|
138
154
|
@movie.poster_time.should == 2.1
|
139
155
|
end
|
156
|
+
|
157
|
+
it "should overlay 2nd movie with transparency" do
|
158
|
+
m2 = QuickTime::Movie.open(File.dirname(__FILE__) + '/../fixtures/dot.png')
|
159
|
+
@movie.composite_movie(m2, 0)
|
160
|
+
@movie.video_tracks.last.enable_alpha
|
161
|
+
File.delete(File.dirname(__FILE__) + '/../output/transparent.mov') rescue nil
|
162
|
+
@movie.flatten(File.dirname(__FILE__) + '/../output/transparent.mov')
|
163
|
+
# this test needs to be checked manually by looking at the output movie
|
164
|
+
end
|
140
165
|
end
|
141
166
|
|
142
167
|
describe "empty movie" do
|
@@ -6,7 +6,7 @@ describe QuickTime::Track do
|
|
6
6
|
@movie = QuickTime::Movie.open(File.dirname(__FILE__) + '/../fixtures/example.mov')
|
7
7
|
end
|
8
8
|
|
9
|
-
describe "
|
9
|
+
describe "video track" do
|
10
10
|
before(:each) do
|
11
11
|
@track = @movie.video_tracks.first
|
12
12
|
end
|
@@ -49,9 +49,37 @@ describe QuickTime::Track do
|
|
49
49
|
@track.offset = 2.5
|
50
50
|
@track.offset.should == 2.5
|
51
51
|
end
|
52
|
+
|
53
|
+
it "should be able to scale size of track" do
|
54
|
+
@track.scale(0.5, 0.5)
|
55
|
+
@track.width.should == 30
|
56
|
+
@track.height.should == 25
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be able to move (translate) position of track" do
|
60
|
+
@track.translate(10, 20)
|
61
|
+
@track.bounds[:left].should == 10
|
62
|
+
@track.bounds[:top].should == 20
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should reset transformations" do
|
66
|
+
@track.scale(0.5, 0.5)
|
67
|
+
@track.translate(10, 20)
|
68
|
+
@track.reset_transformations
|
69
|
+
@track.bounds[:left].should == 0
|
70
|
+
@track.bounds[:top].should == 0
|
71
|
+
@track.bounds[:right].should == 60
|
72
|
+
@track.bounds[:bottom].should == 50
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should rotate track" do
|
76
|
+
@track.rotate(90)
|
77
|
+
@track.width.should == 50
|
78
|
+
@track.height.should == 60
|
79
|
+
end
|
52
80
|
end
|
53
|
-
|
54
|
-
describe "
|
81
|
+
|
82
|
+
describe "audio track" do
|
55
83
|
before(:each) do
|
56
84
|
@track = @movie.audio_tracks.first
|
57
85
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rmov
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Bates
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2009-07-28 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -52,6 +52,7 @@ files:
|
|
52
52
|
- Manifest
|
53
53
|
- Rakefile
|
54
54
|
- README.rdoc
|
55
|
+
- spec/fixtures/dot.png
|
55
56
|
- spec/fixtures/settings.st
|
56
57
|
- spec/quicktime/exporter_spec.rb
|
57
58
|
- spec/quicktime/movie_spec.rb
|
@@ -64,6 +65,8 @@ files:
|
|
64
65
|
- rmov.gemspec
|
65
66
|
has_rdoc: true
|
66
67
|
homepage: http://github.com/ryanb/rmov
|
68
|
+
licenses: []
|
69
|
+
|
67
70
|
post_install_message:
|
68
71
|
rdoc_options:
|
69
72
|
- --line-numbers
|
@@ -90,9 +93,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
93
|
requirements: []
|
91
94
|
|
92
95
|
rubyforge_project: rmov
|
93
|
-
rubygems_version: 1.
|
96
|
+
rubygems_version: 1.3.3
|
94
97
|
signing_key:
|
95
|
-
specification_version:
|
98
|
+
specification_version: 3
|
96
99
|
summary: Ruby wrapper for the QuickTime C API.
|
97
100
|
test_files: []
|
98
101
|
|