Sample Obj-C code for using Apryse SDK to extract text, paths, and images from a PDF. The sample also shows how to do color conversion, image normalization, and process changes in the graphics state. Learn more about our iOS SDK and PDF Data Extraction SDK Capabilities.
1//---------------------------------------------------------------------------------------
2// Copyright (c) 2001-2024 by Apryse Software Inc. All Rights Reserved.
3// Consult legal.txt regarding legal and license information.
4//---------------------------------------------------------------------------------------
5
6#import <OBJC/PDFNetOBJC.h>
7#import <Foundation/Foundation.h>
8
9char m_buf[4000];
10
11void ProcessElements(PTElementReader *reader);
12
13void ProcessPath(PTElementReader *reader, PTElement *path)
14{
15 if ([path IsClippingPath])
16 {
17 NSLog(@"This is a clipping path");
18 }
19
20 PTPathData* pathData = [path GetPathData];
21 NSMutableArray* data = [pathData GetPoints];
22 NSData* opr = [pathData GetOperators];
23
24 NSUInteger opr_index = 0;
25 NSUInteger opr_end = opr.length;
26 NSUInteger data_index = 0;
27 NSUInteger data_end = data.count;
28
29 double x1, y1, x2, y2, x3, y3;
30 NSString *str = @"";
31
32 // Use path.GetCTM() if you are interested in CTM (current transformation matrix).
33
34 unsigned char* opr_data = (unsigned char*)opr.bytes;
35 str = [str stringByAppendingFormat: @" Path Data Points := \""];
36 for (; opr_index<opr_end; opr_index = opr_index + 1)
37 {
38 switch(opr_data[opr_index])
39 {
40 case e_ptmoveto:
41 x1 = [data[data_index] doubleValue]; ++data_index;
42 y1 = [data[data_index] doubleValue]; ++data_index;
43 sprintf(m_buf, "M%.5g %.5g", x1, y1);
44 str = [str stringByAppendingFormat: @"%s", m_buf];
45 break;
46 case e_ptlineto:
47 x1 = [data[data_index] doubleValue]; ++data_index;
48 y1 = [data[data_index] doubleValue]; ++data_index;
49 sprintf(m_buf, " L%.5g %.5g", x1, y1);
50 str = [str stringByAppendingFormat: @"%s", m_buf];
51 break;
52 case e_ptcubicto:
53 x1 = [data[data_index] doubleValue]; ++data_index;
54 y1 = [data[data_index] doubleValue]; ++data_index;
55 x2 = [data[data_index] doubleValue]; ++data_index;
56 y2 = [data[data_index] doubleValue]; ++data_index;
57 x3 = [data[data_index] doubleValue]; ++data_index;
58 y3 = [data[data_index] doubleValue]; ++data_index;
59 sprintf(m_buf, " C%.5g %.5g %.5g %.5g %.5g %.5g", x1, y1, x2, y2, x3, y3);
60 str = [str stringByAppendingFormat: @"%s", m_buf];
61 break;
62 case e_ptrect:
63 {
64 x1 = [data[data_index] doubleValue]; ++data_index;
65 y1 = [data[data_index] doubleValue]; ++data_index;
66 double w = [data[data_index] doubleValue]; ++data_index;
67 double h = [data[data_index] doubleValue]; ++data_index;
68 x2 = x1 + w;
69 y2 = y1;
70 x3 = x2;
71 y3 = y1 + h;
72 double x4 = x1;
73 double y4 = y3;
74 sprintf(m_buf, "M%.5g %.5g L%.5g %.5g L%.5g %.5g L%.5g %.5g Z",
75 x1, y1, x2, y2, x3, y3, x4, y4);
76 str = [str stringByAppendingFormat: @"%s", m_buf];
77 }
78 break;
79 case e_ptclosepath:
80 str = [str stringByAppendingString: @" Close Path"];
81 break;
82 default:
83 assert(false);
84 break;
85 }
86 }
87
88 str = [str stringByAppendingString: @"\" "];
89
90 PTGState *gs = [path GetGState];
91
92 // Set Path State 0 (stroke, fill, fill-rule) -----------------------------------
93 if ([path IsStroked])
94 {
95 str = [str stringByAppendingString: @"Stroke path\n"];
96
97 if ([[gs GetStrokeColorSpace] GetType] == e_ptpattern)
98 {
99 str = [str stringByAppendingString: @"Path has associated pattern"];
100 }
101 else
102 {
103 // Get stroke color (you can use PDFNet color conversion facilities)
104 // ColorPt rgb;
105 // gs.GetStrokeColorSpace().Convert2RGB(gs.GetStrokeColor(), rgb);
106 }
107 }
108 else
109 {
110 // Do not stroke path
111 }
112
113 if ([path IsFilled])
114 {
115 str = [str stringByAppendingString: @"Fill path"];
116
117 if ([[gs GetFillColorSpace] GetType] == e_ptpattern)
118 {
119 str = [str stringByAppendingString: @"Path has associated pattern"];
120 }
121 else
122 {
123 // PTColorPt *rgb = [[[PTColorPt alloc] init] autorelease];
124 // [[gs GetFillColorSpace] Convert2RGB: [gs GetFillColorWithColorPt: rgb]];
125 }
126 }
127 else
128 {
129 // Do not fill path
130 }
131
132 // Process any changes in graphics state ---------------------------------
133
134 PTGSChangesIterator *gs_itr = [reader GetChangesIterator];
135 for (; [gs_itr HasNext]; [gs_itr Next])
136 {
137 switch([gs_itr Current])
138 {
139 case e_pttransform :
140 // Get transform matrix for this element. Unlike path.GetCTM()
141 // that return full transformation matrix gs.GetTransform() return
142 // only the transformation matrix that was installed for this element.
143 //
144 // gs.GetTransform();
145 break;
146 case e_ptline_width :
147 // gs.GetLineWidth();
148 break;
149 case e_ptline_cap :
150 // gs.GetLineCap();
151 break;
152 case e_ptline_join :
153 // gs.GetLineJoin();
154 break;
155 case e_ptflatness :
156 break;
157 case e_ptmiter_limit :
158 // gs.GetMiterLimit();
159 break;
160 case e_ptdash_pattern :
161 {
162 // std::vector<double> dashes;
163 // gs.GetDashes(dashes);
164 // gs.GetPhase()
165 }
166 break;
167 case e_ptfill_color:
168 {
169 if ( [[gs GetFillColorSpace] GetType] == e_ptpattern &&
170 [[gs GetFillPattern] GetType] != e_ptshading )
171 {
172 //process the pattern data
173 [reader PatternBegin: YES reset_ctm_tfm: NO];
174 ProcessElements(reader);
175 [reader End];
176 }
177 }
178 break;
179 default:
180 break;
181 }
182 }
183 [reader ClearChangeList];
184 NSLog(@"%@", str);
185}
186
187void ProcessText(PTElementReader* page_reader)
188{
189 // Begin text element
190 NSLog(@"Begin Text Block:");
191
192 PTElement *element;
193 while ((element = [page_reader Next]) != NULL)
194 {
195 switch ([element GetType])
196 {
197 case e_pttext_end:
198 // Finish the text block
199 //str = [str stringByAppendingString: @"End Text Block.\n"];
200 NSLog(@"End Text Block.");
201 return;
202
203 case e_pttext_obj:
204 {
205 PTGState *gs = [element GetGState];
206
207 PTColorSpace *cs_fill = [gs GetFillColorSpace];
208 PTColorPt *fill = [gs GetFillColor];
209
210 PTColorPt *outColor = [cs_fill Convert2RGB: fill];
211
212 PTColorSpace *cs_stroke = [gs GetStrokeColorSpace];
213 PTColorPt *stroke = [gs GetStrokeColor];
214
215 PTFont *font = [gs GetFont];
216
217 NSLog(@"Font Name: %@\n", [font GetName]);
218
219 // font.IsFixedWidth();
220 // font.IsSerif();
221 // font.IsSymbolic();
222 // font.IsItalic();
223 // ...
224
225 // double font_size = gs.GetFontSize();
226 // double word_spacing = gs.GetWordSpacing();
227 // double char_spacing = gs.GetCharSpacing();
228 // const UString* txt = element.GetTextString();
229
230 if ( [font GetType] == e_ptType3 )
231 {
232 //type 3 font, process its data
233 PTCharIterator *itr;
234 for (itr = [element GetCharIterator]; [itr HasNext]; [itr Next])
235 {
236 [page_reader Type3FontBegin: [itr Current] resource_dict: 0];
237 ProcessElements(page_reader);
238 [page_reader End];
239 }
240 }
241
242 else
243 {
244 PTMatrix2D *text_mtx = [element GetTextMatrix];
245 double x, y;
246 unsigned int char_code;
247
248 PTCharIterator *itr;
249 NSString* str = @"";
250 for (itr = [element GetCharIterator]; [itr HasNext]; [itr Next])
251 {
252 char_code = [[itr Current] getChar_code];
253 if (char_code>=32 || char_code<=255) { // Print if in ASCII range...
254 str = [str stringByAppendingFormat: @"%c", char_code];
255 }
256
257 x = [[itr Current] getX]; // character positioning information
258 y = [[itr Current] getY];
259
260 // Use element.GetCTM() if you are interested in the CTM
261 // (current transformation matrix).
262 PTMatrix2D *ctm = [element GetCTM];
263
264 // To get the exact character positioning information you need to
265 // concatenate current text matrix with CTM and then multiply
266 // relative positioning coordinates with the resulting matrix.
267 PTMatrix2D *mtx = text_mtx;
268 [mtx Concat: [ctm getM_a] b: [ctm getM_b] c: [ctm getM_c] d: [ctm getM_d] h: [ctm getM_h] v: [ctm getM_v]];
269 [mtx Mult: [[PTPDFPoint alloc] initWithPx: x py: y]];
270
271 // Get glyph path...
272 //vector<UChar> oprs;
273 //vector<double> glyph_data;
274 //font.GetGlyphPath(char_code, oprs, glyph_data, false, 0);
275 }
276 NSLog(@"%@", str);
277 }
278
279 //str = [str stringByAppendingString: @"\n"];
280 }
281 break;
282 default:
283 break;
284 }
285 }
286}
287
288void ProcessImage(PTElement *image)
289{
290 bool image_mask = [image IsImageMask];
291 bool interpolate = [image IsImageInterpolate];
292 int width = [image GetImageWidth];
293 int height = [image GetImageHeight];
294 int out_data_sz = width * height * 3;
295
296 NSLog(@"Image: width=\"%d\" height=\"%d\"", width, height);
297
298 // Matrix2D& mtx = image->GetCTM(); // image matrix (page positioning info)
299
300 // You can use GetImageData to read the raw (decoded) image data
301 //image->GetBitsPerComponent();
302 //image->GetImageData(); // get raw image data
303 // .... or use Image2RGB filter that converts every image to RGB format,
304 // This should save you time since you don't need to deal with color conversions,
305 // image up-sampling, decoding etc.
306
307 PTImage2RGB *img_conv = [[PTImage2RGB alloc] initWithImage_element: image]; // Extract and convert image to RGB 8-bpc format
308 PTFilterReader *reader = [[PTFilterReader alloc] initWithFilter: img_conv];
309
310 // A buffer used to keep image data.
311 NSData *image_data_out = [reader Read: out_data_sz];
312 // &image_data_out.front() contains RGB image data.
313
314 // Note that you don't need to read a whole image at a time. Alternatively
315 // you can read a chuck at a time by repeatedly calling reader.Read(buf, buf_sz)
316 // until the function returns 0.
317}
318
319void ProcessElements(PTElementReader *reader)
320{
321 PTElement *element;
322 while ((element = [reader Next]) != NULL) // Read page contents
323 {
324 switch ([element GetType])
325 {
326 case e_ptpath: // Process path data...
327 {
328 ProcessPath(reader, element);
329 }
330 break;
331 case e_pttext_begin: // Process text block...
332 {
333 ProcessText(reader);
334 }
335 break;
336 case e_ptform: // Process form XObjects
337 {
338 [reader FormBegin];
339 ProcessElements(reader);
340 [reader End];
341 }
342 break;
343 case e_ptimage: // Process Images
344 {
345 ProcessImage(element);
346 }
347 break;
348
349 default:
350 break;
351 }
352
353 }
354}
355
356int main(int argc, char *argv[])
357{
358 @autoreleasepool {
359 int ret = 0;
360 [PTPDFNet Initialize: 0];
361
362 @try // Extract text data from all pages in the document
363 {
364 NSLog(@"__________________________________________________");
365 NSLog(@"Extract page element information from all ");
366 NSLog(@"pages in the document.");
367
368 PTPDFDoc *doc = [[PTPDFDoc alloc] initWithFilepath: @"../../TestFiles/newsletter.pdf"];
369 [doc InitSecurityHandler];
370
371 int pgnum = [doc GetPageCount];
372 PTPageIterator *page_begin = [doc GetPageIterator: 1];
373
374 PTElementReader *page_reader = [[PTElementReader alloc] init];
375
376 PTPageIterator *itr;
377 for (itr = page_begin; [itr HasNext]; [itr Next]) // Read every page
378 {
379 NSLog(@"Page %d----------------------------------------", [[itr Current] GetIndex]);
380 [page_reader Begin: [itr Current]];
381 ProcessElements(page_reader);
382 [page_reader End];
383 }
384
385 NSLog(@"Done.");
386 }
387 @catch(NSException *e)
388 {
389 NSLog(@"%@", e.reason);
390 ret = 1;
391 }
392 [PTPDFNet Terminate: 0];
393 return ret;
394 }
395
396}
1//---------------------------------------------------------------------------------------
2// Copyright (c) 2001-2019 by PDFTron Systems Inc. All Rights Reserved.
3// Consult legal.txt regarding legal and license information.
4//---------------------------------------------------------------------------------------
5
6import PDFNet
7import Foundation
8
9func ProcessPath(reader: PTElementReader, path: PTElement) {
10 if path.isClippingPath() {
11 print("This is a clipping path")
12 }
13
14 let pathData: PTPathData = path.getPathData()
15 let data: NSMutableArray = pathData.getPoints()
16 let opr: Data = pathData.getOperators()
17
18 var opr_index: Int = 0
19 let opr_end: Int = opr.count
20 var data_index: Int = 0
21
22 var x1: Double = 0.0
23 var y1: Double = 0.0
24 var x2: Double = 0.0
25 var y2: Double = 0.0
26 var x3: Double = 0.0
27 var y3: Double = 0.0
28 var str = ""
29
30 // Use path.GetCTM() if you are interested in CTM (current transformation matrix).
31
32 str += (" Path Data Points := \"")
33
34 while opr_index < opr_end {
35 switch PTPathSegmentType(rawValue: UInt32(opr[opr_index])) {
36 case e_ptmoveto:
37 x1 = data[data_index] as! Double
38 data_index += 1
39 y1 = data[data_index] as! Double
40 data_index += 1
41 str += String(format: "M%.5g %.5g", x1, y1)
42 case e_ptlineto:
43 x1 = data[data_index] as! Double
44 data_index += 1
45 y1 = data[data_index] as! Double
46 data_index += 1
47 str += String(format: " L%.5g %.5g", x1, y1)
48 case e_ptcubicto:
49 x1 = data[data_index] as! Double
50 data_index += 1
51 y1 = data[data_index] as! Double
52 data_index += 1
53 x2 = data[data_index] as! Double
54 data_index += 1
55 y2 = data[data_index] as! Double
56 data_index += 1
57 x3 = data[data_index] as! Double
58 data_index += 1
59 y3 = data[data_index] as! Double
60 data_index += 1
61 str += String(format: " C%.5g %.5g %.5g %.5g %.5g %.5g", x1, y1, x2, y2, x3, y3)
62 case e_ptrect:
63 x1 = data[data_index] as! Double
64 data_index += 1
65 y1 = data[data_index] as! Double
66 data_index += 1
67 let w = data[data_index] as! Double
68 data_index += 1
69 let h = data[data_index] as! Double
70 data_index += 1
71 x2 = x1 + w
72 y2 = y1
73 x3 = x2
74 y3 = y1 + h
75 let x4: Double = x1
76 let y4: Double = y3
77 str += String(format: "M%.5g %.5g L%.5g %.5g L%.5g %.5g L%.5g %.5g Z", x1, y1, x2, y2, x3, y3, x4, y4)
78 case e_ptclosepath:
79 str += (" Close Path")
80 default:
81 assert(false)
82 }
83 opr_index = opr_index + 1
84 }
85
86 str += ("\" ")
87
88 let gs: PTGState = path.getGState()
89
90 // Set Path State 0 (stroke, fill, fill-rule) -----------------------------------
91 if path.isStroked() {
92 str = str + ("Stroke path")
93 if gs.getStrokeColorSpace().getType() == e_ptpattern {
94 str = str + ("Path has associated pattern")
95 }
96 else {
97 // Get stroke color (you can use PDFNet color conversion facilities)
98 // let rgb: PTColorPt = gs.getStrokeColorSpace().convert2RGB(gs.getStrokeColor())
99 }
100 }
101 else {
102 // Do not stroke path
103 }
104
105 if path.isFilled() {
106 str = str + ("Fill path")
107 if gs.getFillColorSpace().getType() == e_ptpattern {
108 str = str + ("Path has associated pattern")
109 }
110 else {
111 // let rgb: PTColorPt = gs.getFillColorSpace().convert2RGB(gs.getFillColor())
112 }
113 }
114 else {
115 // Do not fill path
116 }
117
118 // Process any changes in graphics state ---------------------------------
119
120 let gs_itr: PTGSChangesIterator = reader.getChangesIterator()
121
122 while gs_itr.hasNext() {
123 switch PTGStateAttribute(rawValue: UInt32(gs_itr.current())) {
124 case e_pttransform:
125 // Get transform matrix for this element. Unlike path.GetCTM()
126 // that return full transformation matrix gs.GetTransform() return
127 // only the transformation matrix that was installed for this element.
128 //
129 // gs.getTransform()
130 break
131 case e_ptline_width:
132 // gs.getLineWidth()
133 break
134 case e_ptline_cap:
135 // gs.getLineCap()
136 break
137 case e_ptline_join:
138 // gs.getLineJoin()
139 break
140 case e_ptflatness:
141 break
142 case e_ptmiter_limit:
143 // gs.GetmiterLimit()
144 break
145 case e_ptdash_pattern:
146 // let dashes: NSMutableArray = gs.getDashes()
147 // gs.getPhase()
148 break
149 case e_ptfill_color:
150 if gs.getFillColorSpace().getType() == e_ptpattern && gs.getFillPattern().getType() != e_ptshading {
151 //process the pattern data
152 reader.patternBegin(true, reset_ctm_tfm: false)
153 ProcessElements(reader: reader)
154 reader.end()
155 }
156 default:
157 break
158 }
159 gs_itr.next()
160 }
161 reader.clearChangeList()
162 print("\(str)")
163
164}
165
166func ProcessText(page_reader: PTElementReader) {
167 // Begin text element
168 print("Begin Text Block:")
169
170 while let element = page_reader.next() {
171 switch element.getType() {
172 case e_pttext_end:
173 // Finish the text block
174 print("End Text Block.")
175 return
176 case e_pttext_obj:
177 let gs: PTGState = element.getGState()
178
179 let cs_fill: PTColorSpace = gs.getFillColorSpace()
180 let fill: PTColorPt = gs.getFillColor()
181
182 let _: PTColorPt = cs_fill.convert2RGB(fill) // outColor
183
184 let _: PTColorSpace = gs.getStrokeColorSpace() // cs_stroke
185 let _: PTColorPt = gs.getStrokeColor() // stroke
186
187 let font: PTFont = gs.getFont()
188
189 print("Font Name: \(font.getName()!)")
190
191 // font.IsFixedWidth();
192 // font.IsSerif();
193 // font.IsSymbolic();
194 // font.IsItalic();
195 // ...
196
197 // double font_size = gs.GetFontSize();
198 // double word_spacing = gs.GetWordSpacing();
199 // double char_spacing = gs.GetCharSpacing();
200 // const UString* txt = element.GetTextString();
201
202 if font.getType() == e_ptType3 {
203 //type 3 font, process its data
204 let itr: PTCharIterator = element.getCharIterator()
205 while itr.hasNext() {
206 page_reader.type3FontBegin(itr.current(), resource_dict: nil)
207 ProcessElements(reader: page_reader)
208 page_reader.end()
209 itr.next()
210 }
211 }
212 else {
213 let text_mtx: PTMatrix2D = element.getTextMatrix()
214 var x: Double
215 var y: Double
216 var char_code: UInt32
217
218 var str = ""
219 let itr: PTCharIterator = element.getCharIterator()
220 while itr.hasNext() {
221 char_code = itr.current().getChar_code()
222 if char_code >= 32 || char_code <= 255 {
223 // Print if in ASCII range...
224 if let scalar = UnicodeScalar(char_code){
225 str += ("\(Character(scalar))")
226 }
227 }
228
229 x = itr.current().getX() // character positioning information
230 y = itr.current().getY()
231
232 // Use element.getCTM() if you are interested in the CTM
233 // (current transformation matrix).
234 let ctm: PTMatrix2D = element.getCTM()
235
236 // To get the exact character positioning information you need to
237 // concatenate current text matrix with CTM and then multiply
238 // relative positioning coordinates with the resulting matrix.
239 let mtx: PTMatrix2D = text_mtx
240 mtx.concat(ctm.getM_a(), b: ctm.getM_b(), c: ctm.getM_c(), d: ctm.getM_d(), h: ctm.getM_h(), v: ctm.getM_v())
241 mtx.mult(PTPDFPoint(px: x, py: y))
242
243 // Get glyph path...
244 //vector<UChar> oprs;
245 //vector<double> glyph_data;
246 //font.GetGlyphPath(char_code, oprs, glyph_data, false, 0);
247 itr.next()
248 }
249 print("\(str)")
250 }
251 default:
252 break
253 }
254 }
255}
256
257func ProcessImage(image: PTElement) {
258 let _: Bool = image.isImageMask() // image_mask
259 let _: Bool = image.isImageInterpolate() // interpolate
260 let width = image.getImageWidth()
261 let height = image.getImageHeight()
262 let out_data_sz = width * height * 3
263
264 print("Image: width=\"\(width)\" height=\"\(height)\"")
265
266 //let mtx: PTMatrix2D = image.getCTM() // image matrix (page positioning info)
267 // You can use GetImageData to read the raw (decoded) image data
268 //image.getBitsPerComponent()
269 //image.getImageData() // get raw image data
270 // .... or use Image2RGB filter that converts every image to RGB format,
271 // This should save you time since you don't need to deal with color conversions,
272 // image up-sampling, decoding etc.
273
274 let img_conv = PTImage2RGB(image_element: image) // Extract and convert image to RGB 8-bpc format
275 let reader: PTFilterReader = PTFilterReader(filter: img_conv)
276
277 // A buffer used to keep image data.
278 let _: Data = reader.read(UInt(out_data_sz)) // image_data_out
279 // &image_data_out.front() contains RGB image data.
280
281 // Note that you don't need to read a whole image at a time. Alternatively
282 // you can read a chuck at a time by repeatedly calling reader.Read(buf, buf_sz)
283 // until the function returns 0.
284}
285
286func ProcessElements(reader: PTElementReader) {
287 while let element = reader.next() {
288 switch element.getType() {
289 case e_ptpath:
290 // Process path data...
291 ProcessPath(reader: reader, path: element)
292 case e_pttext_begin:
293 // Process text block...
294 ProcessText(page_reader: reader)
295 case e_ptform:
296 // Process form XObjects
297 reader.formBegin()
298 ProcessElements(reader: reader)
299 reader.end()
300 case e_ptimage:
301 // Process Images
302 ProcessImage(image: element)
303 default:
304 break
305 }
306 }
307}
308
309func runElementReaderAdvTest() -> Int {
310 return autoreleasepool {
311 var ret: Int = 0
312
313
314 do {
315 try PTPDFNet.catchException {
316 // Extract text data from all pages in the document
317 print("__________________________________________________")
318 print("Extract page element information from all ")
319 print("pages in the document.")
320
321 let doc: PTPDFDoc = PTPDFDoc(filepath: Bundle.main.path(forResource: "newsletter", ofType: "pdf"))
322 doc.initSecurityHandler()
323
324 let page_begin: PTPageIterator = doc.getPageIterator(1)
325
326 let page_reader: PTElementReader = PTElementReader()
327
328 let itr: PTPageIterator = page_begin
329 while itr.hasNext() {
330 print("Page \(itr.current().getIndex())----------------------------------------")
331 page_reader.begin(itr.current())
332 ProcessElements(reader: page_reader)
333 page_reader.end()
334 itr.next()
335 }
336
337 print("Done.")
338 }
339 } catch let e as NSError {
340 print("\(e)")
341 ret = 1
342 }
343
344 return ret
345 }
346}
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales