1 module sand.api; 2 3 import sand.sane; 4 import sand.saneopts; 5 6 import std.exception : enforce, assertThrown; 7 import std.algorithm.iteration, std.string; 8 import std.conv, std.range, std.utf; 9 import std.algorithm: canFind; 10 import std.stdio; 11 import std.traits; 12 import core.thread; 13 14 15 /** The D interface to SANE */ 16 @system 17 class Sane { 18 int versionMajor, versionMinor, versionBuild; 19 Device[] m_devices; 20 21 this() { 22 init(); 23 } 24 25 ~this() { 26 sane_exit(); 27 } 28 29 private void init() { 30 int api_version; 31 auto status = sane_init(&api_version, null); 32 enforce(status == SANE_Status.SANE_STATUS_GOOD); 33 SANE_VERSION_CODE(versionMajor, versionMinor, versionBuild); 34 } 35 36 /** 37 * Get list of devices connected to this machine 38 * Params: 39 * force = recheck all devices 40 */ 41 auto devices(bool force=false) { 42 if(!m_devices.length || force) { 43 SANE_Device** device_list; 44 auto status = sane_get_devices(&device_list, true); 45 auto size = 0; 46 while(*(device_list + size)) 47 size++; 48 m_devices = device_list[0 .. size].map!(device => new Device(device)).array; 49 } 50 return m_devices; 51 } 52 } 53 54 enum Format { 55 GREY = 0, 56 RGB = 1, 57 RED = 2, 58 GREEN = 3, 59 BLUE = 4 60 } 61 62 struct Parameters { 63 Format frame; 64 bool lastFrame; 65 uint bytesPerLine; 66 uint pixelsPerLine; 67 uint lines; 68 uint bitdepth; 69 } 70 71 @system 72 class Device { 73 string name; 74 string vendor; 75 string model; 76 string type; 77 SANE_Device* device; 78 private Option[] m_options; 79 private SANE_Handle handle; 80 private bool open; 81 bool scanning; 82 83 this(SANE_Device* device) { 84 name = to!string((*device).name); 85 vendor = to!string((*device).vendor); 86 model = to!string((*device).model); 87 type = to!string((*device).type); 88 this.device = device; 89 sane_open(device.name, &handle); 90 } 91 92 override string toString() { 93 return format("SANE Device: %s - %s", vendor, model); 94 } 95 96 /** Get options */ 97 @property auto options() { 98 if(!open) { 99 populateOptions(); 100 open = true; 101 } 102 return m_options; 103 } 104 105 106 /** Get current scan parameters */ 107 @property Parameters parameters() { 108 SANE_Parameters p; 109 sane_get_parameters(handle, &p); 110 auto parameters = Parameters(cast(Format)p.format, 111 cast(bool)p.last_frame, 112 p.bytes_per_line, 113 p.pixels_per_line, 114 p.lines, 115 p.depth); 116 return parameters; 117 } 118 119 private void populateOptions() { 120 auto size = 0; 121 while(sane_get_option_descriptor(handle, size)) 122 size++; 123 m_options = iota(size).map!(i => new Option(handle, i, this)).array; 124 } 125 126 127 ubyte[] readImage() { 128 sane_set_io_mode(handle, true); 129 sane_start(handle); 130 SANE_Parameters params; 131 enforce(sane_get_parameters(handle, ¶ms) == SANE_Status.SANE_STATUS_GOOD); 132 auto totalBytes = params.lines * params.bytes_per_line; 133 auto data = new ubyte[totalBytes]; 134 int length, offset; 135 SANE_Status status; 136 do { 137 ubyte* ptr = data.ptr + offset; 138 status = sane_read(handle, ptr, totalBytes, &length); 139 offset += length; 140 } while (status == SANE_Status.SANE_STATUS_GOOD); 141 return data; 142 } 143 144 145 void readImageAsync(ref ubyte[] data, ref int bytesRead) { 146 scope(failure) { data = readImage(); this.scanning = false; return; } 147 this.scanning = true; 148 sane_start(this.handle); 149 bool nonBlocking; 150 enforce(SANE_Status.SANE_STATUS_GOOD == sane_set_io_mode(handle, 1)); 151 SANE_Parameters params; 152 enforce(sane_get_parameters(handle, ¶ms) == SANE_Status.SANE_STATUS_GOOD); 153 auto totalBytes = params.lines * params.bytes_per_line; 154 data = new ubyte[totalBytes]; 155 int length, offset; 156 SANE_Status status; 157 Fiber.yield(); 158 do { 159 ubyte* ptr = data.ptr + offset; 160 status = sane_read(handle, ptr, totalBytes, &length); 161 offset += length; 162 bytesRead = offset; 163 Fiber.yield(); 164 } while (status == SANE_Status.SANE_STATUS_GOOD); 165 this.scanning = false; 166 } 167 } 168 169 enum ValueType { 170 BOOL = 0, 171 GROUP = 1, 172 INT = 2, 173 FIXED = 3, 174 STRING = 4, 175 BUTTON = 5 176 } 177 178 enum ConstraintType { 179 NONE = 0, 180 RANGE = 1, 181 WORD_LIST = 2, 182 STRING_LIST = 3 183 } 184 185 @system 186 class Option { 187 int number; 188 const string name; 189 const string title; 190 const string description; 191 const string unit; 192 private SANE_Handle handle; 193 Device parent; 194 195 this(SANE_Handle handle, int number, Device parent) { 196 this.number = number; 197 auto descriptor = sane_get_option_descriptor(handle, number); 198 name = to!string((*descriptor).name); 199 title = to!string((*descriptor).title); 200 description = to!string((*descriptor).desc); 201 unit = unitToString(descriptor.unit); 202 this.handle = handle; 203 this.parent = parent; 204 } 205 206 override string toString() { 207 return format("Option:\nNumber: %s\nName: %s\nTitle: %s\nDescription: %s\nUnit: %s" ~ 208 "\nSettable: %s\nActive: %s", number, name, title, description, unit, settable(), active()); 209 } 210 211 private string unitToString(SANE_Unit unit) { 212 switch(unit) { 213 case SANE_Unit.SANE_UNIT_NONE: 214 return "(none)"; 215 case SANE_Unit.SANE_UNIT_PIXEL: 216 return "pixels"; 217 case SANE_Unit.SANE_UNIT_BIT: 218 return "bits"; 219 case SANE_Unit.SANE_UNIT_MM: 220 return "millimetres"; 221 case SANE_Unit.SANE_UNIT_DPI: 222 return "dots per inch"; 223 case SANE_Unit.SANE_UNIT_PERCENT: 224 return "percentage"; 225 case SANE_Unit.SANE_UNIT_MICROSECOND: 226 return "microseconds"; 227 default: 228 assert(0); 229 } 230 } 231 232 @property const(bool) settable() { 233 return SANE_OPTION_IS_SETTABLE(sane_get_option_descriptor(handle, number).cap); 234 } 235 236 @property const(bool) active() { 237 return SANE_OPTION_IS_ACTIVE(sane_get_option_descriptor(handle, number).cap); 238 } 239 240 @property const(bool) group() { 241 return type() == ValueType.GROUP; 242 } 243 244 @property const(ValueType) type() { 245 auto type = sane_get_option_descriptor(handle, number).type; 246 switch(type) { 247 case SANE_Value_Type.SANE_TYPE_BOOL: 248 return ValueType.BOOL; 249 case SANE_Value_Type.SANE_TYPE_GROUP: 250 return ValueType.GROUP; 251 case SANE_Value_Type.SANE_TYPE_INT: 252 return ValueType.INT; 253 case SANE_Value_Type.SANE_TYPE_FIXED: 254 return ValueType.FIXED; 255 case SANE_Value_Type.SANE_TYPE_STRING: 256 return ValueType.STRING; 257 case SANE_Value_Type.SANE_TYPE_BUTTON: 258 return ValueType.BUTTON; 259 default: 260 throw new Exception("Unknown Type"); 261 } 262 } 263 264 @property const(T) max(T)() { 265 static if(is(T == double)) { 266 return SANE_UNFIX(sane_get_option_descriptor(handle, number).constraint.range.max); 267 } else { 268 return sane_get_option_descriptor(handle, number).constraint.range.max; 269 } 270 } 271 272 @property const(T) min(T)() { 273 static if(is(T == double)) { 274 return SANE_UNFIX(sane_get_option_descriptor(handle, number).constraint.range.min); 275 } else { 276 return sane_get_option_descriptor(handle, number).constraint.range.min; 277 } 278 } 279 280 @property const(T) quant(T)() { 281 static if(is(T == double)) { 282 return SANE_UNFIX(sane_get_option_descriptor(handle, number).constraint.range.quant); 283 } else { 284 return sane_get_option_descriptor(handle, number).constraint.range.quant; 285 } 286 } 287 288 @property const(T) value(T)() { 289 auto descriptor = sane_get_option_descriptor(handle, number); 290 char[] space = new char[descriptor.size]; 291 auto status = sane_control_option(handle, number, SANE_Action.SANE_ACTION_GET_VALUE, space.ptr, null); 292 enforce(status == SANE_Status.SANE_STATUS_GOOD); 293 static if(is(T == string)) { 294 return to!string(cast(char*)(space)); 295 } else static if(is(T == double)) { 296 return SANE_UNFIX(*(cast(SANE_Fixed*)(space.ptr))); 297 } 298 else static if(isPointer!T) { 299 return cast(T)(space.ptr); 300 } else { 301 auto value = cast(T*)(space.ptr); 302 return *value; 303 } 304 } 305 306 /** 307 * Set property value 308 */ 309 @property void value(T)(T value) { 310 if(!this.parent.scanning) { 311 if(!settable()) 312 throw new Exception("Option is not settable"); 313 if(!meetsConstraint(value)) 314 throw new Exception("Value doesn't meet constriant"); 315 auto descriptor = sane_get_option_descriptor(handle, number); 316 static if(is(T == string)) { 317 auto v = cast(char*)value.toStringz(); 318 auto status = sane_control_option(handle, number, SANE_Action.SANE_ACTION_SET_VALUE, v, null); 319 enforce(status == SANE_Status.SANE_STATUS_GOOD); 320 } else static if(is(T == double)) { 321 auto v = SANE_FIX(value); 322 auto status = sane_control_option(handle, number, SANE_Action.SANE_ACTION_SET_VALUE, &v, null); 323 enforce(status == SANE_Status.SANE_STATUS_GOOD); 324 } else static if(is(T == bool)) { 325 auto v = cast(int)value; 326 auto status = sane_control_option(handle, number, SANE_Action.SANE_ACTION_SET_VALUE, &v, null); 327 enforce(status == SANE_Status.SANE_STATUS_GOOD); 328 } 329 else { 330 auto status = sane_control_option(handle, number, SANE_Action.SANE_ACTION_SET_VALUE, &value, null); 331 enforce(status == SANE_Status.SANE_STATUS_GOOD); 332 } 333 } 334 } 335 336 @property const(char*)[] strings() { 337 const(char*)[] stringList; 338 auto descriptor = sane_get_option_descriptor(handle, number); 339 switch(descriptor.constraint_type) { 340 case SANE_Constraint_Type.SANE_CONSTRAINT_STRING_LIST: 341 int position = 0; 342 while(*(descriptor.constraint.string_list + position)) { 343 stringList ~= *(descriptor.constraint.string_list + position); 344 position++; 345 } 346 break; 347 default: 348 assert(0); 349 } 350 return stringList; 351 } 352 353 @property const(int)[] words() { 354 auto descriptor = sane_get_option_descriptor(handle, number); 355 int length = *(descriptor.constraint.word_list); 356 return descriptor.constraint.word_list[1..length + 1].array; 357 } 358 359 @property ConstraintType constraintType() { 360 auto descriptor = sane_get_option_descriptor(handle, number); 361 return cast(ConstraintType)descriptor.constraint_type; 362 } 363 364 private bool meetsConstraint(T)(T value) { 365 auto descriptor = sane_get_option_descriptor(handle, number); 366 switch(descriptor.constraint_type) { 367 case SANE_Constraint_Type.SANE_CONSTRAINT_NONE: 368 return true; 369 case SANE_Constraint_Type.SANE_CONSTRAINT_RANGE: 370 auto range = descriptor.constraint.range; 371 // if(range.quant != 0) { 372 // if(value < range.min || value > range.max) 373 // return false; 374 // if((range.quant * value + range.min) <= range.max) 375 // return true; 376 // return false; 377 // } 378 return true; 379 case SANE_Constraint_Type.SANE_CONSTRAINT_WORD_LIST: 380 auto count = *descriptor.constraint.word_list; 381 auto wordList = descriptor.constraint.word_list[1..count + 1]; 382 return wordList.canFind(value); 383 default: 384 throw new Exception("Value doesn't meet constraint"); 385 } 386 } 387 388 private bool meetsConstraint(string value) { 389 auto descriptor = sane_get_option_descriptor(handle, number); 390 switch(descriptor.constraint_type) { 391 case SANE_Constraint_Type.SANE_CONSTRAINT_STRING_LIST: 392 string[] stringList; 393 int position = 0; 394 while(*(descriptor.constraint.string_list + position)) { 395 stringList ~= to!string(*(descriptor.constraint.string_list + position)); 396 position++; 397 } 398 return stringList.canFind(value); 399 default: 400 throw new Exception("Can't enfore constraint"); 401 } 402 } 403 } 404 405 unittest { 406 auto s = new Sane(); 407 auto devices = s.devices(); 408 409 // Test option setting 410 assert(devices[0].options[3].value!int == 8); 411 devices[0].options[3].value = 16; 412 assert(devices[0].options[3].value!int == 16); 413 assert(devices[0].options[3].settable); 414 assert(devices[0].options[3].active); 415 assert(devices[0].options[2].value!string == "Gray"); 416 devices[0].options[2].value!string = "Gray"; 417 assertThrown(devices[0].options[2].value = "Grey"); 418 419 420 // test non blocking io 421 // set to io to non blocking 422 devices[0].options[19].value!bool = true; 423 ubyte[] data; 424 int readBytes = 0; 425 auto f = new Fiber(()=> devices[0].readImageAsync(data, readBytes)); 426 while(f.state != Fiber.State.TERM) { 427 f.call(); 428 } 429 // reset to blocking IO 430 devices[0].options[19].value!bool = false; 431 // check image reading works 432 devices[0].readImage(); 433 readBytes = 0; 434 435 assertThrown(devices[0].options[0].value = 5); 436 assert(devices[0].options[1].group); 437 assert(!devices[0].options[2].group); 438 }