client_for_poslynx 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/CHANGELOG +6 -0
- data/README.md +3 -0
- data/foo.svg +20 -0
- data/lib/client_for_poslynx/bit_sequence.rb +139 -0
- data/lib/client_for_poslynx/data/requests/can_visit.rb +1 -0
- data/lib/client_for_poslynx/data/requests/credit_card_sale.rb +1 -0
- data/lib/client_for_poslynx/data/requests/pin_pad_get_signature.rb +17 -0
- data/lib/client_for_poslynx/data/requests.rb +1 -0
- data/lib/client_for_poslynx/data/responses/credit_card_sale.rb +1 -0
- data/lib/client_for_poslynx/data/responses/pin_pad_get_signature.rb +18 -0
- data/lib/client_for_poslynx/data/responses.rb +1 -0
- data/lib/client_for_poslynx/experimental.rb +16 -0
- data/lib/client_for_poslynx/has_client_console_support.rb +7 -1
- data/lib/client_for_poslynx/message_handling/xml_extractor.rb +3 -2
- data/lib/client_for_poslynx/signature_image/draw.rb +43 -0
- data/lib/client_for_poslynx/signature_image/move.rb +43 -0
- data/lib/client_for_poslynx/signature_image/to_svg_converter.rb +99 -0
- data/lib/client_for_poslynx/signature_image.rb +97 -0
- data/lib/client_for_poslynx/version.rb +1 -1
- data/spec/client_for_poslynx/bit_sequence_spec.rb +147 -0
- data/spec/client_for_poslynx/data/requests/credit_card_sale_spec.rb +15 -11
- data/spec/client_for_poslynx/data/requests/pin_pad_get_signature_spec.rb +72 -0
- data/spec/client_for_poslynx/data/responses/credit_card_sale_spec.rb +4 -0
- data/spec/client_for_poslynx/data/responses/pin_pad_display_message_spec.rb +14 -18
- data/spec/client_for_poslynx/signature_image/to_svg_converter_spec.rb +114 -0
- data/spec/client_for_poslynx/signature_image_spec.rb +91 -0
- data/spec/spec_helper.rb +1 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZmJmMWFiZTY5MDAyYWRiMDM4ZGUxY2ZhYzJhZjEwNmJmMDc3ODU3NQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
NmZkZTU4MGVlOGVjY2VkMzgxNjU2MjdhM2E3MDFhOWUyNmQyOWY1Yg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YTliZjg2OGE3MjIwYjMwNzFiZmI5ZjVhZGVkMDRmZWE2ZmVkM2E0ZWQwZjk5
|
10
|
+
NjQ4NzUxNzg5OGI0NGZhZGVjNGY3YjI0ZGNiYTZkNDAxMDVlNDE3ZjY0Y2U0
|
11
|
+
ZDkxOThmODRmMWYwMTI5ZjRkMWVlM2YwZGVjNzgxMThiYTliNWY=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YmEwNTQ1NzZlM2RhNjc1YzNlZTg0ZDk2NDhhMmQ1OWMxODZhMjY2YWZkYjkw
|
14
|
+
ZDg3NWJiZTJkOWZiMDY2ODY5MTkyODZlMTlmMTlhNmVlODNlNzcwY2UzMTFl
|
15
|
+
NDRmZjlhNGViZWZhNjk2ZjJlZjM4OWE1YzNjZmUxMjkxNmZmY2Q=
|
data/CHANGELOG
CHANGED
@@ -23,3 +23,9 @@ Version 0.2.3
|
|
23
23
|
Version 0.2.4
|
24
24
|
- Fix bug: Fake terminal script crashes during user interaction
|
25
25
|
for a debit sale request without a specified cash back amount.
|
26
|
+
|
27
|
+
Version 0.2.4
|
28
|
+
- Introduce experimental signature image parsing and conversion
|
29
|
+
to SVG.
|
30
|
+
- Fix bug: New format of POSLynx response message XML was causing
|
31
|
+
client to hang.
|
data/README.md
CHANGED
@@ -45,6 +45,9 @@ lib/client_for_poslynx/has_client_colsole_support.rb file
|
|
45
45
|
provides a good example of how to use the facilities that this
|
46
46
|
gem provides.
|
47
47
|
|
48
|
+
Some releases may also include experimental features that must
|
49
|
+
be separately loaded by requiring "client_for_poslynx/experimental'.
|
50
|
+
|
48
51
|
## Known Limitations
|
49
52
|
|
50
53
|
* Only a subset of the possible messages and elements is supported.
|
data/foo.svg
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
<svg version="1.1"
|
2
|
+
width="67.17mm" height="13.434mm"
|
3
|
+
viewBox="0 0 640 128"
|
4
|
+
preserveAspectRatio='none'
|
5
|
+
xmlns="http://www.w3.org/2000/svg"
|
6
|
+
>
|
7
|
+
|
8
|
+
<path d='M 40,10 l 0,30 0,30 -5,5 -3,1 -3,-1 -5,-5
|
9
|
+
M 24,10 l 30,0
|
10
|
+
M 65,40 l 10,0 6,4 4,6 0,15 -4,6 -6,4 -10,0 -6,-4 -4,-6 0,-15, 4,-6
|
11
|
+
M 93,40 l 14,28 5,10
|
12
|
+
M 130,40 l -15,30 -15,30 M 160,50 l 0,0'
|
13
|
+
stroke='black'
|
14
|
+
stroke-width='1mm'
|
15
|
+
stroke-linejoin='round'
|
16
|
+
stroke-linecap='square'
|
17
|
+
fill='none'
|
18
|
+
/>
|
19
|
+
|
20
|
+
</svg>
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# coding: binary
|
2
|
+
|
3
|
+
module ClientForPoslynx
|
4
|
+
class BitSequence
|
5
|
+
class TooManyBitsLong < StandardError ; end
|
6
|
+
class NumberOutOfBounds < StandardError ; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
private :new
|
10
|
+
|
11
|
+
def from_packed_bits(packed_string)
|
12
|
+
digits_string = packed_string.unpack('B*').first
|
13
|
+
from_bit_digits( digits_string )
|
14
|
+
end
|
15
|
+
|
16
|
+
alias ^ from_packed_bits
|
17
|
+
|
18
|
+
def new_empty
|
19
|
+
from_bit_digits('')
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_bit_digits(digits_string)
|
23
|
+
new( digits_string )
|
24
|
+
end
|
25
|
+
|
26
|
+
alias / from_bit_digits
|
27
|
+
|
28
|
+
def from_unsigned(value, seq_length)
|
29
|
+
if seq_length > 64
|
30
|
+
raise TooManyBitsLong, "Can't build a representation more than 64 bits long from an unsigned number"
|
31
|
+
end
|
32
|
+
if value < 0
|
33
|
+
raise NumberOutOfBounds, "Can't build an unsigned representation of a negative number"
|
34
|
+
end
|
35
|
+
if value >= 2 ** seq_length
|
36
|
+
raise NumberOutOfBounds, "The largest value representable in #{seq_length} bits is less than #{value}"
|
37
|
+
end
|
38
|
+
packed_bits = [ value ].pack('Q>')
|
39
|
+
bit_seq = from_packed_bits( packed_bits )
|
40
|
+
bit_seq.shift( 64 - seq_length )
|
41
|
+
bit_seq
|
42
|
+
end
|
43
|
+
|
44
|
+
def from_sign_and_magnitude_of(value, seq_length)
|
45
|
+
magnitude = value.abs
|
46
|
+
magnitude_seq = from_unsigned( magnitude, seq_length - 1 )
|
47
|
+
sign_bit = from_bit_digits( value < 0 ? '1' : '0' )
|
48
|
+
magnitude_seq.unshift( sign_bit )
|
49
|
+
end
|
50
|
+
|
51
|
+
def from_uuencoded(uuencoded)
|
52
|
+
packed_bits = uuencoded.unpack('u').first
|
53
|
+
from_packed_bits( packed_bits )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(digits_string)
|
58
|
+
@digits_string = digits_string
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
"#<#{self.class.name}: #{pretty_digits}>"
|
63
|
+
end
|
64
|
+
|
65
|
+
def pretty_digits
|
66
|
+
nibbles = digits_string.scan(/.{1,4}/)
|
67
|
+
bytes = nibbles.each_slice(2).map{ |n| n.join(' ') }
|
68
|
+
bytes.join(' ')
|
69
|
+
end
|
70
|
+
|
71
|
+
def ==(other)
|
72
|
+
return false unless other.respond_to?( :to_bit_digits )
|
73
|
+
digits_string == other.to_bit_digits
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_packed_bits
|
77
|
+
[ digits_string ].pack('B*')
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_bit_digits
|
81
|
+
digits_string
|
82
|
+
end
|
83
|
+
|
84
|
+
def uuencode
|
85
|
+
[ to_packed_bits ].pack('u')
|
86
|
+
end
|
87
|
+
|
88
|
+
def length
|
89
|
+
digits_string.length
|
90
|
+
end
|
91
|
+
|
92
|
+
def first_bit_digit
|
93
|
+
digits_string[0..0]
|
94
|
+
end
|
95
|
+
|
96
|
+
def push(sequence)
|
97
|
+
digits_string << sequence.to_bit_digits
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
alias << push
|
102
|
+
|
103
|
+
def shift(bit_count)
|
104
|
+
digits_taken = digits_string[0...bit_count]
|
105
|
+
digits_string[0...bit_count] = ''
|
106
|
+
self.class.from_bit_digits( digits_taken )
|
107
|
+
end
|
108
|
+
|
109
|
+
def unshift( bit_sequence )
|
110
|
+
digits_string[0...0] = bit_sequence.to_bit_digits
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def as_unsigned
|
115
|
+
if length > 64
|
116
|
+
raise TooManyBitsLong,
|
117
|
+
"Cannot coerce sequence longer than 64 bits to unsigned number"
|
118
|
+
end
|
119
|
+
little_endian_digits = digits_string.reverse + '0' * 64
|
120
|
+
packed_little_endian = [ little_endian_digits ].pack('b*')
|
121
|
+
packed_little_endian.unpack('Q<').first
|
122
|
+
end
|
123
|
+
|
124
|
+
def as_sign_and_magnitude
|
125
|
+
if length > 65
|
126
|
+
raise TooManyBitsLong,
|
127
|
+
"Cannot coerce sequence longer than 65 bits to unsigned number"
|
128
|
+
end
|
129
|
+
magnitude_seq = self.class.from_bit_digits( digits_string[1..-1] )
|
130
|
+
magnitude = magnitude_seq.as_unsigned
|
131
|
+
first_bit_digit == '1' ? - magnitude : magnitude
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
attr_reader :digits_string
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -11,6 +11,7 @@ module ClientForPoslynx
|
|
11
11
|
def visit_PinPadInitialize(visitee) ; visit_general visitee ; end
|
12
12
|
def visit_PinPadDisplayMessage(visitee) ; visit_general visitee ; end
|
13
13
|
def visit_PinPadDisplaySpecifiedForm(visitee) ; visit_general visitee ; end
|
14
|
+
def visit_PinPadGetSignature(visitee) ; visit_general visitee ; end
|
14
15
|
|
15
16
|
def visit_general(visitee) ; end
|
16
17
|
|
@@ -20,6 +20,7 @@ module ClientForPoslynx
|
|
20
20
|
attr_element_mapping attribute: :track_1, element: 'Track1'
|
21
21
|
attr_element_mapping attribute: :card_number, element: 'CardNumber'
|
22
22
|
attr_element_mapping attribute: :expiry_date, element: 'ExpiryDate'
|
23
|
+
attr_element_mapping attribute: :capture_signature, element: 'ReqPPSigCapture'
|
23
24
|
|
24
25
|
end
|
25
26
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require_relative 'abstract_request'
|
4
|
+
|
5
|
+
module ClientForPoslynx
|
6
|
+
module Data
|
7
|
+
module Requests
|
8
|
+
|
9
|
+
class PinPadGetSignature < AbstractRequest
|
10
|
+
|
11
|
+
defining_property_value attribute: :command, element: 'Command', value: 'PPGETSIGNATURE'
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -5,6 +5,7 @@ require_relative 'requests/debit_card_sale'
|
|
5
5
|
require_relative 'requests/pin_pad_initialize'
|
6
6
|
require_relative 'requests/pin_pad_display_message'
|
7
7
|
require_relative 'requests/pin_pad_display_specified_form'
|
8
|
+
require_relative 'requests/pin_pad_get_signature'
|
8
9
|
require_relative 'requests/can_visit'
|
9
10
|
|
10
11
|
module ClientForPoslynx
|
@@ -23,6 +23,7 @@ module ClientForPoslynx
|
|
23
23
|
attr_element_mapping attribute: :transaction_date, element: 'TransactionDate'
|
24
24
|
attr_element_mapping attribute: :transaction_time, element: 'TransactionTime'
|
25
25
|
attr_element_mapping attribute: :input_method, element: 'InputMethod'
|
26
|
+
attr_element_mapping attribute: :signature, element: 'Signature'
|
26
27
|
attr_element_mapping attribute: :receipt, element: 'Receipt', numbered_lines: 'Receipt%d'
|
27
28
|
attr_element_mapping attribute: :customer_receipt, element: 'ReceiptCustomer', numbered_lines: 'Receipt%d'
|
28
29
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require_relative 'abstract_response'
|
4
|
+
|
5
|
+
module ClientForPoslynx
|
6
|
+
module Data
|
7
|
+
module Responses
|
8
|
+
|
9
|
+
class PinPadGetSignature < AbstractResponse
|
10
|
+
|
11
|
+
defining_property_value attribute: :command, element: 'Command', value: 'PPGETSIGNATURE'
|
12
|
+
attr_element_mapping attribute: :signature, element: 'Signature'
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -5,6 +5,7 @@ require_relative 'responses/debit_card_sale'
|
|
5
5
|
require_relative 'responses/pin_pad_initialize'
|
6
6
|
require_relative 'responses/pin_pad_display_message'
|
7
7
|
require_relative 'responses/pin_pad_display_specified_form'
|
8
|
+
require_relative 'responses/pin_pad_get_signature'
|
8
9
|
|
9
10
|
module ClientForPoslynx
|
10
11
|
module Data
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Require this file to load experimental features that
|
4
|
+
# are not yet expected to be fully usable.
|
5
|
+
|
6
|
+
# Bit sequence is actually in pretty good shape, but it's
|
7
|
+
# here because it is only used by SignatureImage.
|
8
|
+
require "client_for_poslynx/bit_sequence"
|
9
|
+
|
10
|
+
# SignatureImage currently only supports the legacy
|
11
|
+
# Hypercom/Equinox signature data format, which is useful
|
12
|
+
# to approximately 0% of POSLynx users. It has also not
|
13
|
+
# been tested with real data produced by a payment terminal,
|
14
|
+
# so it is possible that some understandings about the
|
15
|
+
# format are incorrect.
|
16
|
+
require "client_for_poslynx/signature_image"
|
@@ -22,7 +22,7 @@ module ClientForPoslynx
|
|
22
22
|
conn = TCPSocket.new( config.host, config.port )
|
23
23
|
conn.puts request.xml_serialize
|
24
24
|
response = get_response_from( conn )
|
25
|
-
conn.close unless conn.
|
25
|
+
conn.close unless conn.closed?
|
26
26
|
response
|
27
27
|
end
|
28
28
|
|
@@ -81,6 +81,12 @@ module ClientForPoslynx
|
|
81
81
|
}
|
82
82
|
end
|
83
83
|
|
84
|
+
def example_pin_pad_get_signature
|
85
|
+
ClientForPoslynx::Data::Requests::PinPadGetSignature.new.tap { |req|
|
86
|
+
assign_common_example_request_attrs_to req
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
84
90
|
private
|
85
91
|
|
86
92
|
def get_response_from( connection )
|
@@ -23,11 +23,12 @@ module ClientForPoslynx
|
|
23
23
|
root_name = nil
|
24
24
|
while true
|
25
25
|
line = stream.gets
|
26
|
+
puts line
|
26
27
|
message << line
|
27
|
-
if (! root_name) && line =~
|
28
|
+
if (! root_name) && line =~ /^(?:<\?.+?\?>)?<([A-Za-z_][^\s>]*)[ >]/
|
28
29
|
root_name = $1
|
29
30
|
end
|
30
|
-
break if root_name && line =~ /<\/#{root_name}\s
|
31
|
+
break if root_name && line =~ /<\/#{root_name}\s*>\s*$/
|
31
32
|
end
|
32
33
|
message
|
33
34
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ClientForPoslynx
|
4
|
+
class SignatureImage
|
5
|
+
|
6
|
+
class Draw
|
7
|
+
DELTA_BITS_LONG = 6
|
8
|
+
BIT_SEQUENCE_LENGTH = 1 + DELTA_BITS_LONG * 2
|
9
|
+
|
10
|
+
def self.first_in_bit_sequence(bit_seq)
|
11
|
+
bit_seq.first_bit_digit == '0' &&
|
12
|
+
bit_seq.length >= BIT_SEQUENCE_LENGTH
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse_from_bit_sequence!(bit_seq)
|
16
|
+
bit_seq.shift 1
|
17
|
+
dx_bit_seq = bit_seq.shift( DELTA_BITS_LONG )
|
18
|
+
dy_bit_seq = bit_seq.shift( DELTA_BITS_LONG )
|
19
|
+
new( dx_bit_seq.as_sign_and_magnitude, dy_bit_seq.as_sign_and_magnitude )
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :dx, :dy
|
23
|
+
|
24
|
+
def initialize(dx, dy)
|
25
|
+
@dx = dx
|
26
|
+
@dy = dy
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
return false unless self.class === other
|
31
|
+
return dx == other.dx && dy == other.dy
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_bit_sequence
|
35
|
+
bit_seq = ClientForPoslynx::BitSequence / '0'
|
36
|
+
bit_seq << ClientForPoslynx::BitSequence.from_sign_and_magnitude_of( dx, DELTA_BITS_LONG )
|
37
|
+
bit_seq << ClientForPoslynx::BitSequence.from_sign_and_magnitude_of( dy, DELTA_BITS_LONG )
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ClientForPoslynx
|
4
|
+
class SignatureImage
|
5
|
+
|
6
|
+
class Move
|
7
|
+
X_BITS_LONG = 10
|
8
|
+
Y_BITS_LONG = 7
|
9
|
+
BIT_SEQUENCE_LENGTH = 1 + X_BITS_LONG + Y_BITS_LONG
|
10
|
+
|
11
|
+
def self.first_in_bit_sequence(bit_seq)
|
12
|
+
bit_seq.first_bit_digit == '1' &&
|
13
|
+
bit_seq.length >= BIT_SEQUENCE_LENGTH
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse_from_bit_sequence!(bit_seq)
|
17
|
+
bit_seq.shift 1
|
18
|
+
x_bit_seq = bit_seq.shift( X_BITS_LONG )
|
19
|
+
y_bit_seq = bit_seq.shift( Y_BITS_LONG )
|
20
|
+
new( x_bit_seq.as_unsigned, y_bit_seq.as_unsigned )
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :x, :y
|
24
|
+
|
25
|
+
def initialize(x,y)
|
26
|
+
@x = x
|
27
|
+
@y = y
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
return false unless self.class === other
|
32
|
+
return x == other.x && y == other.y
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_bit_sequence
|
36
|
+
bit_seq = ClientForPoslynx::BitSequence / '1'
|
37
|
+
bit_seq << ClientForPoslynx::BitSequence.from_unsigned( x, X_BITS_LONG )
|
38
|
+
bit_seq << ClientForPoslynx::BitSequence.from_unsigned( y, Y_BITS_LONG )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ClientForPoslynx
|
4
|
+
class SignatureImage
|
5
|
+
|
6
|
+
class ToSvgConverter
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
private :new
|
11
|
+
|
12
|
+
def convert( signature_image )
|
13
|
+
new( signature_image ).call
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :signature_image
|
19
|
+
private :signature_image=
|
20
|
+
|
21
|
+
def initialize( signature_image )
|
22
|
+
self.signature_image = signature_image
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
apply_document_characteristics
|
27
|
+
|
28
|
+
path_el = svg_document.create_element('path')
|
29
|
+
svg_element.add_child path_el
|
30
|
+
|
31
|
+
apply_path_characteristics_to path_el
|
32
|
+
|
33
|
+
path_el['d'] = path_data
|
34
|
+
|
35
|
+
svg_document.to_xml
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def apply_document_characteristics
|
41
|
+
svg_element['width' ] = '67.17mm'
|
42
|
+
svg_element['height' ] = '13.434mm'
|
43
|
+
svg_element['viewBox' ] = '0 0 640 128'
|
44
|
+
svg_element['preserveAspectRatio'] = 'none'
|
45
|
+
end
|
46
|
+
|
47
|
+
def svg_element
|
48
|
+
svg_document.root
|
49
|
+
end
|
50
|
+
|
51
|
+
def svg_document
|
52
|
+
@svg_document ||= begin
|
53
|
+
doc = Nokogiri::XML::Document.new
|
54
|
+
root_el = doc.root = doc.create_element('svg')
|
55
|
+
root_el.default_namespace = 'http://www.w3.org/2000/svg'
|
56
|
+
root_el['version'] = '1.1'
|
57
|
+
doc
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def apply_path_characteristics_to(path_el)
|
62
|
+
apply_stroke_styling_to path_el
|
63
|
+
path_el['fill'] = 'none'
|
64
|
+
end
|
65
|
+
|
66
|
+
def apply_stroke_styling_to(path_el)
|
67
|
+
path_el['stroke' ] = 'black'
|
68
|
+
path_el['stroke-width' ] = '1mm'
|
69
|
+
path_el['stroke-linejoin'] = 'round'
|
70
|
+
# Square line cap improves visibility of zero-length
|
71
|
+
# lines (dots).
|
72
|
+
path_el['stroke-linecap' ] = 'square'
|
73
|
+
end
|
74
|
+
|
75
|
+
def path_data
|
76
|
+
data = ''
|
77
|
+
signature_image.shape_step_groups.each do |shape_steps|
|
78
|
+
data << abs_moveto_instruction_for_move_step( shape_steps.first )
|
79
|
+
data << rel_lineto_instruction_for_draw_steps( shape_steps[1..-1] )
|
80
|
+
end
|
81
|
+
data
|
82
|
+
end
|
83
|
+
|
84
|
+
def abs_moveto_instruction_for_move_step(step)
|
85
|
+
"M #{step.x},#{step.y} "
|
86
|
+
end
|
87
|
+
|
88
|
+
def rel_lineto_instruction_for_draw_steps(steps)
|
89
|
+
return 'l 0,0 ' if steps.empty?
|
90
|
+
|
91
|
+
steps.reduce( 'l ' ) { |instruction, step|
|
92
|
+
instruction << "#{step.dx},#{step.dy} "
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require_relative 'signature_image/move'
|
4
|
+
require_relative 'signature_image/draw'
|
5
|
+
require_relative 'signature_image/to_svg_converter'
|
6
|
+
|
7
|
+
module ClientForPoslynx
|
8
|
+
|
9
|
+
class SignatureImage
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def deserialize(serialized_data)
|
14
|
+
packed_binary = uudecode( serialized_data )
|
15
|
+
bit_seq = ClientForPoslynx::BitSequence.from_packed_bits( packed_binary )
|
16
|
+
|
17
|
+
sig_image = new
|
18
|
+
step_types = [ SignatureImage::Move, SignatureImage::Draw ]
|
19
|
+
while true
|
20
|
+
step_type = step_types.detect{ |st| st.first_in_bit_sequence( bit_seq ) }
|
21
|
+
break if step_type.nil?
|
22
|
+
step = step_type.parse_from_bit_sequence!( bit_seq )
|
23
|
+
sig_image << step
|
24
|
+
end
|
25
|
+
|
26
|
+
sig_image
|
27
|
+
end
|
28
|
+
|
29
|
+
def uudecode(uuencoded)
|
30
|
+
uuencoded.unpack('u').first
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
return false unless self.class === other
|
37
|
+
sequence == other.sequence
|
38
|
+
end
|
39
|
+
|
40
|
+
def move(*args)
|
41
|
+
sequence << Move.new(*args)
|
42
|
+
end
|
43
|
+
|
44
|
+
def draw(*args)
|
45
|
+
sequence << Draw.new(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def <<(step)
|
49
|
+
sequence << step
|
50
|
+
end
|
51
|
+
|
52
|
+
def each_step
|
53
|
+
sequence.each do |step| ; yield step ; end
|
54
|
+
end
|
55
|
+
|
56
|
+
def shape_step_groups
|
57
|
+
groups = []
|
58
|
+
group = []
|
59
|
+
sequence.each do |step|
|
60
|
+
if SignatureImage::Move === step
|
61
|
+
group = []
|
62
|
+
groups << group
|
63
|
+
end
|
64
|
+
group << step
|
65
|
+
end
|
66
|
+
groups
|
67
|
+
end
|
68
|
+
|
69
|
+
def serialize_legacy
|
70
|
+
unless Move === sequence.first
|
71
|
+
raise 'Must have an initial move-type step in order to serialize'
|
72
|
+
end
|
73
|
+
bit_seq = ClientForPoslynx::BitSequence.new_empty
|
74
|
+
sequence.each do |step|
|
75
|
+
bit_seq << step.to_bit_sequence
|
76
|
+
end
|
77
|
+
bit_seq.uuencode
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def sequence
|
83
|
+
@sequence ||= []
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def pack_binary_digits(digits_string)
|
89
|
+
[ digits_string ].pack('B*')
|
90
|
+
end
|
91
|
+
|
92
|
+
def uuencode_packed_bits(packed_bits_string)
|
93
|
+
[ packed_bits_string ].pack('u')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|