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 }