contrek 1.2.7 → 1.2.9

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rubocop.yml +11 -0
  4. data/CHANGELOG.md +7 -1
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +1 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/contrek.gemspec +2 -0
  10. data/ext/cpp_polygon_finder/PolygonFinder/CMakeLists.txt +2 -2
  11. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +108 -1
  12. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.h +1 -0
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +73 -6
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.h +1 -1
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +6 -6
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +3 -2
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.cpp +114 -0
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.h +41 -0
  19. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +52 -2
  20. data/ext/cpp_polygon_finder/extconf.rb +2 -0
  21. data/lib/contrek/bitmaps/bitmap.rb +2 -0
  22. data/lib/contrek/bitmaps/chunky_bitmap.rb +2 -0
  23. data/lib/contrek/bitmaps/painting.rb +2 -0
  24. data/lib/contrek/bitmaps/png_bitmap.rb +2 -0
  25. data/lib/contrek/bitmaps/raw_bitmap.rb +2 -0
  26. data/lib/contrek/bitmaps/rgb_color.rb +2 -0
  27. data/lib/contrek/bitmaps/rgb_cpp_color.rb +2 -0
  28. data/lib/contrek/bitmaps/sample_generator.rb +2 -0
  29. data/lib/contrek/cpp/cpp_concurrent_finder.rb +2 -0
  30. data/lib/contrek/cpp/cpp_concurrent_horizontal_merger.rb +2 -0
  31. data/lib/contrek/cpp/cpp_concurrent_merger.rb +2 -0
  32. data/lib/contrek/cpp/cpp_concurrent_streaming_merger.rb +11 -0
  33. data/lib/contrek/cpp/cpp_concurrent_vertical_merger.rb +2 -0
  34. data/lib/contrek/cpp/cpp_result.rb +2 -0
  35. data/lib/contrek/cpp/cpp_tempfile.rb +28 -0
  36. data/lib/contrek/finder/bounds.rb +2 -0
  37. data/lib/contrek/finder/concurrent/clipped_polygon_finder.rb +2 -0
  38. data/lib/contrek/finder/concurrent/cluster.rb +2 -0
  39. data/lib/contrek/finder/concurrent/cursor.rb +2 -0
  40. data/lib/contrek/finder/concurrent/end_point.rb +2 -0
  41. data/lib/contrek/finder/concurrent/fake_cluster.rb +2 -0
  42. data/lib/contrek/finder/concurrent/finder.rb +2 -0
  43. data/lib/contrek/finder/concurrent/horizontal_merger.rb +2 -0
  44. data/lib/contrek/finder/concurrent/hub.rb +2 -0
  45. data/lib/contrek/finder/concurrent/inner_polyline.rb +2 -0
  46. data/lib/contrek/finder/concurrent/listable.rb +2 -0
  47. data/lib/contrek/finder/concurrent/merger.rb +3 -0
  48. data/lib/contrek/finder/concurrent/part.rb +2 -0
  49. data/lib/contrek/finder/concurrent/partitionable.rb +2 -0
  50. data/lib/contrek/finder/concurrent/polyline.rb +2 -0
  51. data/lib/contrek/finder/concurrent/poolable.rb +2 -0
  52. data/lib/contrek/finder/concurrent/position.rb +2 -0
  53. data/lib/contrek/finder/concurrent/queueable.rb +2 -0
  54. data/lib/contrek/finder/concurrent/sequence.rb +2 -0
  55. data/lib/contrek/finder/concurrent/shape.rb +2 -0
  56. data/lib/contrek/finder/concurrent/streaming_merger.rb +89 -0
  57. data/lib/contrek/finder/concurrent/tile.rb +2 -0
  58. data/lib/contrek/finder/concurrent/vertical_merger.rb +4 -2
  59. data/lib/contrek/finder/list.rb +2 -0
  60. data/lib/contrek/finder/list_entry.rb +2 -0
  61. data/lib/contrek/finder/listable.rb +2 -0
  62. data/lib/contrek/finder/lists.rb +2 -0
  63. data/lib/contrek/finder/node.rb +2 -0
  64. data/lib/contrek/finder/node_cluster.rb +3 -1
  65. data/lib/contrek/finder/polygon_finder.rb +2 -0
  66. data/lib/contrek/finder/result.rb +2 -0
  67. data/lib/contrek/map/mercator_projection.rb +2 -0
  68. data/lib/contrek/matchers/matcher.rb +2 -0
  69. data/lib/contrek/matchers/matcher_hsb.rb +2 -0
  70. data/lib/contrek/matchers/value_not_matcher.rb +2 -0
  71. data/lib/contrek/reducers/linear_reducer.rb +2 -0
  72. data/lib/contrek/reducers/reducer.rb +2 -0
  73. data/lib/contrek/reducers/uniq_reducer.rb +2 -0
  74. data/lib/contrek/reducers/visvalingam_reducer.rb +2 -0
  75. data/lib/contrek/shared/result.rb +2 -0
  76. data/lib/contrek/version.rb +3 -1
  77. data/lib/contrek.rb +5 -0
  78. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae46fa52ddce3383ef534142814ed28ddc5cae45042e677c8f445140bee67e82
4
- data.tar.gz: 57cbaf53a0d6e8c7d31bdfcef0718c94654ceaf0fd7b2be84325751dc29f6224
3
+ metadata.gz: 8f506a5908c11f7ac1de118b4772ad9e85cd00746e4ee57b5d9c1600faea371e
4
+ data.tar.gz: 81c2807712361d51398962c9496bf3c833b21eb42d34907069c923cd62654f62
5
5
  SHA512:
6
- metadata.gz: ee922b6b787ac0f91b98a2e7d608c351499ea5405e325a0eb6d23f751a29075b390c08d757824132c7fbef7d64cef9f3af7dbb11709506e84777ff652f9f15fc
7
- data.tar.gz: 848102a6980857b1f4d3914e60de04408d0f7a4cc4c950a20b3fa8ef7231e31db029220eb6e3fb71847b6986a09c0d7b1fedae70f8e30e8ef758a3adeabeddcd
6
+ metadata.gz: 76c41016ce95c124ed72beacd5c838bf5fe960483e3cf7b78848d53c3d997948f5de0c8ecacdb08d3c38b8faf7349ec79cef9546415773e5d5201019b04fe3b2
7
+ data.tar.gz: 1823196730b740221b2e75be9915706b5b96afd91349dd898eda70d1ccce69f569270e134663137523ea3860e5d49a419b59a083a6a11be027bf4e14a1a0e30b
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .ruby-version
4
4
  # rspec failure tracking
5
5
  .rspec_status
6
+ /spec/files/performance_history.json
6
7
 
7
8
  /ext/cpp_polygon_finder/PolygonFinder/build/
8
9
  /ext/cpp_polygon_finder/PolygonFinder/contrek
@@ -12,4 +13,4 @@
12
13
  /ext/cpp_polygon_finder/cpp_polygon_finder.o
13
14
  /ext/cpp_polygon_finder/cpp_polygon_finder.so
14
15
  /ext/cpp_polygon_finder/mkmf.log
15
- /lib/cpp_polygon_finder.so
16
+ /lib/cpp_polygon_finder.so
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ DisabledByDefault: true
3
+ TargetRubyVersion: 3.1
4
+ Exclude:
5
+ - 'bin/**/*'
6
+ - 'vendor/**/*'
7
+
8
+ # Attiva l'obbligo del magic comment
9
+ Style/FrozenStringLiteralComment:
10
+ Enabled: true
11
+ EnforcedStyle: always
data/CHANGELOG.md CHANGED
@@ -115,4 +115,10 @@ All notable changes to this project will be documented in this file.
115
115
 
116
116
  ## [1.2.7] - 2026-06-02
117
117
  ### Changed
118
- - **Refactored `bounds` option:** Starting from this release, precalculated bounds for each polygon can now be requested in concurrent mode as well, in addition to single-threaded mode.
118
+ - **Refactored `bounds` option:** Starting from this release, precalculated bounds for each polygon can now be requested in concurrent mode as well, in addition to single-threaded mode.
119
+
120
+ ## [1.2.8] - 2026-06-07
121
+ - **Optimize main pixel scanning loop:** Implemented 4-way loop unrolling to maximize L1 cache hits and eliminate redundant RAM lookups via direct register bit-casting.
122
+
123
+ ## [1.2.9] - 2026-06-13
124
+ - **Streaming merger:** The streaming merger class extends VerticalMerger and adds a useful feature: the progressive extraction of contours into a disk buffer (SVG file). In this way, all extracted polygons that are no longer within the junction zone of the next stripe are removed from the system and streamed directly to disk. This incredibly reduces memory consumption, allowing the processing of very large files on machines with low memory availability, at the expense of increased processing times. An example of this technique is available in both C++ and Ruby in the repository.
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  gemspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.2.7)
4
+ contrek (1.2.9)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Contrek
2
- Contrek is a Ruby gem powered by a <u>standalone C++17 core library</u> for fast contour tracing and edge detection in PNG images and raw memory streams. It employs a **topological approach** to extract polygonal contours, representing shapes as a connected graph of shared endpoints. This ensures perfect adjacency and structural integrity for shape analysis and raster-to-vector workflows, such as PNG to SVG conversion, managed via libspng (0.7.4) with multithreading support.
2
+ Contrek is a Ruby gem powered by a <u>[standalone C++17 core library](#-c-standalone-library-usage)</u> for fast contour tracing and edge detection in PNG images and raw memory streams. It employs a **topological approach** to extract polygonal contours, representing shapes as a connected graph of shared endpoints. This ensures perfect adjacency and structural integrity for shape analysis and raster-to-vector workflows, such as PNG to SVG conversion, managed via libspng (0.7.4) with multithreading support.
3
3
 
4
4
 
5
5
  ## About Contrek library
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rake"
2
4
 
3
5
  desc "compiles c++ extension"
data/contrek.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path("lib", __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require "contrek/version"
@@ -11,8 +11,8 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
11
11
  list(FILTER CMAKE_CXX_FLAGS EXCLUDE REGEX "-DNDEBUG")
12
12
  list(FILTER CMAKE_C_FLAGS EXCLUDE REGEX "-DNDEBUG")
13
13
  else()
14
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread -march=native -DNDEBUG -Ofast -flto")
15
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -march=native -fPIC -DNDEBUG")
14
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread -march=native -fPIC -DNDEBUG -Ofast -flto -ftree-vectorize")
15
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pthread -march=native -fPIC -DNDEBUG -Ofast -flto -ftree-vectorize")
16
16
  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -flto=auto -Wl,--no-as-needed")
17
17
  find_library(TCMALLOC_LIB tcmalloc)
18
18
  if(TCMALLOC_LIB)
@@ -34,6 +34,7 @@
34
34
  #include "polygon/finder/concurrent/Finder.h"
35
35
  #include "polygon/finder/concurrent/HorizontalMerger.h"
36
36
  #include "polygon/finder/concurrent/VerticalMerger.h"
37
+ #include "polygon/finder/concurrent/StreamingMerger.h"
37
38
  #include "polygon/finder/concurrent/Sequence.h"
38
39
  #include "polygon/finder/concurrent/Position.h"
39
40
  #include "polygon/finder/Polygon.h"
@@ -385,6 +386,112 @@ void stream_png_image(const std::string& filepath, uint32_t stripe_height, bool
385
386
  }
386
387
 
387
388
  void Tests::test_i() {
388
- stream_png_image("../images/graphs_1024x1024.png", 300);
389
+ stream_png_image("../images/graphs_1024x1024.png", 300, true);
390
+ std::cout << "Memory usage peak: " << get_peak_rss() << " MB" << std::endl;
391
+ }
392
+
393
+ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_height) {
394
+ std::vector<ProcessResult*> result_clones;
395
+ std::vector<std::string> varguments = {"--bounds"};
396
+ // opens image to stream
397
+ FILE* fp = fopen(filepath.c_str(), "rb");
398
+ if (!fp) {
399
+ std::cerr << "Unable open file: " << filepath << std::endl;
400
+ return;
401
+ }
402
+
403
+ // exams image
404
+ spng_ctx *ctx = spng_ctx_new(0);
405
+ spng_set_png_file(ctx, fp);
406
+ struct spng_ihdr ihdr;
407
+ if (spng_get_ihdr(ctx, &ihdr)) {
408
+ fclose(fp);
409
+ spng_ctx_free(ctx);
410
+ return;
411
+ }
412
+ uint32_t total_width = ihdr.width;
413
+ uint32_t total_height = ihdr.height;
414
+
415
+ // allocates stripe buffer
416
+ RawBitmap stripe_bitmap;
417
+ stripe_bitmap.define(total_width, stripe_height, 4, true);
418
+ RGBNotMatcher not_matcher(-1);
419
+ if (spng_decode_image(ctx, NULL, 0, SPNG_FMT_RGBA8, SPNG_DECODE_PROGRESSIVE)) {
420
+ fclose(fp);
421
+ spng_ctx_free(ctx);
422
+ return;
423
+ }
424
+
425
+ // allocates streaming svg buffer
426
+ std::string output_path = "streaming_buffer.svg";
427
+ std::ofstream shared_stream(output_path, std::ios::out | std::ios::binary);
428
+ if (!shared_stream) {
429
+ std::cerr << "Error: Unable creating output streaming file!" << std::endl;
430
+ }
431
+
432
+ StreamingMerger vmerger(0, &varguments, &shared_stream, total_width, total_height);
433
+ try {
434
+ size_t row_size = static_cast<size_t>(total_width) * 4;
435
+ int stripe_count = 0;
436
+ // main stripes loop
437
+ for (uint32_t current_y_offset = 0; current_y_offset < total_height; current_y_offset += stripe_height) {
438
+ int uncovered_height = total_height - current_y_offset;
439
+
440
+ // copy previous last line to the next new one (each contigue stripe must share one pixel scanline)
441
+ if (current_y_offset > 0) {
442
+ unsigned char* last_row_prev = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(stripe_height - 1));
443
+ unsigned char* first_row_curr = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(0));
444
+ std::memcpy(first_row_curr, last_row_prev, row_size);
445
+ }
446
+ // clears the part of the stripe wont be overwritten by png data
447
+ if (uncovered_height < stripe_height)
448
+ { stripe_bitmap.draw_filled_rectangle(0, 1, total_width, stripe_height - 1, 255, 255, 255);
449
+ }
450
+ // decoding data directly in the stripe buffer
451
+ uint32_t lines_to_read = std::min(stripe_height, total_height - current_y_offset);
452
+ for (uint32_t y = (current_y_offset == 0 ? 0 : 1); y < lines_to_read; y++) {
453
+ unsigned char* row_ptr = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(y));
454
+ int ret = spng_decode_row(ctx, row_ptr, row_size);
455
+ if (ret != 0 && ret != SPNG_EOI) break;
456
+ }
457
+ // stripe contour tracing
458
+ std::vector<std::string> finder_arguments = {
459
+ "--versus=a",
460
+ "--bounds",
461
+ "--strict_bounds", // <- this option is strictly needed when working with vertical merger
462
+ "--compress_uniq",
463
+ "--compress_linear"
464
+ };
465
+
466
+ PolygonFinder polygon_finder(&stripe_bitmap, &not_matcher, nullptr, &finder_arguments);
467
+ ProcessResult *result = polygon_finder.process_info();
468
+ if (result) {
469
+ std::cout << "stripe " << stripe_count << ": found polygons " << result->groups << std::endl;
470
+ ProcessResult* safe_result = result->clone();
471
+ result_clones.push_back(safe_result);
472
+ vmerger.add_tile(*safe_result, !(current_y_offset + stripe_height < total_height));
473
+ delete result;
474
+ }
475
+ stripe_count++;
476
+ }
477
+
478
+ ProcessResult *merged_result = vmerger.process_info();
479
+ std::cout << "total found polygons " << merged_result->groups << std::endl;
480
+ delete merged_result;
481
+
482
+ // frees memory
483
+ for (auto c : result_clones) {
484
+ delete c;
485
+ }
486
+ } catch (const std::exception& e) {
487
+ std::cerr << "\n[ERROR] Processing exception: " << e.what() << std::endl;
488
+ if (shared_stream.is_open()) shared_stream.close();
489
+ }
490
+ spng_ctx_free(ctx);
491
+ fclose(fp);
492
+ }
493
+
494
+ void Tests::test_l() {
495
+ stream_progressive_png_image("../images/mixed_shapes_1024x1024.png", 300);
389
496
  std::cout << "Memory usage peak: " << get_peak_rss() << " MB" << std::endl;
390
497
  }
@@ -20,4 +20,5 @@ class Tests {
20
20
  virtual void test_g();
21
21
  virtual void test_h();
22
22
  virtual void test_i();
23
+ virtual void test_l();
23
24
  };
@@ -20,6 +20,8 @@
20
20
  #include <sstream>
21
21
  #include <stdexcept>
22
22
  #include <limits>
23
+ #include <cstdint>
24
+
23
25
  #include "../bitmaps/Bitmap.h"
24
26
  #include "NodeCluster.h"
25
27
  #include "Node.h"
@@ -210,15 +212,80 @@ class PolygonFinder {
210
212
  void run_loop(M* specific_matcher, F&& fetch_color, int offset) {
211
213
  int img_h = this->source_bitmap->h();
212
214
  int bpp = this->source_bitmap->get_bytes_per_pixel();
215
+
213
216
  for (int y = 0; y < img_h; y++) {
214
217
  const unsigned char* row_ptr = this->source_bitmap->get_row_ptr(y);
215
218
  const unsigned char* p = row_ptr + (this->start_x * bpp);
219
+
216
220
  int min_x = 0;
217
221
  bool matching = false;
218
222
  unsigned char last_red_value = 0;
219
- for (int x = this->start_x; x < this->end_x; x++) {
223
+
224
+ int x = this->start_x;
225
+
226
+ if (bpp == 4) {
227
+ for (; x <= this->end_x - 4; x += 4) {
228
+ // read 4 pixels (16 bytes)
229
+ unsigned int c0 = fetch_color(p);
230
+ unsigned int c1 = fetch_color(p + 4);
231
+ unsigned int c2 = fetch_color(p + 8);
232
+ unsigned int c3 = fetch_color(p + 12);
233
+
234
+ // extracts value (used as debugging segment label)
235
+ unsigned char v0 = static_cast<unsigned char>(c0);
236
+ unsigned char v1 = static_cast<unsigned char>(c1);
237
+ unsigned char v2 = static_cast<unsigned char>(c2);
238
+ unsigned char v3 = static_cast<unsigned char>(c3);
239
+
240
+ p += 16;
241
+
242
+ bool m0 = specific_matcher->match(c0);
243
+ bool m1 = specific_matcher->match(c1);
244
+ bool m2 = specific_matcher->match(c2);
245
+ bool m3 = specific_matcher->match(c3);
246
+
247
+ if (m0) {
248
+ if (!matching) {
249
+ min_x = x; last_red_value = v0; matching = true;
250
+ }
251
+ } else if (matching) {
252
+ this->node_cluster->add_node(min_x, x - 1, y, last_red_value, offset);
253
+ matching = false;
254
+ }
255
+
256
+ if (m1) {
257
+ if (!matching) {
258
+ min_x = x + 1; last_red_value = v1; matching = true;
259
+ }
260
+ } else if (matching) {
261
+ this->node_cluster->add_node(min_x, x, y, last_red_value, offset);
262
+ matching = false;
263
+ }
264
+
265
+ if (m2) {
266
+ if (!matching) {
267
+ min_x = x + 2; last_red_value = v2; matching = true;
268
+ }
269
+ } else if (matching) {
270
+ this->node_cluster->add_node(min_x, x + 1, y, last_red_value, offset);
271
+ matching = false;
272
+ }
273
+
274
+ if (m3) {
275
+ if (!matching) {
276
+ min_x = x + 3; last_red_value = v3; matching = true;
277
+ }
278
+ } else if (matching) {
279
+ this->node_cluster->add_node(min_x, x + 2, y, last_red_value, offset);
280
+ matching = false;
281
+ }
282
+ }
283
+ }
284
+
285
+ // remaining pixels (width not a multiple of 4)
286
+ for (; x < this->end_x; x++) {
220
287
  unsigned int color = fetch_color(p);
221
- unsigned char current_val = p[0];
288
+ unsigned char current_val = static_cast<unsigned char>(color);
222
289
  p += bpp;
223
290
  if (specific_matcher->match(color)) {
224
291
  if (!matching) {
@@ -226,15 +293,15 @@ class PolygonFinder {
226
293
  last_red_value = current_val;
227
294
  matching = true;
228
295
  }
229
- if (x == this->end_x - 1) {
230
- this->node_cluster->add_node(min_x, x, y, last_red_value, offset);
231
- matching = false;
232
- }
233
296
  } else if (matching) {
234
297
  this->node_cluster->add_node(min_x, x - 1, y, last_red_value, offset);
235
298
  matching = false;
236
299
  }
237
300
  }
301
+
302
+ if (matching) {
303
+ this->node_cluster->add_node(min_x, this->end_x - 1, y, last_red_value, offset);
304
+ }
238
305
  }
239
306
  }
240
307
 
@@ -35,7 +35,6 @@ class Finder : public Poolable {
35
35
  Matcher *matcher;
36
36
  pf_Options options_;
37
37
  std::vector<std::string> input_options;
38
- Tile* whole_tile = nullptr;
39
38
  std::queue<ClippedPolygonFinder*> finders;
40
39
  std::mutex finders_mutex;
41
40
  std::map<std::string, double> reports;
@@ -45,6 +44,7 @@ class Finder : public Poolable {
45
44
  Queue<Tile*> tiles_;
46
45
  int maximum_width_;
47
46
  int height = 0;
47
+ Tile* whole_tile = nullptr;
48
48
  void process_tiles();
49
49
 
50
50
  public:
@@ -21,7 +21,7 @@ Polyline::Polyline(Tile* tile, const std::vector<Point*>& polygon, const std::op
21
21
  tile(tile)
22
22
  { if (bounds.has_value()) {
23
23
  min_x = bounds->min_x;
24
- max_x = bounds->max_x;
24
+ max_x_ = bounds->max_x;
25
25
  min_y_ = bounds->min_y;
26
26
  max_y_ = bounds->max_y;
27
27
  } else {
@@ -32,17 +32,17 @@ Polyline::Polyline(Tile* tile, const std::vector<Point*>& polygon, const std::op
32
32
 
33
33
  int Polyline::width() {
34
34
  if (raw_.empty()) return 0;
35
- return(max_x - min_x);
35
+ return(max_x_ - min_x);
36
36
  }
37
37
 
38
38
  bool Polyline::boundary() {
39
- return( tile->tg_border(Point{min_x, 0}) || tile->tg_border(Point{max_x, 0}));
39
+ return( tile->tg_border(Point{min_x, 0}) || tile->tg_border(Point{max_x_, 0}));
40
40
  }
41
41
 
42
42
  void Polyline::find_boundary() {
43
43
  if (raw_.empty()) return;
44
44
  min_x = std::numeric_limits<int>::max();
45
- max_x = -std::numeric_limits<int>::max();
45
+ max_x_ = -std::numeric_limits<int>::max();
46
46
  min_y_ = std::numeric_limits<int>::max();
47
47
  max_y_ = -std::numeric_limits<int>::max();
48
48
  for (Point* p : raw_) {
@@ -50,7 +50,7 @@ void Polyline::find_boundary() {
50
50
  int x = p->x;
51
51
  int y = p->y;
52
52
  if (x < min_x) min_x = x;
53
- if (x > max_x) max_x = x;
53
+ if (x > max_x_) max_x_ = x;
54
54
  if (y < min_y_) min_y_ = y;
55
55
  if (y > max_y_) max_y_ = y;
56
56
  }
@@ -98,7 +98,7 @@ std::string Polyline::named() {
98
98
 
99
99
  void Polyline::fill_bounds(RectBounds& target_bounds) const {
100
100
  target_bounds.min_x = this->min_x;
101
- target_bounds.max_x = this->max_x;
101
+ target_bounds.max_x = this->max_x_;
102
102
  target_bounds.min_y = this->min_y_;
103
103
  target_bounds.max_y = this->max_y_;
104
104
  }
@@ -40,10 +40,11 @@ class Polyline : public Partitionable {
40
40
  int width();
41
41
  Tile *tile = nullptr;
42
42
  Shape* shape = nullptr;
43
- std::vector<Point*> raw() const { return raw_; }
43
+ const std::vector<Point*>& raw() const { return raw_; }
44
44
  const std::vector<Part*>& parts() const { return parts_; }
45
45
  const int max_y() const { return max_y_; }
46
46
  const int min_y() const { return min_y_; }
47
+ const int max_x() const { return max_x_; }
47
48
  void clear();
48
49
  bool is_empty();
49
50
  bool any_ancients = false;
@@ -56,7 +57,7 @@ class Polyline : public Partitionable {
56
57
 
57
58
  private:
58
59
  std::vector<Point*> raw_;
59
- int min_x, max_x, min_y_, max_y_;
60
+ int min_x, max_x_, min_y_, max_y_;
60
61
  void find_boundary();
61
62
  uint32_t flags_ = 0;
62
63
  std::string named_;
@@ -0,0 +1,114 @@
1
+ /*
2
+ * StreamingMerger.cpp
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+
10
+ #include "StreamingMerger.h"
11
+ #include <sstream>
12
+ #include <algorithm>
13
+ #include <vector>
14
+ #include <string>
15
+
16
+ StreamingMerger::StreamingMerger(int number_of_threads,
17
+ std::vector<std::string>* options,
18
+ std::ofstream* stream_to,
19
+ int total_width, int total_height)
20
+ : VerticalMerger(number_of_threads, options), stream(stream_to), total_width(total_width), total_height(total_height) {
21
+ if (!stream) {
22
+ throw std::invalid_argument("Streaming requires a valid destination output. stream_to cannot be null.");
23
+ }
24
+ if (total_width <= 0 || total_height <= 0) {
25
+ throw std::invalid_argument("Streaming requires valid canvas dimensions (width and height must be > 0).");
26
+ }
27
+ }
28
+
29
+ void StreamingMerger::add_tile(ProcessResult& result, bool flush)
30
+ { VerticalMerger::add_tile(result);
31
+ if (tiles_.size() == 2) {
32
+ this->process_tiles();
33
+ this->tiles_.queue_push(this->whole_tile);
34
+ this->stream_polygons(this->whole_tile, flush);
35
+ }
36
+ }
37
+
38
+ ProcessResult* StreamingMerger::process_info() {
39
+ ProcessResult *pr = VerticalMerger::process_info();
40
+ pr->groups = this->moved;
41
+ return(pr);
42
+ }
43
+
44
+ void StreamingMerger::stream_polygons(Tile* tile, bool flush) {
45
+ ensure_header();
46
+ if (int tile_end_x = tile->end_x(); true) {
47
+ tile->shapes().erase(
48
+ std::remove_if(tile->shapes().begin(), tile->shapes().end(), [this, flush, tile_end_x](const Shape* shape) {
49
+ if (flush || shape->outer_polyline->max_x() < (tile_end_x - 1)) {
50
+ this->moved++;
51
+ this->stream_raw_polygon(shape);
52
+ return true;
53
+ }
54
+ return false;
55
+ }),
56
+ tile->shapes().end());
57
+ }
58
+ if (flush) {
59
+ ensure_footer();
60
+ }
61
+ }
62
+
63
+ void StreamingMerger::stream_raw_polygon(const Shape* shape) {
64
+ std::ostringstream outer_oss;
65
+ const std::vector<Point*> points = shape->outer_polyline->raw();
66
+ for (size_t i = 0; i < points.size(); ++i) {
67
+ outer_oss << points[i]->y << "," << points[i]->x;
68
+ if (i < points.size() - 1) outer_oss << " ";
69
+ }
70
+ *stream << svg_outer_polygon_string(outer_oss.str());
71
+
72
+ for (const auto& inner_polyline : shape->inner_polylines) {
73
+ std::ostringstream inner_oss;
74
+ const std::vector<Point*> inner_points = inner_polyline->raw();
75
+ for (size_t i = 0; i < inner_points.size(); ++i) {
76
+ inner_oss << inner_points[i]->y << "," << inner_points[i]->x;
77
+ if (i < inner_points.size() - 1) inner_oss << " ";
78
+ }
79
+ *stream << svg_inner_polygon_string(inner_oss.str());
80
+ }
81
+ }
82
+
83
+ void StreamingMerger::ensure_header() {
84
+ if (stream && stream->tellp() == 0) {
85
+ *stream << svg_header_string();
86
+ }
87
+ }
88
+
89
+ void StreamingMerger::ensure_footer() {
90
+ if (stream) {
91
+ *stream << svg_footer_string();
92
+ }
93
+ }
94
+
95
+ std::string StreamingMerger::svg_css() {
96
+ return ".out{fill:none;stroke:red;stroke-width:1;}.in{fill:none;stroke:green;stroke-width:1;}.out:hover{stroke:yellow;}";
97
+ }
98
+
99
+ std::string StreamingMerger::svg_header_string() {
100
+ return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + std::to_string(total_width) +
101
+ "\" height=\"" + std::to_string(total_height) + "\"><style>" + svg_css() + "</style>";
102
+ }
103
+
104
+ std::string StreamingMerger::svg_footer_string() {
105
+ return "</svg>";
106
+ }
107
+
108
+ std::string StreamingMerger::svg_outer_polygon_string(std::string_view points) {
109
+ return "<polygon points=\"" + std::string(points) + "\" class=\"out\"/>";
110
+ }
111
+
112
+ std::string StreamingMerger::svg_inner_polygon_string(std::string_view points) {
113
+ return "<polygon points=\"" + std::string(points) + "\" class=\"in\"/>";
114
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * StreamingMerger.h
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+
10
+ #pragma once
11
+ #include "VerticalMerger.h"
12
+ #include <fstream>
13
+ #include <string>
14
+ #include <string_view>
15
+ #include <stdexcept>
16
+ #include <vector>
17
+
18
+ class StreamingMerger : public VerticalMerger {
19
+ private:
20
+ std::ofstream* stream;
21
+ int total_width;
22
+ int total_height;
23
+ int moved = 0;
24
+ void ensure_header();
25
+ void ensure_footer();
26
+ void stream_polygons(Tile* tile, bool flush = false);
27
+ void stream_raw_polygon(const Shape* shape);
28
+ virtual std::string svg_css();
29
+ virtual std::string svg_header_string();
30
+ virtual std::string svg_footer_string();
31
+ virtual std::string svg_outer_polygon_string(std::string_view points);
32
+ virtual std::string svg_inner_polygon_string(std::string_view points);
33
+
34
+ public:
35
+ StreamingMerger(int number_of_threads,
36
+ std::vector<std::string>* options,
37
+ std::ofstream* stream_to,
38
+ int total_width, int total_height);
39
+ void add_tile(ProcessResult& result, bool flush = false);
40
+ ProcessResult* process_info() override;
41
+ };
@@ -96,6 +96,8 @@
96
96
  #include "PolygonFinder/src/polygon/finder/concurrent/HorizontalMerger.cpp"
97
97
  #include "PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.h"
98
98
  #include "PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.cpp"
99
+ #include "PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.h"
100
+ #include "PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.cpp"
99
101
  #include "PolygonFinder/src/polygon/finder/concurrent/ShapePool.h"
100
102
  #include "PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp"
101
103
  extern "C" {
@@ -290,6 +292,36 @@ ProcessResult ruby_result_to_process_result(Rice::Object rb_result) {
290
292
 
291
293
  } // namespace Rice::detail
292
294
 
295
+ struct OfstreamWrapper {
296
+ std::string path;
297
+ std::ofstream stream;
298
+ OfstreamWrapper(std::string p) : path(p), stream(p, std::ios::out) {}
299
+ void close() { stream.close(); }
300
+ bool closed() { return !stream.is_open(); }
301
+ void rewind() { /* noop */ }
302
+ std::string read() {
303
+ stream.flush();
304
+ std::ifstream in(path);
305
+ return std::string((std::istreambuf_iterator<char>(in)),
306
+ std::istreambuf_iterator<char>());
307
+ }
308
+ std::ofstream& get_stream() { return stream; }
309
+ };
310
+
311
+ StreamingMerger* create_streaming_merger(Object self,
312
+ int number_of_threads,
313
+ std::vector<std::string>* options,
314
+ Object stream_obj,
315
+ int total_width,
316
+ int total_height) {
317
+ OfstreamWrapper* wrapper = Rice::detail::From_Ruby<OfstreamWrapper*>().convert(stream_obj.value());
318
+ return new StreamingMerger(number_of_threads, options, &wrapper->get_stream(), total_width, total_height);
319
+ }
320
+
321
+ OfstreamWrapper* create_ofstream(Object self, std::string path) {
322
+ return new OfstreamWrapper(path);
323
+ }
324
+
293
325
  extern "C"
294
326
  void Init_cpp_polygon_finder() {
295
327
  #ifdef HAVE_TCMALLOC
@@ -384,9 +416,27 @@ void Init_cpp_polygon_finder() {
384
416
  define_class<HorizontalMerger, Merger>("CPPHorizontalMerger")
385
417
  .define_constructor(Constructor<HorizontalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true);
386
418
 
387
- Data_Type<VerticalMerger> rb_cVerticalMerger =
419
+ Data_Type<VerticalMerger> rb_cVerticalMerger =
388
420
  define_class<VerticalMerger, Merger>("CPPVerticalMerger")
389
- .define_constructor(Constructor<VerticalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true);
421
+ .define_constructor(Constructor<VerticalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true)
422
+ .define_method("add_tile", [](VerticalMerger& self, Object rb_result) {
423
+ ProcessResult pr = Rice::detail::ruby_result_to_process_result(rb_result);
424
+ self.add_tile(pr);
425
+ });
426
+
427
+ Data_Type<OfstreamWrapper> rb_cstd_ofstream = define_class<OfstreamWrapper>("CPPOfstream");
428
+ rb_cstd_ofstream.define_singleton_method("new", &create_ofstream);
429
+ rb_cstd_ofstream.define_method("close", [](OfstreamWrapper& self) { self.close(); });
430
+ rb_cstd_ofstream.define_method("closed?", [](OfstreamWrapper& self) { return self.closed(); });
431
+ rb_cstd_ofstream.define_method("rewind", [](OfstreamWrapper& self) { self.rewind(); });
432
+ rb_cstd_ofstream.define_method("read", [](OfstreamWrapper& self) { return self.read(); });
433
+
434
+ Data_Type<StreamingMerger> rb_cstreaming_merger = define_class<StreamingMerger, VerticalMerger>("CPPStreamingMerger");
435
+ rb_cstreaming_merger.define_method("add_tile", [](StreamingMerger& self, Object rb_result, bool flush) {
436
+ ProcessResult pr = Rice::detail::ruby_result_to_process_result(rb_result);
437
+ self.add_tile(pr, flush);
438
+ });
439
+ rb_cstreaming_merger.define_singleton_method("new", &create_streaming_merger);
390
440
 
391
441
  Module mContrek = define_module("Contrek");
392
442
  Module mCpp = define_module_under(mContrek, "Cpp");
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "mkmf-rice"
2
4
 
3
5
  has_tcmalloc = find_library("tcmalloc", "malloc")