client_for_poslynx 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|