1 module y4md;
2 
3 import std.stdio,
4        std.string,
5        std.algorithm,
6        std.conv;
7 
8 struct Rational
9 {
10     int num;
11     int denom;
12 }
13 
14 class Y4MException : Exception
15 {
16     public
17     {
18         @safe pure nothrow this(string message, string file =__FILE__, size_t line = __LINE__, Throwable next = null)
19         {
20             super(message, file, line, next);
21         }
22     }
23 }
24 
25 enum Interlacing
26 {
27     Progressive,
28     TopFieldFirst,
29     BottomFieldFirst,
30     MixedModes
31 }
32 
33 
34 enum Subsampling
35 {
36     C420,     /// 4:2:0 with coincident chroma planes
37     C422,     /// 4:2:2 with coincident chroma planes
38     C444,     /// 4:4:4 with coincident chroma planes
39     C420jpeg, /// 4:2:0 with biaxially-displaced chroma planes
40     C420paldv, /// 4:2:0 with vertically-displaced chroma planes
41     C420mpeg2
42 }
43 
44 bool is420Subsampling(Subsampling sub) pure nothrow @nogc
45 {
46     final switch (sub) with (Subsampling)
47     {
48         case C420: 
49         case C420jpeg:
50         case C420paldv:
51         case C420mpeg2:
52             return true;
53 
54         case C422:
55         case C444:
56             return false;
57     }
58 }
59 
60 struct Y4MDesc
61 {
62     int width = 0;
63     int height = 0;
64     Rational framerate = Rational(0,0);
65     Rational pixelAR = Rational(0, 0); // default: unknown
66     Interlacing interlacing = Interlacing.Progressive;
67     Subsampling subsampling = Subsampling.C420;
68     int bitdepth = 8;
69 
70     size_t sampleSize() pure const
71     {
72         if (8 < bitdepth || bitdepth > 16)
73             throw new Y4MException(format("Bit-depth should be from 8 to 16 bits/sample, not %s.", bitdepth));
74 
75         if (bitdepth == 8)
76             return 1;
77         else
78             return 2;
79     }
80 
81     size_t frameSize() pure const
82     {
83         size_t oneSample = sampleSize();
84 
85         final switch (subsampling)
86         {
87             case Subsampling.C420:
88             case Subsampling.C420jpeg:
89             case Subsampling.C420paldv:
90             case Subsampling.C420mpeg2:
91                 return oneSample * (width * height * 3) / 2;
92 
93             case Subsampling.C422:
94                 return oneSample * width * height * 2;
95 
96             case Subsampling.C444:
97                 return oneSample * width * height * 3;
98         }
99     }
100 }
101 
102 
103 /// Instantiate this to read sequentially frame of a Y4M file.
104 class Y4MReader
105 {
106     public
107     {
108         Y4MDesc desc;
109         alias desc this;
110 
111         this(string filename)
112         {
113             this(File(filename, "rb"));
114         }
115 
116         this(File file)
117         {
118             _file = file;
119             _index = 0;
120             _hasPeek = false;
121             fetchHeader();
122 
123             _frameBuffer.length = frameSize();
124         }
125 
126         // null if no more frames
127         ubyte[] readFrame()
128         {
129             if (_file.eof)
130                 return null;
131 
132             // Not all input are seekable
133             //if (_index == _file.size())
134             //    return null; // end of input
135 
136             // read 5 bytes
137             string frame = "FRAME";
138 
139             for (int i = 0; i < 5; ++i)
140                 if (frame[i] != next())
141                     throw new Y4MException("Expected \"FRAME\" in y4m.");
142 
143             fetchParamList();
144 
145             _index += _frameBuffer.length;
146             ubyte[] res = _file.rawRead!ubyte(_frameBuffer[]);
147             if (res.length != _frameBuffer.length)
148                 throw new Y4MException(format("Incomplete frame at end of y4m file: expected %s bytes, got %s.", _frameBuffer.length, res.length));
149 
150             return res;
151         }
152     }
153 
154     private
155     {
156         ubyte[] _frameBuffer;
157         File _file;
158         size_t _index;
159         ubyte _peeked;
160         bool _hasPeek;
161         
162 
163         /// Returns: current byte in input and do not advance cursor.
164         ubyte peek()
165         {
166             if (!_hasPeek)
167             {
168                 ubyte[1] buf;
169                 ubyte[] res = _file.rawRead!ubyte(buf[0..1]);
170                 _index += 1;
171 
172                 if (res.length != 1)
173                     throw new Y4MException("Wrong y4m, not enough bytes.");
174 
175                 _peeked = buf[0];
176 
177                 _hasPeek = true;
178             }
179             return _peeked;
180         }
181 
182         /// Returns: current byte in input and advance cursor.
183         ubyte next()
184         {
185             ubyte current = peek();
186             _hasPeek = false;
187             return current;
188         }
189 
190         void fetchHeader()
191         {
192             // read first 9 bytes
193             string magic = "YUV4MPEG2";
194 
195             for (int i = 0; i < 9; ++i)
196                 if (magic[i] != next())
197                     throw new Y4MException("Wrong y4m header.");
198 
199             fetchParamList();
200         }
201 
202         void fetchParamList()
203         {
204             Rational parseRatio(string p, Rational defaultValue)
205             {
206                 if (p.length < 3)
207                     throw new Y4MException("Wrong y4m header, missing chars in fraction.");
208 
209                 ptrdiff_t index = countUntil(p, ":");
210                 if (index == -1)
211                     throw new Y4MException("Wrong y4m header, expected ':' in fraction.");
212 
213                 if (index == 0)
214                     throw new Y4MException("Wrong y4m header, missing numerator in fraction.");
215 
216                 if (index + 1 == p.length)
217                     throw new Y4MException("Wrong y4m header, missing denominator in fraction.");
218 
219                 int num = to!int(p[0..index]);
220                 int denom = to!int(p[index + 1..$]);
221 
222                 if (denom == 0)
223                     return defaultValue;
224 
225                 return Rational(num, denom);
226             }
227 
228             string param;
229             while ( (param = fetchParam()) !is null)
230             {
231                 if (param[0] == 'W')
232                 {
233                     width = to!int(param[1..$]);
234                 }
235                 else if (param[0] == 'H')
236                 {
237                     height = to!int(param[1..$]);
238                 }
239                 else if (param[0] == 'F')
240                 {
241                     framerate = parseRatio(param[1..$], Rational(0));
242                 }
243                 else if (param[0] == 'I')
244                 {
245                     bool known = false;
246                     for(auto inter = Interlacing.min; inter <= Interlacing.max; ++inter)
247                     {
248                         if (param == interlacingString(inter))
249                         {
250                             interlacing = inter;
251                             known = true;
252                             break;
253                         }
254                     }
255                     if (!known)
256                         throw new Y4MException(format("Unsupported y4m interlacing attribute %s", param));
257                 }
258                 else if (param[0] == 'A')
259                 {
260                     pixelAR = parseRatio(param[1..$], Rational(1, 1));
261                 }
262                 else if (param[0] == 'C')
263                 {
264                     bool known = false;
265                     for(auto sub = Subsampling.min; sub <= Subsampling.max; ++sub)
266                     {
267                         string ssub = subsamplingString(sub);
268                         if (param == ssub)
269                         {
270                             subsampling = sub;
271                             known = true;
272                             break;
273                         }
274                         else if (param.length > ssub.length && param[0..ssub.length] == ssub && param[ssub.length] == 'p')
275                         {
276                             // high bit-depth support
277                             subsampling = sub;
278                             known = true;
279                             try
280                                 bitdepth = to!int(param[ssub.length+1..$]);
281                             catch(ConvException e)
282                                 throw new Y4MException(format("Expected an integer for bitdepth in colorspace attribute, got '%s' instead.", param));
283                             break;
284                         }
285                     }
286                     if (!known)
287                         throw new Y4MException(format("Unsupported y4m colorspace attribute %s", param));
288                 }
289                 else if (param[0] == 'X')
290                 {
291                     // comment, ignored
292                 }
293                 else
294                     throw new Y4MException(format("Unsupported y4m attribute %s", param));
295             }
296 
297             // check mandatory params
298             if (width == 0)
299                 throw new Y4MException(format("Missing width in y4m.", param));
300             if (height == 0)
301                 throw new Y4MException(format("Missing height in y4m.", param));
302             if (framerate.num == 0 && framerate.denom == 0)
303                 throw new Y4MException(format("Missing framerate in y4m.", param));
304 
305         }
306 
307         // read parameter (space then alphanum+)
308         // null if no parameters
309         string fetchParam()
310         {
311             ubyte b = peek();
312             if (b == '\n')
313             {
314                 next();
315                 return null; // end of parameter list
316             }
317             else if (b == ' ')
318             {
319                 next();
320                 string result = "";
321 
322                 while(true)
323                 {
324                     ubyte c = peek();
325                     if (c == ' ')
326                         break;
327                     if (c == 10)
328                         break;
329                     next();
330                     result ~= c;
331                 }
332 
333                 return result;
334             }
335             else
336                 throw new Y4MException("Wrong y4m, unexpected character.");
337 
338         }
339     }
340 }
341 
342 /// Instantiate this to write sequentially frames into a Y4M file.
343 class Y4MWriter
344 {
345     public
346     {
347         Y4MDesc desc;
348         alias desc this;
349 
350 
351         this(string filename, int width, int height, 
352              Rational framerate = Rational(0, 0),
353              Rational pixelAR = Rational(0, 0), // default: unknown
354              Interlacing interlacing = Interlacing.Progressive,
355              Subsampling subsampling = Subsampling.C420,
356              int bitdepth = 8)
357         {
358             this(File(filename, "wb"), width, height, framerate, pixelAR, interlacing, subsampling, bitdepth);
359         }
360 
361         this(File file, int width, int height, 
362              Rational framerate = Rational(0, 0),
363              Rational pixelAR = Rational(0, 0), // default: unknown
364              Interlacing interlacing = Interlacing.Progressive,
365              Subsampling subsampling = Subsampling.C420,
366              int bitdepth = 8)
367         {
368             _file = file;
369             this.width = width;
370             this.height = height;
371             this.framerate = framerate;
372             this.pixelAR = pixelAR;
373             this.interlacing = interlacing;
374             this.subsampling = subsampling;
375             this.bitdepth = bitdepth;
376 
377             string header = format("YUV4MPEG2 W%s H%s F%s:%s A%s:%s %s %s%s\n", 
378                                    width, height, framerate.num, framerate.denom, pixelAR.num, pixelAR.denom,
379                                    interlacingString(interlacing), subsamplingString(subsampling), bitdepthString(bitdepth));
380             
381 
382             _file.rawWrite!char(header[]);
383         }
384 
385         void writeFrame(ubyte[] frameData)
386         {
387             if (frameData.length != frameSize)
388                 assert(false); // unrecoverable error, contract not followed
389 
390             static immutable string FRAME = "FRAME\n";
391 
392             _file.rawWrite!char(FRAME[]);
393             _file.rawWrite!ubyte(frameData[]);
394         }
395     }
396 
397     private
398     {
399         File _file;
400     }
401 }
402 
403 private
404 {
405     string interlacingString(Interlacing interlacing) pure nothrow
406     {
407         final switch(interlacing)
408         {
409             case Interlacing.Progressive:      return "Ip";
410             case Interlacing.TopFieldFirst:    return "It";
411             case Interlacing.BottomFieldFirst: return "Ib";
412             case Interlacing.MixedModes:       return "Im";
413         }
414     }
415 
416     string subsamplingString(Subsampling subsampling) pure nothrow
417     {
418         final switch(subsampling)
419         {
420             case Subsampling.C420:      return "C420";
421             case Subsampling.C422:      return "C422";
422             case Subsampling.C444:      return "C444";
423             case Subsampling.C420jpeg:  return "C420jpeg";
424             case Subsampling.C420paldv: return "C420paldv";
425             case Subsampling.C420mpeg2: return "C420mpeg2";
426         }
427     }
428 
429     string bitdepthString(int bitdepth) pure
430     {
431         if (bitdepth == 8)
432             return "";
433         else
434             return "p" ~ to!string(bitdepth);
435     }
436 }