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, &params) == 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, &params) == 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 }