• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DomCR / ACadSharp / 16520510906

25 Jul 2025 11:07AM UTC coverage: 75.22% (+0.04%) from 75.178%
16520510906

push

github

web-flow
Merge pull request #721 from DomCR/svg-units

SVG paper units

6062 of 8858 branches covered (68.44%)

Branch coverage included in aggregate %.

98 of 113 new or added lines in 6 files covered. (86.73%)

5 existing lines in 1 file now uncovered.

24023 of 31138 relevant lines covered (77.15%)

78683.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.08
/src/ACadSharp/IO/SVG/SvgXmlWriter.cs
1
using ACadSharp.Entities;
2
using ACadSharp.Extensions;
3
using ACadSharp.IO.DXF;
4
using ACadSharp.Objects;
5
using ACadSharp.Tables;
6
using ACadSharp.Types.Units;
7
using CSMath;
8
using CSUtilities.Extensions;
9
using System;
10
using System.Collections.Generic;
11
using System.Globalization;
12
using System.IO;
13
using System.Linq;
14
using System.Text;
15
using System.Xml;
16

17
namespace ACadSharp.IO.SVG
18
{
19
        internal class SvgXmlWriter : XmlTextWriter
20
        {
21
                public event NotificationEventHandler OnNotification;
22

23
                public SvgConfiguration Configuration { get; } = new();
99✔
24

25
                public Layout Layout { get; set; }
140✔
26

27
                public UnitsType Units { get; set; }
1,581✔
28

29
                public SvgXmlWriter(Stream w, Encoding encoding, SvgConfiguration configuration) : base(w, encoding)
6✔
30
                {
6✔
31
                        this.Configuration = configuration;
6✔
32
                }
6✔
33

34
                public void WriteAttributeString(string localName, double value, UnitsType? units = null)
35
                {
45✔
36
                        string unitSufix = string.Empty;
45✔
37
                        if (units.HasValue)
45✔
38
                        {
13✔
39
                                switch (units.Value)
13!
40
                                {
41
                                        case UnitsType.Centimeters:
NEW
42
                                                unitSufix = "cm";
×
NEW
43
                                                break;
×
44
                                        case UnitsType.Millimeters:
45
                                                unitSufix = "mm";
9✔
46
                                                break;
9✔
47
                                        case UnitsType.Inches:
48
                                                unitSufix = "in";
2✔
49
                                                break;
2✔
50
                                }
51
                        }
13✔
52

53
                        this.WriteAttributeString(
45✔
54
                                localName,
45✔
55
                                $"{value.ToString(CultureInfo.InvariantCulture)}{unitSufix}");
45✔
56
                }
45✔
57

58
                public void WriteBlock(BlockRecord record)
59
                {
1✔
60
                        this.Units = record.Units;
1✔
61

62
                        BoundingBox box = record.GetBoundingBox();
1✔
63

64
                        this.startDocument(box);
1✔
65

66
                        foreach (var e in record.Entities)
47✔
67
                        {
22✔
68
                                this.writeEntity(e);
22✔
69
                        }
22✔
70

71
                        this.endDocument();
1✔
72
                }
1✔
73

74
                public void WriteLayout(Layout layout)
75
                {
5✔
76
                        this.Layout = layout;
5✔
77
                        this.Units = layout.PaperUnits.ToUnits();
5✔
78

79
                        double paperWidth = layout.PaperWidth;
5✔
80
                        double paperHeight = layout.PaperHeight;
5✔
81

82
                        switch (layout.PaperRotation)
5✔
83
                        {
84
                                case PlotRotation.Degrees90:
85
                                case PlotRotation.Degrees270:
86
                                        paperWidth = layout.PaperHeight;
3✔
87
                                        paperHeight = layout.PaperWidth;
3✔
88
                                        break;
3✔
89
                        }
90

91
                        XYZ lowerCorner = XYZ.Zero;
5✔
92
                        XYZ upperCorner = new XYZ(paperWidth, paperHeight, 0.0);
5✔
93
                        BoundingBox paper = new BoundingBox(lowerCorner, upperCorner);
5✔
94

95
                        XYZ lowerMargin = this.Layout.UnprintableMargin.BottomLeftCorner.Convert<XYZ>();
5✔
96
                        BoundingBox margins = new BoundingBox(
5✔
97
                                lowerMargin,
5✔
98
                                this.Layout.UnprintableMargin.TopCorner.Convert<XYZ>());
5✔
99

100
                        this.startDocument(paper);
5✔
101

102
                        Transform transform = new Transform(
5✔
103
                                lowerMargin.ToPixelSize(this.Units),
5✔
104
                                new XYZ(layout.PrintScale),
5✔
105
                                XYZ.Zero);
5✔
106

107
                        foreach (var e in layout.AssociatedBlock.Entities)
223✔
108
                        {
104✔
109
                                this.writeEntity(e, transform);
104✔
110
                        }
104✔
111

112
                        this.endDocument();
5✔
113
                }
5✔
114

115
                private string colorSvg(Color color)
116
                {
119✔
117
                        if (this.Layout != null && color.Equals(Color.Default))
119✔
118
                        {
77✔
119
                                color = Color.Black;
77✔
120
                        }
77✔
121

122
                        return $"rgb({color.R},{color.G},{color.B})";
119✔
123
                }
119✔
124

125
                private void endDocument()
126
                {
6✔
127
                        this.WriteEndElement();
6✔
128
                        this.WriteEndDocument();
6✔
129
                        this.Close();
6✔
130
                }
6✔
131

132
                private void notify(string message, NotificationType type, Exception ex = null)
133
                {
8✔
134
                        this.OnNotification?.Invoke(this, new NotificationEventArgs(message, type, ex));
8!
135
                }
8✔
136

137
                private void startDocument(BoundingBox box)
138
                {
6✔
139
                        this.WriteStartDocument();
6✔
140

141
                        this.WriteStartElement("svg");
6✔
142
                        this.WriteAttributeString("xmlns", "http://www.w3.org/2000/svg");
6✔
143

144
                        this.WriteAttributeString("width", box.Max.X - box.Min.X, this.Units);
6✔
145
                        this.WriteAttributeString("height", box.Max.Y - box.Min.Y, this.Units);
6✔
146

147
                        this.WriteStartAttribute("viewBox");
6✔
148
                        this.WriteValue(box.Min.X.ToSvg(this.Units));
6✔
149
                        this.WriteValue(" ");
6✔
150
                        this.WriteValue(box.Min.Y.ToSvg(this.Units));
6✔
151
                        this.WriteValue(" ");
6✔
152
                        this.WriteValue((box.Max.X - box.Min.X).ToSvg(this.Units));
6✔
153
                        this.WriteValue(" ");
6✔
154
                        this.WriteValue((box.Max.Y - box.Min.Y).ToSvg(this.Units));
6✔
155
                        this.WriteEndAttribute();
6✔
156

157
                        this.WriteAttributeString("transform", $"scale(1,-1)");
6✔
158

159
                        if (this.Layout != null)
6✔
160
                        {
5✔
161
                                this.WriteAttributeString("style", "background-color:white");
5✔
162
                        }
5✔
163
                }
6✔
164

165
                private string svgPoints(IEnumerable<IVector> points, Transform transform)
166
                {
11✔
167
                        if (!points.Any())
11!
168
                        {
×
169
                                return string.Empty;
×
170
                        }
171

172
                        StringBuilder sb = new StringBuilder();
11✔
173
                        sb.Append(points.First().ToPixelSize(this.Units).ToSvg());
11✔
174
                        foreach (IVector point in points.Skip(1))
2,115✔
175
                        {
1,041✔
176
                                sb.Append(' ');
1,041✔
177
                                sb.Append(point.ToPixelSize(this.Units).ToSvg());
1,041✔
178
                        }
1,041✔
179

180
                        return sb.ToString();
11✔
181
                }
11✔
182

183
                private void writeArc(Arc arc, Transform transform)
184
                {
3✔
185
                        //A rx ry rotation large-arc-flag sweep-flag x y
186

187
                        this.WriteStartElement("polyline");
3✔
188

189
                        this.writeEntityHeader(arc, transform);
3✔
190

191
                        IEnumerable<IVector> vertices = arc.PolygonalVertexes(256).OfType<IVector>();
3✔
192
                        string pts = this.svgPoints(vertices, transform);
3✔
193
                        this.WriteAttributeString("points", pts);
3✔
194
                        this.WriteAttributeString("fill", "none");
3✔
195

196
                        this.WriteEndElement();
3✔
197
                }
3✔
198

199
                private void writeCircle(Circle circle, Transform transform)
200
                {
2✔
201
                        var loc = transform.ApplyTransform(circle.Center);
2✔
202

203
                        this.WriteStartElement("circle");
2✔
204

205
                        this.writeEntityHeader(circle, transform);
2✔
206

207
                        this.WriteAttributeString("r", circle.Radius);
2✔
208
                        this.WriteAttributeString("cx", loc.X);
2✔
209
                        this.WriteAttributeString("cy", loc.Y);
2✔
210

211
                        this.WriteAttributeString("fill", "none");
2✔
212

213
                        this.WriteEndElement();
2✔
214
                }
2✔
215

216
                private void writeEllipse(Ellipse ellipse, Transform transform)
217
                {
1✔
218
                        this.WriteStartElement("polygon");
1✔
219

220
                        this.writeEntityHeader(ellipse, transform);
1✔
221

222
                        IEnumerable<IVector> vertices = ellipse.PolygonalVertexes(256).OfType<IVector>();
1✔
223
                        string pts = this.svgPoints(vertices, transform);
1✔
224
                        this.WriteAttributeString("points", pts);
1✔
225
                        this.WriteAttributeString("fill", "none");
1✔
226

227
                        this.WriteEndElement();
1✔
228
                }
1✔
229

230
                private void writeEntity(Entity entity)
231
                {
23✔
232
                        this.writeEntity(entity, new Transform());
23✔
233
                }
23✔
234

235
                private void writeEntity(Entity entity, Transform transform)
236
                {
127✔
237
                        switch (entity)
127✔
238
                        {
239
                                case Arc arc:
240
                                        this.writeArc(arc, transform);
3✔
241
                                        break;
3✔
242
                                case Line line:
243
                                        this.writeLine(line, transform);
78✔
244
                                        break;
78✔
245
                                case Point point:
246
                                        this.writePoint(point, transform);
1✔
247
                                        break;
1✔
248
                                case Circle circle:
249
                                        this.writeCircle(circle, transform);
2✔
250
                                        break;
2✔
251
                                case Ellipse ellipse:
252
                                        this.writeEllipse(ellipse, transform);
1✔
253
                                        break;
1✔
254
                                //case Hatch hatch:
255
                                //        this.writeHatch(hatch, transform);
256
                                //        break;
257
                                case Insert insert:
258
                                        this.writeInsert(insert, transform);
1✔
259
                                        break;
1✔
260
                                case IPolyline polyline:
261
                                        this.writePolyline(polyline, transform);
7✔
262
                                        break;
7✔
263
                                case IText text:
264
                                        this.writeText(text, transform);
26✔
265
                                        break;
26✔
266
                                default:
267
                                        this.notify($"[{entity.ObjectName}] Entity not implemented.", NotificationType.NotImplemented);
8✔
268
                                        break;
8✔
269
                        }
270
                }
127✔
271

272
                private void writeEntityHeader(IEntity entity, Transform transform)
273
                {
92✔
274
                        Color color = entity.GetActiveColor();
92✔
275

276
                        this.WriteAttributeString("vector-effect", "non-scaling-stroke");
92✔
277
                        this.WriteAttributeString("stroke", this.colorSvg(color));
92✔
278

279
                        var lineWeight = entity.LineWeight;
92✔
280
                        switch (lineWeight)
92✔
281
                        {
282
                                case LineweightType.ByLayer:
283
                                        lineWeight = entity.Layer.LineWeight;
77✔
284
                                        break;
77✔
285
                        }
286

287
                        this.WriteAttributeString("stroke-width", $"{this.Configuration.GetLineWeightValue(lineWeight, this.Units).ToSvg(UnitsType.Millimeters)}");
92!
288

289
                        this.writeTransform(transform);
92✔
290
                }
92✔
291

292
                private void writeHatch(Hatch hatch, Transform transform)
293
                {
×
294
                        this.WriteStartElement("g");
×
295

296
                        this.writePattern(hatch.Pattern);
×
297

298
                        foreach (Hatch.BoundaryPath path in hatch.Paths)
×
299
                        {
×
300
                                this.WriteStartElement("polyline");
×
301

302
                                this.writeEntityHeader(hatch, transform);
×
303

304
                                foreach (var item in path.Edges)
×
305
                                {
×
306
                                        //TODO: svg edges for hatch drawing
307
                                }
×
308

309
                                //this.WriteAttributeString("points", pts);
310

311
                                this.WriteAttributeString("fill", "none");
×
312

313
                                this.WriteEndElement();
×
314
                        }
×
315

316
                        this.WriteEndElement();
×
317
                }
×
318

319
                private void writeInsert(Insert insert, Transform transform)
320
                {
1✔
321
                        var insertTransform = insert.GetTransform();
1✔
322
                        var merged = new Transform(transform.Matrix * insertTransform.Matrix);
1✔
323

324
                        this.WriteStartElement("g");
1✔
325
                        this.writeTransform(merged);
1✔
326

327
                        foreach (var e in insert.Block.Entities)
5✔
328
                        {
1✔
329
                                this.writeEntity(e);
1✔
330
                        }
1✔
331

332
                        this.WriteEndElement();
1✔
333
                }
1✔
334

335
                private void writeLine(Line line, Transform transform)
336
                {
78✔
337
                        this.WriteStartElement("line");
78✔
338

339
                        this.writeEntityHeader(line, transform);
78✔
340

341
                        this.WriteAttributeString("x1", line.StartPoint.X.ToSvg(this.Units));
78✔
342
                        this.WriteAttributeString("y1", line.StartPoint.Y.ToSvg(this.Units));
78✔
343
                        this.WriteAttributeString("x2", line.EndPoint.X.ToSvg(this.Units));
78✔
344
                        this.WriteAttributeString("y2", line.EndPoint.Y.ToSvg(this.Units));
78✔
345

346
                        this.WriteEndElement();
78✔
347
                }
78✔
348

349
                private void writePattern(HatchPattern pattern)
350
                {
×
351
                        this.WriteStartElement("pattern");
×
352

353
                        this.WriteEndElement();
×
354
                }
×
355

356
                private void writePoint(Point point, Transform transform)
357
                {
1✔
358
                        this.WriteStartElement("circle");
1✔
359

360
                        this.writeEntityHeader(point, transform);
1✔
361

362
                        this.WriteAttributeString("r", this.Configuration.PointRadius, UnitsType.Millimeters);
1✔
363
                        this.WriteAttributeString("cx", point.Location.X);
1✔
364
                        this.WriteAttributeString("cy", point.Location.Y);
1✔
365

366
                        this.WriteAttributeString("fill", this.colorSvg(point.GetActiveColor()));
1✔
367

368
                        this.WriteEndElement();
1✔
369
                }
1✔
370

371
                private void writePolyline(IPolyline polyline, Transform transform)
372
                {
7✔
373
                        if (polyline.IsClosed)
7✔
374
                        {
6✔
375
                                this.WriteStartElement("polygon");
6✔
376
                        }
6✔
377
                        else
378
                        {
1✔
379
                                this.WriteStartElement("polyline");
1✔
380
                        }
1✔
381

382
                        this.writeEntityHeader(polyline, transform);
7✔
383

384
                        var vertices = polyline.Vertices.Select(v => v.Location).ToList();
35✔
385

386
                        string pts = this.svgPoints(polyline.Vertices.Select(v => v.Location), transform);
49✔
387
                        this.WriteAttributeString("points", pts);
7✔
388
                        this.WriteAttributeString("fill", "none");
7✔
389

390
                        this.WriteEndElement();
7✔
391
                }
7✔
392

393
                private void writeText(IText text, Transform transform)
394
                {
26✔
395
                        XYZ insert;
396

397
                        if (text is TextEntity lineText
26✔
398
                                && (lineText.HorizontalAlignment != TextHorizontalAlignment.Left
26✔
399
                                || lineText.VerticalAlignment != TextVerticalAlignmentType.Baseline)
26✔
400
                                && !(lineText.HorizontalAlignment == TextHorizontalAlignment.Fit
26✔
401
                                || lineText.HorizontalAlignment == TextHorizontalAlignment.Aligned))
26✔
402
                        {
12✔
403
                                insert = lineText.AlignmentPoint;
12✔
404
                        }
12✔
405
                        else
406
                        {
14✔
407
                                insert = text.InsertPoint;
14✔
408
                        }
14✔
409

410
                        this.WriteStartElement("g");
26✔
411
                        this.writeTransform(transform);
26✔
412

413
                        this.WriteStartElement("text");
26✔
414

415
                        this.writeTransform(translation: insert.ToPixelSize(this.Units), scale: new XYZ(1, -1, 0), rotation: text.Rotation != 0 ? text.Rotation : null);
26✔
416

417
                        this.WriteAttributeString("fill", this.colorSvg(text.GetActiveColor()));
26✔
418

419
                        //<text x="20" y="35" class="small">My</text>
420
                        this.WriteStartAttribute("style");
26✔
421
                        this.WriteValue("font:");
26✔
422
                        this.WriteValue(text.Height.ToSvg(this.Units));
26✔
423
                        if (this.Units == UnitsType.Unitless)
26✔
424
                        {
4✔
425
                                this.WriteValue("px");
4✔
426
                        }
4✔
427
                        this.WriteValue(" ");
26✔
428
                        this.WriteValue(Path.GetFileNameWithoutExtension(text.Style.Filename));
26✔
429
                        this.WriteEndAttribute();
26✔
430

431
                        switch (text)
26✔
432
                        {
433
                                case MText mtext:
434
                                        switch (mtext.AttachmentPoint)
5!
435
                                        {
436
                                                case AttachmentPointType.TopLeft:
437
                                                        this.WriteAttributeString("alignment-baseline", "hanging");
4✔
438
                                                        this.WriteAttributeString("text-anchor", "start");
4✔
439
                                                        break;
4✔
440
                                                case AttachmentPointType.TopCenter:
441
                                                        this.WriteAttributeString("alignment-baseline", "hanging");
×
442
                                                        this.WriteAttributeString("text-anchor", "middle");
×
443
                                                        break;
×
444
                                                case AttachmentPointType.TopRight:
445
                                                        this.WriteAttributeString("alignment-baseline", "hanging");
1✔
446
                                                        this.WriteAttributeString("text-anchor", "end");
1✔
447
                                                        break;
1✔
448
                                                case AttachmentPointType.MiddleLeft:
449
                                                        this.WriteAttributeString("alignment-baseline", "middle");
×
450
                                                        this.WriteAttributeString("text-anchor", "start");
×
451
                                                        break;
×
452
                                                case AttachmentPointType.MiddleCenter:
453
                                                        this.WriteAttributeString("alignment-baseline", "middle");
×
454
                                                        this.WriteAttributeString("text-anchor", "middle");
×
455
                                                        break;
×
456
                                                case AttachmentPointType.MiddleRight:
457
                                                        this.WriteAttributeString("alignment-baseline", "middle");
×
458
                                                        this.WriteAttributeString("text-anchor", "end");
×
459
                                                        break;
×
460
                                                case AttachmentPointType.BottomLeft:
461
                                                        this.WriteAttributeString("alignment-baseline", "baseline");
×
462
                                                        this.WriteAttributeString("text-anchor", "start");
×
463
                                                        break;
×
464
                                                case AttachmentPointType.BottomCenter:
465
                                                        this.WriteAttributeString("alignment-baseline", "baseline");
×
466
                                                        this.WriteAttributeString("text-anchor", "middle");
×
467
                                                        break;
×
468
                                                case AttachmentPointType.BottomRight:
469
                                                        this.WriteAttributeString("alignment-baseline", "baseline");
×
470
                                                        this.WriteAttributeString("text-anchor", "end");
×
471
                                                        break;
×
472
                                                default:
473
                                                        break;
×
474
                                        }
475

476
                                        foreach (var item in mtext.GetTextLines())
53✔
477
                                        {
19✔
478
                                                this.WriteStartElement("tspan");
19✔
479
                                                this.WriteAttributeString("x", 0);
19✔
480
                                                this.WriteAttributeString("dy", "1em");
19✔
481
                                                this.WriteString(item);
19✔
482
                                                this.WriteEndElement();
19✔
483
                                        }
19✔
484

485
                                        //Line to avoid the strange offset at the end
486
                                        this.WriteStartElement("tspan");
5✔
487
                                        this.WriteAttributeString("x", 0);
5✔
488
                                        this.WriteAttributeString("dy", "1em");
5✔
489
                                        this.WriteAttributeString("visibility", "hidden");
5✔
490
                                        this.WriteString(".");
5✔
491
                                        this.WriteEndElement();
5✔
492
                                        break;
5✔
493
                                case TextEntity textEntity:
494

495
                                        switch (textEntity.HorizontalAlignment)
21✔
496
                                        {
497
                                                case TextHorizontalAlignment.Left:
498
                                                        this.WriteAttributeString("text-anchor", "start");
10✔
499
                                                        break;
10✔
500
                                                case TextHorizontalAlignment.Middle:
501
                                                case TextHorizontalAlignment.Center:
502
                                                        this.WriteAttributeString("text-anchor", "middle");
5✔
503
                                                        break;
5✔
504
                                                case TextHorizontalAlignment.Right:
505
                                                        this.WriteAttributeString("text-anchor", "end");
4✔
506
                                                        break;
4✔
507
                                        }
508

509
                                        switch (textEntity.VerticalAlignment)
21✔
510
                                        {
511
                                                case TextVerticalAlignmentType.Baseline:
512
                                                case TextVerticalAlignmentType.Bottom:
513
                                                        this.WriteAttributeString("alignment-baseline", "baseline");
15✔
514
                                                        break;
15✔
515
                                                case TextVerticalAlignmentType.Middle:
516
                                                        this.WriteAttributeString("alignment-baseline", "middle");
3✔
517
                                                        break;
3✔
518
                                                case TextVerticalAlignmentType.Top:
519
                                                        this.WriteAttributeString("alignment-baseline", "hanging");
3✔
520
                                                        break;
3✔
521
                                        }
522

523
                                        this.WriteString(text.Value);
21✔
524
                                        break;
21✔
525
                        }
526

527
                        this.WriteEndElement();
26✔
528
                        this.WriteEndElement();
26✔
529
                }
26✔
530

531
                private void writeTransform(Transform transform)
532
                {
119✔
533
                        XYZ? translation = transform.Translation != XYZ.Zero ? transform.Translation : null;
119✔
534
                        XYZ? scale = transform.Scale != new XYZ(1) ? transform.Scale : null;
119✔
535
                        double? rotation = transform.EulerRotation.Z != 0 ? transform.EulerRotation.Z : null;
119!
536

537
                        this.writeTransform(translation, scale, rotation);
119✔
538
                }
119✔
539

540
                private void writeTransform(XYZ? translation = null, XYZ? scale = null, double? rotation = null)
541
                {
145✔
542
                        StringBuilder sb = new StringBuilder();
145✔
543

544
                        if (translation.HasValue)
145✔
545
                        {
88✔
546
                                var t = translation.Value;
88✔
547

548
                                sb.Append($"translate(");
88✔
549
                                sb.Append($"{t.X.ToString(CultureInfo.InvariantCulture)},");
88✔
550
                                sb.Append($"{t.Y.ToString(CultureInfo.InvariantCulture)})");
88✔
551
                                sb.Append(' ');
88✔
552
                        }
88✔
553

554
                        if (scale.HasValue)
145✔
555
                        {
83✔
556
                                var s = scale.Value;
83✔
557

558
                                sb.Append($"scale(");
83✔
559
                                sb.Append($"{s.X.ToString(CultureInfo.InvariantCulture)},");
83✔
560
                                sb.Append($"{s.Y.ToString(CultureInfo.InvariantCulture)})");
83✔
561
                                sb.Append(' ');
83✔
562
                        }
83✔
563

564
                        if (rotation.HasValue)
145✔
565
                        {
2✔
566
                                var r = -MathHelper.RadToDeg(rotation.Value);
2✔
567

568
                                sb.Append($"rotate(");
2✔
569
                                sb.Append($"{r.ToString(CultureInfo.InvariantCulture)})");
2✔
570
                        }
2✔
571

572
                        if (sb.ToString().IsNullOrEmpty())
145✔
573
                        {
51✔
574
                                return;
51✔
575
                        }
576

577
                        this.WriteAttributeString("transform", sb.ToString());
94✔
578
                }
145✔
579
        }
580
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc