flexablelib-speedups 3.3.1__tar.gz → 3.5.0__tar.gz

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.
@@ -2,3 +2,4 @@ include README.md
2
2
  include LICENSE
3
3
  include flexablelib_speedups/_frames.c
4
4
  include flexablelib_speedups/_width_tables.h
5
+ include flexablelib_speedups/_ascii_table.h
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flexablelib-speedups
3
- Version: 3.3.1
4
- Summary: Optional C acceleration of terminal frame drawing for flexablelib (cut, pad, borders, frame diffs).
3
+ Version: 3.5.0
4
+ Summary: Optional C acceleration of terminal frame drawing for flexablelib (cut, pad, borders, frame diffs, images).
5
5
  Home-page: https://github.com/Opus-Perpetuus/flexablelib
6
6
  Author: Benjamin Ramirez
7
7
  Author-email: chilerito12@gmail.com
@@ -6,7 +6,7 @@ directly: :mod:`flexablelib._frames` picks it up when it is installed and
6
6
  falls back to identical pure-Python code when it is not.
7
7
  """
8
8
 
9
- __version__ = '3.3.1'
9
+ __version__ = '3.5.0'
10
10
 
11
11
  from . import _frames # noqa: F401,E402
12
12
 
@@ -0,0 +1,342 @@
1
+ /* Generated by tools/gen_ascii_table.py from
2
+ * flexablelib._frames.ASCII_FOLD. Do not edit. */
3
+
4
+ struct ascii_fold_entry { Py_UCS4 cp; char ch; };
5
+
6
+ static const struct ascii_fold_entry ascii_fold_table[] = {
7
+ {0x00A0, ' '},
8
+ {0x00A6, '|'},
9
+ {0x00AB, '<'},
10
+ {0x00AC, '-'},
11
+ {0x00B0, 'o'},
12
+ {0x00B1, '+'},
13
+ {0x00B7, '.'},
14
+ {0x00BB, '>'},
15
+ {0x00D7, 'x'},
16
+ {0x00F7, '/'},
17
+ {0x2010, '-'},
18
+ {0x2011, '-'},
19
+ {0x2012, '-'},
20
+ {0x2013, '-'},
21
+ {0x2014, '-'},
22
+ {0x2015, '-'},
23
+ {0x2018, '\''},
24
+ {0x2019, '\''},
25
+ {0x201A, '\''},
26
+ {0x201C, '"'},
27
+ {0x201D, '"'},
28
+ {0x201E, '"'},
29
+ {0x2022, '.'},
30
+ {0x2023, '>'},
31
+ {0x2026, '.'},
32
+ {0x2032, '\''},
33
+ {0x2033, '"'},
34
+ {0x2039, '<'},
35
+ {0x203A, '>'},
36
+ {0x2043, '-'},
37
+ {0x20AC, 'E'},
38
+ {0x2139, 'i'},
39
+ {0x2190, '<'},
40
+ {0x2191, '^'},
41
+ {0x2192, '>'},
42
+ {0x2193, 'v'},
43
+ {0x2194, '-'},
44
+ {0x2195, '|'},
45
+ {0x219E, '<'},
46
+ {0x21A0, '>'},
47
+ {0x21A4, '<'},
48
+ {0x21A6, '>'},
49
+ {0x21A9, '<'},
50
+ {0x21B5, '<'},
51
+ {0x21D0, '<'},
52
+ {0x21D1, '^'},
53
+ {0x21D2, '>'},
54
+ {0x21D3, 'v'},
55
+ {0x21D4, '-'},
56
+ {0x21D5, '|'},
57
+ {0x2205, '0'},
58
+ {0x2212, '-'},
59
+ {0x2219, '.'},
60
+ {0x221A, '+'},
61
+ {0x221E, '8'},
62
+ {0x223C, '~'},
63
+ {0x2248, '~'},
64
+ {0x2260, '#'},
65
+ {0x2264, '<'},
66
+ {0x2265, '>'},
67
+ {0x22EF, '.'},
68
+ {0x23CE, '<'},
69
+ {0x2423, '_'},
70
+ {0x2500, '-'},
71
+ {0x2501, '-'},
72
+ {0x2502, '|'},
73
+ {0x2503, '|'},
74
+ {0x2504, '-'},
75
+ {0x2505, '-'},
76
+ {0x2506, '|'},
77
+ {0x2507, '|'},
78
+ {0x2508, '-'},
79
+ {0x2509, '-'},
80
+ {0x250A, '|'},
81
+ {0x250B, '|'},
82
+ {0x250C, '+'},
83
+ {0x250D, '+'},
84
+ {0x250E, '+'},
85
+ {0x250F, '+'},
86
+ {0x2510, '+'},
87
+ {0x2511, '+'},
88
+ {0x2512, '+'},
89
+ {0x2513, '+'},
90
+ {0x2514, '+'},
91
+ {0x2515, '+'},
92
+ {0x2516, '+'},
93
+ {0x2517, '+'},
94
+ {0x2518, '+'},
95
+ {0x2519, '+'},
96
+ {0x251A, '+'},
97
+ {0x251B, '+'},
98
+ {0x251C, '+'},
99
+ {0x251D, '+'},
100
+ {0x251E, '+'},
101
+ {0x251F, '+'},
102
+ {0x2520, '+'},
103
+ {0x2521, '+'},
104
+ {0x2522, '+'},
105
+ {0x2523, '+'},
106
+ {0x2524, '+'},
107
+ {0x2525, '+'},
108
+ {0x2526, '+'},
109
+ {0x2527, '+'},
110
+ {0x2528, '+'},
111
+ {0x2529, '+'},
112
+ {0x252A, '+'},
113
+ {0x252B, '+'},
114
+ {0x252C, '+'},
115
+ {0x252D, '+'},
116
+ {0x252E, '+'},
117
+ {0x252F, '+'},
118
+ {0x2530, '+'},
119
+ {0x2531, '+'},
120
+ {0x2532, '+'},
121
+ {0x2533, '+'},
122
+ {0x2534, '+'},
123
+ {0x2535, '+'},
124
+ {0x2536, '+'},
125
+ {0x2537, '+'},
126
+ {0x2538, '+'},
127
+ {0x2539, '+'},
128
+ {0x253A, '+'},
129
+ {0x253B, '+'},
130
+ {0x253C, '+'},
131
+ {0x253D, '+'},
132
+ {0x253E, '+'},
133
+ {0x253F, '+'},
134
+ {0x2540, '+'},
135
+ {0x2541, '+'},
136
+ {0x2542, '+'},
137
+ {0x2543, '+'},
138
+ {0x2544, '+'},
139
+ {0x2545, '+'},
140
+ {0x2546, '+'},
141
+ {0x2547, '+'},
142
+ {0x2548, '+'},
143
+ {0x2549, '+'},
144
+ {0x254A, '+'},
145
+ {0x254B, '+'},
146
+ {0x254C, '-'},
147
+ {0x254D, '-'},
148
+ {0x254E, '|'},
149
+ {0x254F, '|'},
150
+ {0x2550, '='},
151
+ {0x2551, '|'},
152
+ {0x2552, '+'},
153
+ {0x2553, '+'},
154
+ {0x2554, '+'},
155
+ {0x2555, '+'},
156
+ {0x2556, '+'},
157
+ {0x2557, '+'},
158
+ {0x2558, '+'},
159
+ {0x2559, '+'},
160
+ {0x255A, '+'},
161
+ {0x255B, '+'},
162
+ {0x255C, '+'},
163
+ {0x255D, '+'},
164
+ {0x255E, '+'},
165
+ {0x255F, '+'},
166
+ {0x2560, '+'},
167
+ {0x2561, '+'},
168
+ {0x2562, '+'},
169
+ {0x2563, '+'},
170
+ {0x2564, '+'},
171
+ {0x2565, '+'},
172
+ {0x2566, '+'},
173
+ {0x2567, '+'},
174
+ {0x2568, '+'},
175
+ {0x2569, '+'},
176
+ {0x256A, '+'},
177
+ {0x256B, '+'},
178
+ {0x256C, '+'},
179
+ {0x256D, '+'},
180
+ {0x256E, '+'},
181
+ {0x256F, '+'},
182
+ {0x2570, '+'},
183
+ {0x2571, '/'},
184
+ {0x2572, '\\'},
185
+ {0x2573, 'X'},
186
+ {0x2574, '-'},
187
+ {0x2575, '|'},
188
+ {0x2576, '-'},
189
+ {0x2577, '|'},
190
+ {0x2578, '-'},
191
+ {0x2579, '|'},
192
+ {0x257A, '-'},
193
+ {0x257B, '|'},
194
+ {0x257C, '-'},
195
+ {0x257D, '|'},
196
+ {0x257E, '-'},
197
+ {0x257F, '|'},
198
+ {0x2580, '-'},
199
+ {0x2581, '_'},
200
+ {0x2582, '_'},
201
+ {0x2583, '.'},
202
+ {0x2584, '-'},
203
+ {0x2585, '='},
204
+ {0x2586, '='},
205
+ {0x2587, '#'},
206
+ {0x2588, '#'},
207
+ {0x2589, '#'},
208
+ {0x258A, '#'},
209
+ {0x258B, '='},
210
+ {0x258C, '='},
211
+ {0x258D, '-'},
212
+ {0x258E, '-'},
213
+ {0x258F, '|'},
214
+ {0x2590, '='},
215
+ {0x2591, '.'},
216
+ {0x2592, ':'},
217
+ {0x2593, '#'},
218
+ {0x2594, '-'},
219
+ {0x2595, '|'},
220
+ {0x2596, '#'},
221
+ {0x2597, '#'},
222
+ {0x2598, '#'},
223
+ {0x2599, '#'},
224
+ {0x259A, '#'},
225
+ {0x259B, '#'},
226
+ {0x259C, '#'},
227
+ {0x259D, '#'},
228
+ {0x259E, '#'},
229
+ {0x259F, '#'},
230
+ {0x25A0, '#'},
231
+ {0x25A1, 'o'},
232
+ {0x25A2, '*'},
233
+ {0x25A3, '*'},
234
+ {0x25A4, '*'},
235
+ {0x25A5, '*'},
236
+ {0x25A6, '*'},
237
+ {0x25A7, '*'},
238
+ {0x25A8, '*'},
239
+ {0x25A9, '*'},
240
+ {0x25AA, '#'},
241
+ {0x25AB, 'o'},
242
+ {0x25AC, '#'},
243
+ {0x25AD, 'o'},
244
+ {0x25AE, '#'},
245
+ {0x25AF, 'o'},
246
+ {0x25B0, '*'},
247
+ {0x25B1, '*'},
248
+ {0x25B2, '^'},
249
+ {0x25B3, '^'},
250
+ {0x25B4, '^'},
251
+ {0x25B5, '^'},
252
+ {0x25B6, '>'},
253
+ {0x25B7, '>'},
254
+ {0x25B8, '>'},
255
+ {0x25B9, '>'},
256
+ {0x25BA, '>'},
257
+ {0x25BB, '>'},
258
+ {0x25BC, 'v'},
259
+ {0x25BD, 'v'},
260
+ {0x25BE, 'v'},
261
+ {0x25BF, 'v'},
262
+ {0x25C0, '<'},
263
+ {0x25C1, '<'},
264
+ {0x25C2, '<'},
265
+ {0x25C3, '<'},
266
+ {0x25C4, '<'},
267
+ {0x25C5, '<'},
268
+ {0x25C6, '*'},
269
+ {0x25C7, '*'},
270
+ {0x25C8, '*'},
271
+ {0x25C9, '*'},
272
+ {0x25CA, '*'},
273
+ {0x25CB, 'o'},
274
+ {0x25CC, 'o'},
275
+ {0x25CD, '*'},
276
+ {0x25CE, '*'},
277
+ {0x25CF, '*'},
278
+ {0x25D0, 'o'},
279
+ {0x25D1, 'o'},
280
+ {0x25D2, 'o'},
281
+ {0x25D3, 'o'},
282
+ {0x25D4, '*'},
283
+ {0x25D5, '*'},
284
+ {0x25D6, '*'},
285
+ {0x25D7, '*'},
286
+ {0x25D8, '*'},
287
+ {0x25D9, '*'},
288
+ {0x25DA, '*'},
289
+ {0x25DB, '*'},
290
+ {0x25DC, '*'},
291
+ {0x25DD, '*'},
292
+ {0x25DE, '*'},
293
+ {0x25DF, '*'},
294
+ {0x25E0, '*'},
295
+ {0x25E1, '*'},
296
+ {0x25E2, '*'},
297
+ {0x25E3, '*'},
298
+ {0x25E4, '*'},
299
+ {0x25E5, '*'},
300
+ {0x25E6, 'o'},
301
+ {0x25E7, '*'},
302
+ {0x25E8, '*'},
303
+ {0x25E9, '*'},
304
+ {0x25EA, '*'},
305
+ {0x25EB, '*'},
306
+ {0x25EC, '*'},
307
+ {0x25ED, '*'},
308
+ {0x25EE, '*'},
309
+ {0x25EF, 'o'},
310
+ {0x25F0, '*'},
311
+ {0x25F1, '*'},
312
+ {0x25F2, '*'},
313
+ {0x25F3, '*'},
314
+ {0x25F4, '*'},
315
+ {0x25F5, '*'},
316
+ {0x25F6, '*'},
317
+ {0x25F7, '*'},
318
+ {0x25F8, '*'},
319
+ {0x25F9, '*'},
320
+ {0x25FA, '*'},
321
+ {0x25FB, '*'},
322
+ {0x25FC, '*'},
323
+ {0x25FF, '*'},
324
+ {0x2605, '*'},
325
+ {0x2606, '*'},
326
+ {0x2610, '.'},
327
+ {0x2611, 'x'},
328
+ {0x2612, 'x'},
329
+ {0x26A0, '!'},
330
+ {0x2713, '+'},
331
+ {0x2714, '+'},
332
+ {0x2716, 'x'},
333
+ {0x2717, 'x'},
334
+ {0x2718, 'x'},
335
+ {0x2726, '*'},
336
+ {0x2727, '*'},
337
+ {0x27F5, '<'},
338
+ {0x27F6, '>'},
339
+ {0x2B51, '*'},
340
+ };
341
+
342
+ #define ASCII_FOLD_COUNT 333
@@ -19,6 +19,7 @@
19
19
  #include <string.h>
20
20
 
21
21
  #include "_width_tables.h"
22
+ #include "_ascii_table.h"
22
23
 
23
24
  #define ESC 0x1B
24
25
  #define BEL 0x07
@@ -682,6 +683,226 @@ fail:
682
683
  return NULL;
683
684
  }
684
685
 
686
+ /* ------------------------------------------------------------------------
687
+ * ascii_fold(text) -> str
688
+ *
689
+ * Every symbol of flexablelib._frames.ASCII_FOLD replaced by its one-column
690
+ * ASCII stand-in; braille cells become ' ', '.', ':' or '#' by dot count.
691
+ */
692
+ static int
693
+ fold_cp(Py_UCS4 cp)
694
+ {
695
+ size_t low = 0, high = ASCII_FOLD_COUNT;
696
+ if (cp < 0x80) return 0;
697
+ if (cp >= 0x2800 && cp <= 0x28FF) {
698
+ unsigned bits = (unsigned)(cp - 0x2800);
699
+ int dots = 0;
700
+ while (bits) { dots += (int)(bits & 1u); bits >>= 1; }
701
+ return dots == 0 ? ' ' : dots <= 2 ? '.' : dots <= 5 ? ':' : '#';
702
+ }
703
+ while (low < high) {
704
+ size_t mid = low + (high - low) / 2;
705
+ if (cp < ascii_fold_table[mid].cp) high = mid;
706
+ else if (cp > ascii_fold_table[mid].cp) low = mid + 1;
707
+ else return (unsigned char)ascii_fold_table[mid].ch;
708
+ }
709
+ return 0;
710
+ }
711
+
712
+ static PyObject *
713
+ py_ascii_fold(PyObject *m, PyObject *text)
714
+ {
715
+ int kind, okind;
716
+ const void *data;
717
+ void *odata;
718
+ Py_ssize_t length, i;
719
+ Py_UCS4 maxchar = 0;
720
+ int changed = 0;
721
+ PyObject *out;
722
+ (void)m;
723
+ if (unpack(text, &kind, &data, &length) < 0) return NULL;
724
+ if (PyUnicode_IS_ASCII(text)) return Py_NewRef(text);
725
+ for (i = 0; i < length; i++) {
726
+ Py_UCS4 cp = PyUnicode_READ(kind, data, i);
727
+ int c = fold_cp(cp);
728
+ if (c) { changed = 1; cp = (Py_UCS4)c; }
729
+ if (cp > maxchar) maxchar = cp;
730
+ }
731
+ if (!changed) return Py_NewRef(text);
732
+ out = PyUnicode_New(length, maxchar);
733
+ if (!out) return NULL;
734
+ okind = PyUnicode_KIND(out);
735
+ odata = PyUnicode_DATA(out);
736
+ for (i = 0; i < length; i++) {
737
+ Py_UCS4 cp = PyUnicode_READ(kind, data, i);
738
+ int c = fold_cp(cp);
739
+ PyUnicode_WRITE(okind, odata, i, c ? (Py_UCS4)c : cp);
740
+ }
741
+ return out;
742
+ }
743
+
744
+ /* ------------------------------------------------------------------------
745
+ * extract_marks(lines) -> (clean_lines, regions)
746
+ *
747
+ * Removes the zero-width hit-region marks (ESC ] 8970 ; id BEL opens,
748
+ * ESC ] 8970 ; BEL closes) and reports (id, row, x0, x1, depth) per span.
749
+ */
750
+ static Py_ssize_t
751
+ measure_range(int kind, const void *data, Py_ssize_t start, Py_ssize_t end)
752
+ {
753
+ Py_ssize_t total = 0, pos = start;
754
+ int prev = 0;
755
+ while (pos < end) {
756
+ Py_UCS4 cp = PyUnicode_READ(kind, data, pos);
757
+ int w;
758
+ if (cp == ESC) {
759
+ Py_ssize_t skip = seq_len(kind, data, end, pos);
760
+ if (skip > 0) { pos += skip; continue; }
761
+ }
762
+ if (cp == VS16) {
763
+ if (prev == 1) { total += 1; prev = 2; }
764
+ pos++;
765
+ continue;
766
+ }
767
+ w = cp_width(cp);
768
+ total += w;
769
+ prev = w;
770
+ pos++;
771
+ }
772
+ return total;
773
+ }
774
+
775
+ static int
776
+ is_mark(int kind, const void *data, Py_ssize_t length, Py_ssize_t pos)
777
+ {
778
+ static const char tag[] = "\x1b]8970;";
779
+ Py_ssize_t k;
780
+ if (pos + 7 > length) return 0;
781
+ for (k = 0; k < 7; k++)
782
+ if (PyUnicode_READ(kind, data, pos + k) != (Py_UCS4)(unsigned char)tag[k])
783
+ return 0;
784
+ return 1;
785
+ }
786
+
787
+ #define MARK_DEPTH 64
788
+
789
+ static int
790
+ add_region(PyObject *regions, long ident, Py_ssize_t row, Py_ssize_t x0,
791
+ Py_ssize_t x1, Py_ssize_t depth)
792
+ {
793
+ PyObject *t = Py_BuildValue("(lnnnn)", ident, row, x0, x1, depth);
794
+ int rc;
795
+ if (!t) return -1;
796
+ rc = PyList_Append(regions, t);
797
+ Py_DECREF(t);
798
+ return rc;
799
+ }
800
+
801
+ static PyObject *
802
+ py_extract_marks(PyObject *m, PyObject *arg)
803
+ {
804
+ PyObject *seq, *out = NULL, *regions = NULL, *result = NULL;
805
+ Py_ssize_t n, row;
806
+ long ids[MARK_DEPTH];
807
+ Py_ssize_t starts[MARK_DEPTH];
808
+ (void)m;
809
+ seq = PySequence_Fast(arg, "lines must be a sequence of str");
810
+ if (!seq) return NULL;
811
+ n = PySequence_Fast_GET_SIZE(seq);
812
+ out = PyList_New(n);
813
+ regions = PyList_New(0);
814
+ if (!out || !regions) goto done;
815
+ for (row = 0; row < n; row++) {
816
+ PyObject *line = PySequence_Fast_GET_ITEM(seq, row);
817
+ PyObject *parts = NULL, *joined, *empty;
818
+ int kind;
819
+ const void *data;
820
+ Py_ssize_t length, pos = 0, k, col = 0, depth = 0, overflow = 0;
821
+ int found = 0;
822
+ if (unpack(line, &kind, &data, &length) < 0) goto done;
823
+ for (k = 0; k < length; k++) {
824
+ if (PyUnicode_READ(kind, data, k) == ESC &&
825
+ is_mark(kind, data, length, k)) { found = 1; break; }
826
+ }
827
+ if (!found) {
828
+ PyList_SET_ITEM(out, row, Py_NewRef(line));
829
+ continue;
830
+ }
831
+ parts = PyList_New(0);
832
+ if (!parts) goto done;
833
+ for (;;) {
834
+ Py_ssize_t mk = -1, end = -1, q;
835
+ PyObject *seg;
836
+ for (k = pos; k < length; k++) {
837
+ if (PyUnicode_READ(kind, data, k) == ESC &&
838
+ is_mark(kind, data, length, k)) { mk = k; break; }
839
+ }
840
+ q = mk < 0 ? length : mk;
841
+ seg = PyUnicode_Substring(line, pos, q);
842
+ if (!seg || PyList_Append(parts, seg) < 0) {
843
+ Py_XDECREF(seg); Py_DECREF(parts); goto done;
844
+ }
845
+ Py_DECREF(seg);
846
+ col += measure_range(kind, data, pos, q);
847
+ if (mk < 0) break;
848
+ for (k = mk; k < length; k++) {
849
+ if (PyUnicode_READ(kind, data, k) == BEL) { end = k; break; }
850
+ }
851
+ if (end < 0) break;
852
+ {
853
+ Py_ssize_t a = mk + 7, len = end - a;
854
+ int digits = len > 0 && len <= 9;
855
+ long ident = 0;
856
+ for (k = a; digits && k < end; k++) {
857
+ Py_UCS4 c = PyUnicode_READ(kind, data, k);
858
+ if (c < '0' || c > '9') digits = 0;
859
+ else ident = ident * 10 + (long)(c - '0');
860
+ }
861
+ if (digits) {
862
+ if (depth < MARK_DEPTH) {
863
+ ids[depth] = ident;
864
+ starts[depth] = col;
865
+ depth++;
866
+ } else {
867
+ overflow++;
868
+ }
869
+ } else if (len == 0) {
870
+ if (overflow) {
871
+ overflow--;
872
+ } else if (depth > 0) {
873
+ depth--;
874
+ if (add_region(regions, ids[depth], row, starts[depth],
875
+ col, depth) < 0) {
876
+ Py_DECREF(parts); goto done;
877
+ }
878
+ }
879
+ }
880
+ }
881
+ pos = end + 1;
882
+ }
883
+ while (depth > 0) {
884
+ depth--;
885
+ if (add_region(regions, ids[depth], row, starts[depth], col,
886
+ depth) < 0) {
887
+ Py_DECREF(parts); goto done;
888
+ }
889
+ }
890
+ empty = PyUnicode_New(0, 0);
891
+ if (!empty) { Py_DECREF(parts); goto done; }
892
+ joined = PyUnicode_Join(empty, parts);
893
+ Py_DECREF(empty);
894
+ Py_DECREF(parts);
895
+ if (!joined) goto done;
896
+ PyList_SET_ITEM(out, row, joined);
897
+ }
898
+ result = PyTuple_Pack(2, out, regions);
899
+ done:
900
+ Py_XDECREF(out);
901
+ Py_XDECREF(regions);
902
+ Py_DECREF(seq);
903
+ return result;
904
+ }
905
+
685
906
  static PyMethodDef methods[] = {
686
907
  {"visible_width", py_visible_width, METH_O, "Visible terminal columns of a string."},
687
908
  {"strip_ansi", py_strip_ansi, METH_O, "The string without ANSI escapes."},
@@ -695,6 +916,9 @@ static PyMethodDef methods[] = {
695
916
  "border_grid(width, height, horiz, vert, glyphs) -> list of str"},
696
917
  {"changed_lines", py_changed_lines, METH_VARARGS, "changed_lines(old, new)"},
697
918
  {"render_diff", py_render_diff, METH_VARARGS, "render_diff(old, new) -> str"},
919
+ {"ascii_fold", py_ascii_fold, METH_O, "ascii_fold(text) -> str"},
920
+ {"extract_marks", py_extract_marks, METH_O,
921
+ "extract_marks(lines) -> (clean_lines, regions)"},
698
922
  {NULL, NULL, 0, NULL}
699
923
  };
700
924
 
@@ -0,0 +1,668 @@
1
+ /*
2
+ * flexablelib_speedups._images -- C versions of the image primitives in
3
+ * flexablelib/_images.py: PNG unfiltering, area-average resizing, median-cut
4
+ * quantization, the sixel encoder and the Unicode/ASCII cell renderer.
5
+ *
6
+ * Every function gives exactly the result of its Python reference (integer
7
+ * arithmetic only, same tie-breaking); tests/test_v350_images.py compares
8
+ * both on random images.
9
+ */
10
+ #define PY_SSIZE_T_CLEAN
11
+ #include <Python.h>
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+
15
+ typedef long long i64;
16
+
17
+ /* ------------------------------------------------------------------ */
18
+ /* growable byte buffer */
19
+
20
+ typedef struct { char *p; Py_ssize_t n, cap; } Buf;
21
+
22
+ static int buf_grow(Buf *b, Py_ssize_t extra)
23
+ {
24
+ if (b->n + extra <= b->cap) return 0;
25
+ Py_ssize_t cap = b->cap ? b->cap : 256;
26
+ while (cap < b->n + extra) cap *= 2;
27
+ char *p = (char *)realloc(b->p, (size_t)cap);
28
+ if (!p) { PyErr_NoMemory(); return -1; }
29
+ b->p = p; b->cap = cap;
30
+ return 0;
31
+ }
32
+
33
+ static int buf_put(Buf *b, const char *s, Py_ssize_t len)
34
+ {
35
+ if (buf_grow(b, len) < 0) return -1;
36
+ memcpy(b->p + b->n, s, (size_t)len);
37
+ b->n += len;
38
+ return 0;
39
+ }
40
+
41
+ static int buf_str(Buf *b, const char *s) { return buf_put(b, s, (Py_ssize_t)strlen(s)); }
42
+
43
+ static int buf_cp(Buf *b, unsigned int cp)
44
+ {
45
+ char t[4]; int n;
46
+ if (cp < 0x80) { t[0] = (char)cp; n = 1; }
47
+ else if (cp < 0x800) { t[0] = (char)(0xC0 | (cp >> 6)); t[1] = (char)(0x80 | (cp & 63)); n = 2; }
48
+ else if (cp < 0x10000) { t[0] = (char)(0xE0 | (cp >> 12)); t[1] = (char)(0x80 | ((cp >> 6) & 63));
49
+ t[2] = (char)(0x80 | (cp & 63)); n = 3; }
50
+ else { t[0] = (char)(0xF0 | (cp >> 18)); t[1] = (char)(0x80 | ((cp >> 12) & 63));
51
+ t[2] = (char)(0x80 | ((cp >> 6) & 63)); t[3] = (char)(0x80 | (cp & 63)); n = 4; }
52
+ return buf_put(b, t, n);
53
+ }
54
+
55
+ static int buf_int(Buf *b, long v)
56
+ {
57
+ char t[32];
58
+ int n = snprintf(t, sizeof t, "%ld", v);
59
+ return buf_put(b, t, n);
60
+ }
61
+
62
+ /* ------------------------------------------------------------------ */
63
+ /* unfilter */
64
+
65
+ static PyObject *
66
+ img_unfilter(PyObject *self, PyObject *args)
67
+ {
68
+ Py_buffer in;
69
+ Py_ssize_t rows, stride, bpp;
70
+ if (!PyArg_ParseTuple(args, "y*nnn", &in, &rows, &stride, &bpp)) return NULL;
71
+ if (rows < 0 || stride < 0 || bpp < 1) {
72
+ PyBuffer_Release(&in);
73
+ PyErr_SetString(PyExc_ValueError, "bad sizes");
74
+ return NULL;
75
+ }
76
+ const unsigned char *src = (const unsigned char *)in.buf;
77
+ PyObject *res = PyBytes_FromStringAndSize(NULL, rows * stride);
78
+ if (!res) { PyBuffer_Release(&in); return NULL; }
79
+ unsigned char *out = (unsigned char *)PyBytes_AS_STRING(res);
80
+ Py_ssize_t pos = 0;
81
+ for (Py_ssize_t r = 0; r < rows; r++) {
82
+ if (pos + 1 + stride > in.len) goto truncated;
83
+ int ft = src[pos];
84
+ const unsigned char *line = src + pos + 1;
85
+ unsigned char *o = out + r * stride;
86
+ const unsigned char *prev = r ? out + (r - 1) * stride : NULL;
87
+ pos += 1 + stride;
88
+ for (Py_ssize_t i = 0; i < stride; i++) {
89
+ int a = i >= bpp ? o[i - bpp] : 0;
90
+ int b = prev ? prev[i] : 0;
91
+ int c = (prev && i >= bpp) ? prev[i - bpp] : 0;
92
+ int pred;
93
+ switch (ft) {
94
+ case 0: pred = 0; break;
95
+ case 1: pred = a; break;
96
+ case 2: pred = b; break;
97
+ case 3: pred = (a + b) >> 1; break;
98
+ case 4: {
99
+ int p = a + b - c;
100
+ int pa = abs(p - a), pb = abs(p - b), pc = abs(p - c);
101
+ pred = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c);
102
+ break; }
103
+ default:
104
+ Py_DECREF(res); PyBuffer_Release(&in);
105
+ PyErr_Format(PyExc_ValueError, "PNG con filtro desconocido %d", ft);
106
+ return NULL;
107
+ }
108
+ o[i] = (unsigned char)((line[i] + pred) & 255);
109
+ }
110
+ }
111
+ PyBuffer_Release(&in);
112
+ return res;
113
+ truncated:
114
+ Py_DECREF(res);
115
+ PyBuffer_Release(&in);
116
+ {
117
+ PyObject *mod = PyImport_ImportModule("flexablelib._images");
118
+ PyObject *cls = mod ? PyObject_GetAttrString(mod, "ImageError") : NULL;
119
+ Py_XDECREF(mod);
120
+ if (cls) { PyErr_SetString(cls, "PNG truncado"); Py_DECREF(cls); }
121
+ }
122
+ return NULL;
123
+ }
124
+
125
+ /* ------------------------------------------------------------------ */
126
+ /* resize */
127
+
128
+ static void spans(Py_ssize_t size, Py_ssize_t n, Py_ssize_t *lo, Py_ssize_t *hi)
129
+ {
130
+ for (Py_ssize_t o = 0; o < n; o++) {
131
+ Py_ssize_t a = o * size / n;
132
+ Py_ssize_t b = ((o + 1) * size + n - 1) / n;
133
+ lo[o] = a;
134
+ hi[o] = b > a + 1 ? b : a + 1;
135
+ }
136
+ }
137
+
138
+ static PyObject *
139
+ img_resize(PyObject *self, PyObject *args)
140
+ {
141
+ Py_buffer in;
142
+ Py_ssize_t w, h, nw, nh;
143
+ if (!PyArg_ParseTuple(args, "y*nnnn", &in, &w, &h, &nw, &nh)) return NULL;
144
+ if (nw <= 0 || nh <= 0 || w <= 0 || h <= 0) {
145
+ PyBuffer_Release(&in);
146
+ PyErr_SetString(PyExc_ValueError, "sizes must be positive");
147
+ return NULL;
148
+ }
149
+ if (in.len < w * h * 4) {
150
+ PyBuffer_Release(&in);
151
+ PyErr_SetString(PyExc_ValueError, "data shorter than width * height * 4");
152
+ return NULL;
153
+ }
154
+ const unsigned char *src = (const unsigned char *)in.buf;
155
+ Py_ssize_t *xl = PyMem_Malloc(sizeof(Py_ssize_t) * (size_t)(2 * nw + 2 * nh));
156
+ i64 *acc = PyMem_Malloc(sizeof(i64) * 4 * (size_t)nw);
157
+ if (!xl || !acc) { PyMem_Free(xl); PyMem_Free(acc); PyBuffer_Release(&in); return PyErr_NoMemory(); }
158
+ Py_ssize_t *xh = xl + nw, *yl = xh + nw, *yh = yl + nh;
159
+ spans(w, nw, xl, xh);
160
+ spans(h, nh, yl, yh);
161
+ PyObject *res = PyBytes_FromStringAndSize(NULL, nw * nh * 4);
162
+ if (!res) { PyMem_Free(xl); PyMem_Free(acc); PyBuffer_Release(&in); return NULL; }
163
+ unsigned char *out = (unsigned char *)PyBytes_AS_STRING(res);
164
+ Py_BEGIN_ALLOW_THREADS
165
+ for (Py_ssize_t oy = 0; oy < nh; oy++) {
166
+ memset(acc, 0, sizeof(i64) * 4 * (size_t)nw);
167
+ for (Py_ssize_t y = yl[oy]; y < yh[oy]; y++) {
168
+ const unsigned char *row = src + y * w * 4;
169
+ for (Py_ssize_t ox = 0; ox < nw; ox++) {
170
+ i64 *s = acc + ox * 4;
171
+ for (Py_ssize_t x = xl[ox]; x < xh[ox]; x++) {
172
+ const unsigned char *p = row + x * 4;
173
+ i64 a = p[3];
174
+ s[0] += p[0] * a; s[1] += p[1] * a; s[2] += p[2] * a; s[3] += a;
175
+ }
176
+ }
177
+ }
178
+ unsigned char *o = out + oy * nw * 4;
179
+ for (Py_ssize_t ox = 0; ox < nw; ox++) {
180
+ i64 *s = acc + ox * 4;
181
+ i64 n = (i64)(yh[oy] - yl[oy]) * (xh[ox] - xl[ox]);
182
+ i64 sa = s[3];
183
+ if (sa) {
184
+ o[ox * 4] = (unsigned char)((s[0] + sa / 2) / sa);
185
+ o[ox * 4 + 1] = (unsigned char)((s[1] + sa / 2) / sa);
186
+ o[ox * 4 + 2] = (unsigned char)((s[2] + sa / 2) / sa);
187
+ } else {
188
+ o[ox * 4] = o[ox * 4 + 1] = o[ox * 4 + 2] = 0;
189
+ }
190
+ o[ox * 4 + 3] = (unsigned char)((sa + n / 2) / n);
191
+ }
192
+ }
193
+ Py_END_ALLOW_THREADS
194
+ PyMem_Free(xl);
195
+ PyMem_Free(acc);
196
+ PyBuffer_Release(&in);
197
+ return res;
198
+ }
199
+
200
+ /* ------------------------------------------------------------------ */
201
+ /* quantize */
202
+
203
+ static int sort_shift;
204
+ static int cmp_keys(const void *a, const void *b)
205
+ {
206
+ int ka = *(const int *)a, kb = *(const int *)b;
207
+ long va = ((long)((ka >> sort_shift) & 31) << 15) | ka;
208
+ long vb = ((long)((kb >> sort_shift) & 31) << 15) | kb;
209
+ return va < vb ? -1 : va > vb;
210
+ }
211
+
212
+ static int box_range(const int *keys, Py_ssize_t n, int shift)
213
+ {
214
+ int lo = (keys[0] >> shift) & 31, hi = lo;
215
+ for (Py_ssize_t i = 1; i < n; i++) {
216
+ int v = (keys[i] >> shift) & 31;
217
+ if (v < lo) lo = v; else if (v > hi) hi = v;
218
+ }
219
+ return hi - lo;
220
+ }
221
+
222
+ static PyObject *
223
+ img_quantize(PyObject *self, PyObject *args)
224
+ {
225
+ Py_buffer in;
226
+ Py_ssize_t w, h;
227
+ int colors, thr;
228
+ if (!PyArg_ParseTuple(args, "y*nnii", &in, &w, &h, &colors, &thr)) return NULL;
229
+ if (colors < 1) colors = 1;
230
+ if (colors > 255) colors = 255;
231
+ Py_ssize_t n = w * h;
232
+ if (w <= 0 || h <= 0 || in.len < n * 4) {
233
+ PyBuffer_Release(&in);
234
+ PyErr_SetString(PyExc_ValueError, "bad image size");
235
+ return NULL;
236
+ }
237
+ const unsigned char *src = (const unsigned char *)in.buf;
238
+ i64 *count = PyMem_Calloc(32768, sizeof(i64));
239
+ i64 *sums = PyMem_Calloc(32768 * 3, sizeof(i64));
240
+ int *pk = PyMem_Malloc(sizeof(int) * (size_t)n);
241
+ int *keys = PyMem_Malloc(sizeof(int) * 32768);
242
+ Py_ssize_t *bstart = PyMem_Malloc(sizeof(Py_ssize_t) * 256);
243
+ Py_ssize_t *blen = PyMem_Malloc(sizeof(Py_ssize_t) * 256);
244
+ i64 *btot = PyMem_Malloc(sizeof(i64) * 256);
245
+ unsigned char *lut = PyMem_Malloc(32768);
246
+ PyObject *pal = NULL, *idx = NULL, *res = NULL;
247
+ if (!count || !sums || !pk || !keys || !bstart || !blen || !btot || !lut) {
248
+ PyErr_NoMemory();
249
+ goto done;
250
+ }
251
+ for (Py_ssize_t p = 0; p < n; p++) {
252
+ const unsigned char *px = src + p * 4;
253
+ if (thr && px[3] < thr) { pk[p] = -1; continue; }
254
+ int key = ((px[0] >> 3) << 10) | ((px[1] >> 3) << 5) | (px[2] >> 3);
255
+ pk[p] = key;
256
+ count[key]++;
257
+ sums[key * 3] += px[0]; sums[key * 3 + 1] += px[1]; sums[key * 3 + 2] += px[2];
258
+ }
259
+ Py_ssize_t used = 0;
260
+ i64 total = 0;
261
+ for (int k = 0; k < 32768; k++) if (count[k]) { keys[used++] = k; total += count[k]; }
262
+ if (!used) {
263
+ pal = PyBytes_FromStringAndSize("\0\0\0", 3);
264
+ idx = PyBytes_FromStringAndSize(NULL, n);
265
+ if (pal && idx) memset(PyBytes_AS_STRING(idx), 255, (size_t)n);
266
+ goto pack;
267
+ }
268
+ int nb = 1;
269
+ bstart[0] = 0; blen[0] = used; btot[0] = total;
270
+ while (nb < colors) {
271
+ int best = -1, best_range = -1;
272
+ i64 best_count = -1;
273
+ for (int bi = 0; bi < nb; bi++) {
274
+ if (blen[bi] < 2) continue;
275
+ const int *bk = keys + bstart[bi];
276
+ int r = box_range(bk, blen[bi], 10), g = box_range(bk, blen[bi], 5), b = box_range(bk, blen[bi], 0);
277
+ int rng = r > g ? r : g; if (b > rng) rng = b;
278
+ if (rng > best_range || (rng == best_range && btot[bi] > best_count)) {
279
+ best = bi; best_range = rng; best_count = btot[bi];
280
+ }
281
+ }
282
+ if (best < 0) break;
283
+ int *bk = keys + bstart[best];
284
+ Py_ssize_t bn = blen[best];
285
+ int r = box_range(bk, bn, 10), g = box_range(bk, bn, 5), b = box_range(bk, bn, 0);
286
+ int mx = r; sort_shift = 10;
287
+ if (g > mx) { mx = g; sort_shift = 5; }
288
+ if (b > mx) { mx = b; sort_shift = 0; }
289
+ qsort(bk, (size_t)bn, sizeof(int), cmp_keys);
290
+ i64 tot = btot[best], acc = 0;
291
+ Py_ssize_t cut = 1;
292
+ for (Py_ssize_t j = 0; j < bn; j++) {
293
+ acc += count[bk[j]];
294
+ if (acc * 2 >= tot) { cut = j + 1; break; }
295
+ }
296
+ if (cut > bn - 1) cut = bn - 1;
297
+ if (cut < 1) cut = 1;
298
+ i64 lt = 0;
299
+ for (Py_ssize_t j = 0; j < cut; j++) lt += count[bk[j]];
300
+ for (int m = nb; m > best + 1; m--) {
301
+ bstart[m] = bstart[m - 1]; blen[m] = blen[m - 1]; btot[m] = btot[m - 1];
302
+ }
303
+ bstart[best + 1] = bstart[best] + cut; blen[best + 1] = bn - cut; btot[best + 1] = tot - lt;
304
+ blen[best] = cut; btot[best] = lt;
305
+ nb++;
306
+ }
307
+ pal = PyBytes_FromStringAndSize(NULL, nb * 3);
308
+ if (!pal) goto done;
309
+ unsigned char *pp = (unsigned char *)PyBytes_AS_STRING(pal);
310
+ for (int bi = 0; bi < nb; bi++) {
311
+ i64 s[3] = {0, 0, 0};
312
+ for (Py_ssize_t j = 0; j < blen[bi]; j++) {
313
+ int k = keys[bstart[bi] + j];
314
+ s[0] += sums[k * 3]; s[1] += sums[k * 3 + 1]; s[2] += sums[k * 3 + 2];
315
+ }
316
+ i64 c = btot[bi];
317
+ for (int ch = 0; ch < 3; ch++) pp[bi * 3 + ch] = (unsigned char)((s[ch] + c / 2) / c);
318
+ }
319
+ for (Py_ssize_t u = 0; u < used; u++) {
320
+ int k = keys[u];
321
+ i64 c = count[k];
322
+ int rr = (int)((sums[k * 3] + c / 2) / c), gg = (int)((sums[k * 3 + 1] + c / 2) / c),
323
+ bb = (int)((sums[k * 3 + 2] + c / 2) / c);
324
+ int best = 0; long dist = 1L << 30;
325
+ for (int pi = 0; pi < nb; pi++) {
326
+ long dr = pp[pi * 3] - rr, dg = pp[pi * 3 + 1] - gg, db = pp[pi * 3 + 2] - bb;
327
+ long d = dr * dr + dg * dg + db * db;
328
+ if (d < dist) { best = pi; dist = d; }
329
+ }
330
+ lut[k] = (unsigned char)best;
331
+ }
332
+ idx = PyBytes_FromStringAndSize(NULL, n);
333
+ if (!idx) goto done;
334
+ {
335
+ unsigned char *ip = (unsigned char *)PyBytes_AS_STRING(idx);
336
+ for (Py_ssize_t p = 0; p < n; p++) ip[p] = pk[p] < 0 ? 255 : lut[pk[p]];
337
+ }
338
+ pack:
339
+ if (pal && idx) res = PyTuple_Pack(2, pal, idx);
340
+ done:
341
+ Py_XDECREF(pal); Py_XDECREF(idx);
342
+ PyMem_Free(count); PyMem_Free(sums); PyMem_Free(pk); PyMem_Free(keys);
343
+ PyMem_Free(bstart); PyMem_Free(blen); PyMem_Free(btot); PyMem_Free(lut);
344
+ PyBuffer_Release(&in);
345
+ return res;
346
+ }
347
+
348
+ /* ------------------------------------------------------------------ */
349
+ /* sixel */
350
+
351
+ static PyObject *
352
+ img_sixel_encode(PyObject *self, PyObject *args)
353
+ {
354
+ Py_buffer ib, pb;
355
+ Py_ssize_t w, h;
356
+ if (!PyArg_ParseTuple(args, "y*nny*", &ib, &w, &h, &pb)) return NULL;
357
+ PyObject *res = NULL;
358
+ Buf b = {0};
359
+ unsigned char *bits = NULL;
360
+ if (w <= 0 || h <= 0 || ib.len < w * h) {
361
+ PyErr_SetString(PyExc_ValueError, "bad image size");
362
+ goto done;
363
+ }
364
+ const unsigned char *idx = (const unsigned char *)ib.buf;
365
+ const unsigned char *pal = (const unsigned char *)pb.buf;
366
+ Py_ssize_t ncolors = pb.len / 3;
367
+ bits = (unsigned char *)malloc((size_t)w);
368
+ if (!bits) { PyErr_NoMemory(); goto done; }
369
+ if (buf_str(&b, "\x1bP0;1;0q\"1;1;") < 0 || buf_int(&b, (long)w) < 0 ||
370
+ buf_str(&b, ";") < 0 || buf_int(&b, (long)h) < 0) goto done;
371
+ for (Py_ssize_t i = 0; i < ncolors; i++) {
372
+ if (buf_str(&b, "#") < 0 || buf_int(&b, (long)i) < 0 || buf_str(&b, ";2;") < 0) goto done;
373
+ for (int c = 0; c < 3; c++) {
374
+ if (c && buf_str(&b, ";") < 0) goto done;
375
+ if (buf_int(&b, (pal[i * 3 + c] * 100 + 127) / 255) < 0) goto done;
376
+ }
377
+ }
378
+ for (Py_ssize_t top = 0; top < h; top += 6) {
379
+ Py_ssize_t band = h - top < 6 ? h - top : 6;
380
+ unsigned char present[256];
381
+ memset(present, 0, sizeof present);
382
+ for (Py_ssize_t y = top; y < top + band; y++)
383
+ for (Py_ssize_t x = 0; x < w; x++) present[idx[y * w + x]] = 1;
384
+ int first = 1;
385
+ for (int color = 0; color < 255; color++) {
386
+ if (!present[color] || color >= ncolors) continue;
387
+ memset(bits, 0, (size_t)w);
388
+ for (Py_ssize_t dy = 0; dy < band; dy++) {
389
+ const unsigned char *row = idx + (top + dy) * w;
390
+ for (Py_ssize_t x = 0; x < w; x++)
391
+ if (row[x] == color) bits[x] |= (unsigned char)(1 << dy);
392
+ }
393
+ Py_ssize_t last = w;
394
+ while (last > 0 && !bits[last - 1]) last--;
395
+ if (!first && buf_str(&b, "$") < 0) goto done;
396
+ first = 0;
397
+ if (buf_str(&b, "#") < 0 || buf_int(&b, color) < 0) goto done;
398
+ Py_ssize_t x = 0;
399
+ while (x < last) {
400
+ unsigned char v = bits[x];
401
+ Py_ssize_t run = 1;
402
+ while (x + run < last && bits[x + run] == v) run++;
403
+ char ch = (char)(63 + v);
404
+ if (run > 3) {
405
+ if (buf_str(&b, "!") < 0 || buf_int(&b, (long)run) < 0 || buf_put(&b, &ch, 1) < 0) goto done;
406
+ } else {
407
+ for (Py_ssize_t k = 0; k < run; k++) if (buf_put(&b, &ch, 1) < 0) goto done;
408
+ }
409
+ x += run;
410
+ }
411
+ }
412
+ if (top + 6 < h && buf_str(&b, "-") < 0) goto done;
413
+ }
414
+ if (buf_str(&b, "\x1b\\") < 0) goto done;
415
+ res = PyUnicode_DecodeASCII(b.p, b.n, NULL);
416
+ done:
417
+ free(b.p);
418
+ free(bits);
419
+ PyBuffer_Release(&ib);
420
+ PyBuffer_Release(&pb);
421
+ return res;
422
+ }
423
+
424
+ /* ------------------------------------------------------------------ */
425
+ /* cells */
426
+
427
+ static const unsigned int QUAD[16] = {0x20, 0x2598, 0x259D, 0x2580, 0x2596, 0x258C, 0x259E, 0x259B,
428
+ 0x2597, 0x259A, 0x2590, 0x259C, 0x2584, 0x2599, 0x259F, 0x2588};
429
+ static const char RAMP[] = " .:-=+*#%@";
430
+ static const int BAYER[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
431
+ static const int BRAILLE_BITS[8] = {0x01, 0x08, 0x02, 0x10, 0x04, 0x20, 0x40, 0x80};
432
+ static const int CUBE[6] = {0, 95, 135, 175, 215, 255};
433
+ static const int ANSI16[16][3] = {{0, 0, 0}, {205, 0, 0}, {0, 205, 0}, {205, 205, 0}, {0, 0, 238},
434
+ {205, 0, 205}, {0, 205, 205}, {229, 229, 229}, {127, 127, 127}, {255, 0, 0}, {0, 255, 0},
435
+ {255, 255, 0}, {92, 92, 255}, {255, 0, 255}, {0, 255, 255}, {255, 255, 255}};
436
+
437
+ enum { M_HALF, M_QUAD, M_SEXT, M_BRAILLE, M_ASCII };
438
+ enum { C_TRUE, C_256, C_16, C_NONE };
439
+
440
+ typedef struct { int set; int r, g, b; } Col;
441
+
442
+ static int lum3(int r, int g, int b) { return (77 * r + 150 * g + 29 * b) >> 8; }
443
+
444
+ static int cube_index(int v) { return v < 48 ? 0 : v < 115 ? 1 : (v - 35) / 40; }
445
+
446
+ static int to256(int r, int g, int b)
447
+ {
448
+ int ri = cube_index(r), gi = cube_index(g), bi = cube_index(b);
449
+ int cr = CUBE[ri], cg = CUBE[gi], cb = CUBE[bi];
450
+ int avg = (r + g + b) / 3;
451
+ int gray = avg > 238 ? 23 : (avg - 3) / 10;
452
+ if (avg < 3) gray = 0;
453
+ if (gray < 0) gray = 0;
454
+ int gv = 8 + 10 * gray;
455
+ long dc = (long)(cr - r) * (cr - r) + (long)(cg - g) * (cg - g) + (long)(cb - b) * (cb - b);
456
+ long dg = (long)(gv - r) * (gv - r) + (long)(gv - g) * (gv - g) + (long)(gv - b) * (gv - b);
457
+ return dg < dc ? 232 + gray : 16 + 36 * ri + 6 * gi + bi;
458
+ }
459
+
460
+ static int to16(int r, int g, int b)
461
+ {
462
+ int best = 0; long dist = 1L << 30;
463
+ for (int i = 0; i < 16; i++) {
464
+ long dr = ANSI16[i][0] - r, dg = ANSI16[i][1] - g, db = ANSI16[i][2] - b;
465
+ long d = dr * dr + dg * dg + db * db;
466
+ if (d < dist) { best = i; dist = d; }
467
+ }
468
+ return best;
469
+ }
470
+
471
+ static int col_eq(Col a, Col b)
472
+ {
473
+ if (a.set != b.set) return 0;
474
+ if (!a.set) return 1;
475
+ return a.r == b.r && a.g == b.g && a.b == b.b;
476
+ }
477
+
478
+ static int put_sgr(Buf *b, Col fg, Col bg, int color)
479
+ {
480
+ if (buf_str(b, "\x1b[") < 0) return -1;
481
+ for (int which = 0; which < 2; which++) {
482
+ Col c = which ? bg : fg;
483
+ int base = which ? 48 : 38;
484
+ if (which && buf_str(b, ";") < 0) return -1;
485
+ if (!c.set) { if (buf_int(b, base + 1) < 0) return -1; continue; }
486
+ if (color == C_TRUE) {
487
+ if (buf_int(b, base) < 0 || buf_str(b, ";2;") < 0 || buf_int(b, c.r) < 0 || buf_str(b, ";") < 0 ||
488
+ buf_int(b, c.g) < 0 || buf_str(b, ";") < 0 || buf_int(b, c.b) < 0) return -1;
489
+ } else if (color == C_256) {
490
+ if (buf_int(b, base) < 0 || buf_str(b, ";5;") < 0 || buf_int(b, to256(c.r, c.g, c.b)) < 0) return -1;
491
+ } else {
492
+ int idx = to16(c.r, c.g, c.b);
493
+ if (buf_int(b, (idx < 8 ? base - 8 : base + 52) + (idx & 7)) < 0) return -1;
494
+ }
495
+ }
496
+ return buf_str(b, "m");
497
+ }
498
+
499
+ static unsigned int sextant_cp(int mask)
500
+ {
501
+ if (mask == 0) return 0x20;
502
+ if (mask == 63) return 0x2588;
503
+ if (mask == 21) return 0x258C;
504
+ if (mask == 42) return 0x2590;
505
+ unsigned int cp = 0x1FB00 + mask - 1;
506
+ if (mask > 21) cp--;
507
+ if (mask > 42) cp--;
508
+ return cp;
509
+ }
510
+
511
+ static Col mean_of(const int (*px)[3], const int *sel, int n)
512
+ {
513
+ Col c = {0, 0, 0, 0};
514
+ int cnt = 0; long s0 = 0, s1 = 0, s2 = 0;
515
+ for (int k = 0; k < n; k++) if (sel[k]) { s0 += px[k][0]; s1 += px[k][1]; s2 += px[k][2]; cnt++; }
516
+ if (!cnt) return c;
517
+ c.set = 1;
518
+ c.r = (int)((s0 + cnt / 2) / cnt); c.g = (int)((s1 + cnt / 2) / cnt); c.b = (int)((s2 + cnt / 2) / cnt);
519
+ return c;
520
+ }
521
+
522
+ static PyObject *
523
+ img_cells(PyObject *self, PyObject *args)
524
+ {
525
+ Py_buffer in;
526
+ Py_ssize_t w, h;
527
+ const char *mode_s, *color_s;
528
+ int thr;
529
+ if (!PyArg_ParseTuple(args, "y*nnssi", &in, &w, &h, &mode_s, &color_s, &thr)) return NULL;
530
+ int mode, pw, ph, color;
531
+ if (!strcmp(mode_s, "half")) { mode = M_HALF; pw = 1; ph = 2; }
532
+ else if (!strcmp(mode_s, "quadrant")) { mode = M_QUAD; pw = 2; ph = 2; }
533
+ else if (!strcmp(mode_s, "sextant")) { mode = M_SEXT; pw = 2; ph = 3; }
534
+ else if (!strcmp(mode_s, "braille")) { mode = M_BRAILLE; pw = 2; ph = 4; }
535
+ else if (!strcmp(mode_s, "ascii")) { mode = M_ASCII; pw = 1; ph = 2; }
536
+ else { PyBuffer_Release(&in); return PyErr_Format(PyExc_ValueError, "unknown mode '%s'", mode_s); }
537
+ if (!strcmp(color_s, "truecolor")) color = C_TRUE;
538
+ else if (!strcmp(color_s, "256")) color = C_256;
539
+ else if (!strcmp(color_s, "16")) color = C_16;
540
+ else if (!strcmp(color_s, "none")) color = C_NONE;
541
+ else { PyBuffer_Release(&in); return PyErr_Format(PyExc_ValueError, "unknown colour level '%s'", color_s); }
542
+ if (w < 0 || h < 0 || in.len < w * h * 4) {
543
+ PyBuffer_Release(&in);
544
+ PyErr_SetString(PyExc_ValueError, "data shorter than width * height * 4");
545
+ return NULL;
546
+ }
547
+ const unsigned char *src = (const unsigned char *)in.buf;
548
+ int mono = color == C_NONE;
549
+ Py_ssize_t cols = w / pw, rows = h / ph;
550
+ PyObject *list = PyList_New(rows);
551
+ if (!list) { PyBuffer_Release(&in); return NULL; }
552
+ Buf b = {0};
553
+ int npx = pw * ph;
554
+ for (Py_ssize_t row = 0; row < rows; row++) {
555
+ b.n = 0;
556
+ Col cur_fg = {0}, cur_bg = {0};
557
+ for (Py_ssize_t col = 0; col < cols; col++) {
558
+ int px[8][3], solid[8], lums[8];
559
+ int nsolid = 0;
560
+ for (int dy = 0; dy < ph; dy++) {
561
+ const unsigned char *p = src + ((row * ph + dy) * w + col * pw) * 4;
562
+ for (int dx = 0; dx < pw; dx++, p += 4) {
563
+ int k = dy * pw + dx;
564
+ solid[k] = p[3] >= thr;
565
+ px[k][0] = p[0]; px[k][1] = p[1]; px[k][2] = p[2];
566
+ lums[k] = lum3(p[0], p[1], p[2]);
567
+ nsolid += solid[k];
568
+ }
569
+ }
570
+ unsigned int ch = 0x20;
571
+ Col fg = {0}, bg = {0};
572
+ if (mode == M_HALF) {
573
+ if (!solid[0] && !solid[1]) ch = 0x20;
574
+ else if (!solid[1]) { ch = 0x2580; fg.set = 1; fg.r = px[0][0]; fg.g = px[0][1]; fg.b = px[0][2]; }
575
+ else if (!solid[0]) { ch = 0x2584; fg.set = 1; fg.r = px[1][0]; fg.g = px[1][1]; fg.b = px[1][2]; }
576
+ else if (mono) {
577
+ static const unsigned int T[4] = {0x20, 0x2584, 0x2580, 0x2588};
578
+ ch = T[(lums[0] >= 128) * 2 + (lums[1] >= 128)];
579
+ } else if (px[0][0] == px[1][0] && px[0][1] == px[1][1] && px[0][2] == px[1][2]) {
580
+ ch = 0x2588; fg.set = 1; fg.r = px[0][0]; fg.g = px[0][1]; fg.b = px[0][2];
581
+ } else {
582
+ ch = 0x2580;
583
+ fg.set = 1; fg.r = px[0][0]; fg.g = px[0][1]; fg.b = px[0][2];
584
+ bg.set = 1; bg.r = px[1][0]; bg.g = px[1][1]; bg.b = px[1][2];
585
+ }
586
+ } else if (mode == M_ASCII) {
587
+ if (nsolid) {
588
+ fg = mean_of((const int (*)[3])px, solid, npx);
589
+ ch = (unsigned char)RAMP[lum3(fg.r, fg.g, fg.b) * 10 / 256];
590
+ if (ch == 0x20 || mono) fg.set = 0;
591
+ }
592
+ } else {
593
+ int on[8] = {0};
594
+ if (nsolid) {
595
+ if (mono) {
596
+ Py_ssize_t gx = col * pw, gy = row * ph;
597
+ for (int k = 0; k < npx; k++)
598
+ if (solid[k] && lums[k] * 16 >
599
+ BAYER[((gy + k / pw) & 3) * 4 + ((gx + k % pw) & 3)] * 256 + 128)
600
+ on[k] = 1;
601
+ } else {
602
+ if (nsolid < npx) {
603
+ for (int k = 0; k < npx; k++) on[k] = solid[k];
604
+ } else if (mode == M_BRAILLE) {
605
+ int s = 0;
606
+ for (int k = 0; k < npx; k++) s += lums[k];
607
+ int mean = s / npx;
608
+ for (int k = 0; k < npx; k++) on[k] = lums[k] >= mean;
609
+ } else {
610
+ int s = 0, any = 0;
611
+ for (int k = 0; k < npx; k++) s += lums[k];
612
+ int mean = (s + npx / 2) / npx;
613
+ for (int k = 0; k < npx; k++) { on[k] = lums[k] > mean; any |= on[k]; }
614
+ if (!any) for (int k = 0; k < npx; k++) on[k] = 1;
615
+ }
616
+ int offs[8];
617
+ for (int k = 0; k < npx; k++) offs[k] = !on[k] && solid[k];
618
+ fg = mean_of((const int (*)[3])px, on, npx);
619
+ if (mode != M_BRAILLE) bg = mean_of((const int (*)[3])px, offs, npx);
620
+ }
621
+ }
622
+ int mask = 0;
623
+ for (int k = 0; k < npx; k++) if (on[k]) mask |= 1 << k;
624
+ if (mode == M_BRAILLE) {
625
+ int bits = 0;
626
+ for (int k = 0; k < 8; k++) if (mask & (1 << k)) bits |= BRAILLE_BITS[k];
627
+ ch = bits ? 0x2800 + (unsigned int)bits : 0x20;
628
+ } else if (mode == M_QUAD) ch = QUAD[mask];
629
+ else ch = sextant_cp(mask);
630
+ if (ch == 0x20) fg.set = 0;
631
+ else if (ch == 0x2588) bg.set = 0;
632
+ }
633
+ if (!mono && !(col_eq(fg, cur_fg) && col_eq(bg, cur_bg))) {
634
+ int r = (!fg.set && !bg.set) ? buf_str(&b, "\x1b[0m") : put_sgr(&b, fg, bg, color);
635
+ if (r < 0) goto fail;
636
+ cur_fg = fg; cur_bg = bg;
637
+ }
638
+ if (buf_cp(&b, ch) < 0) goto fail;
639
+ }
640
+ if ((cur_fg.set || cur_bg.set) && buf_str(&b, "\x1b[0m") < 0) goto fail;
641
+ PyObject *s = PyUnicode_DecodeUTF8(b.p ? b.p : "", b.n, NULL);
642
+ if (!s) goto fail;
643
+ PyList_SET_ITEM(list, row, s);
644
+ }
645
+ free(b.p);
646
+ PyBuffer_Release(&in);
647
+ return list;
648
+ fail:
649
+ free(b.p);
650
+ Py_DECREF(list);
651
+ PyBuffer_Release(&in);
652
+ return NULL;
653
+ }
654
+
655
+ static PyMethodDef methods[] = {
656
+ {"unfilter", img_unfilter, METH_VARARGS, "Undo PNG scanline filters."},
657
+ {"resize", img_resize, METH_VARARGS, "Area-average RGBA resampling."},
658
+ {"quantize", img_quantize, METH_VARARGS, "Median-cut palette and indices."},
659
+ {"sixel_encode", img_sixel_encode, METH_VARARGS, "DCS sixel sequence."},
660
+ {"cells", img_cells, METH_VARARGS, "Unicode block, braille or ASCII cells."},
661
+ {NULL, NULL, 0, NULL}
662
+ };
663
+
664
+ static struct PyModuleDef module = {
665
+ PyModuleDef_HEAD_INIT, "_images", "C image primitives for flexablelib.", -1, methods
666
+ };
667
+
668
+ PyMODINIT_FUNC PyInit__images(void) { return PyModule_Create(&module); }
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flexablelib-speedups
3
- Version: 3.3.1
4
- Summary: Optional C acceleration of terminal frame drawing for flexablelib (cut, pad, borders, frame diffs).
3
+ Version: 3.5.0
4
+ Summary: Optional C acceleration of terminal frame drawing for flexablelib (cut, pad, borders, frame diffs, images).
5
5
  Home-page: https://github.com/Opus-Perpetuus/flexablelib
6
6
  Author: Benjamin Ramirez
7
7
  Author-email: chilerito12@gmail.com
@@ -3,7 +3,9 @@ MANIFEST.in
3
3
  README.md
4
4
  setup.py
5
5
  flexablelib_speedups/__init__.py
6
+ flexablelib_speedups/_ascii_table.h
6
7
  flexablelib_speedups/_frames.c
8
+ flexablelib_speedups/_images.c
7
9
  flexablelib_speedups/_width_tables.h
8
10
  flexablelib_speedups.egg-info/PKG-INFO
9
11
  flexablelib_speedups.egg-info/SOURCES.txt
@@ -20,7 +20,7 @@ setup(
20
20
  name='flexablelib-speedups',
21
21
  version=version(),
22
22
  description='Optional C acceleration of terminal frame drawing for '
23
- 'flexablelib (cut, pad, borders, frame diffs).',
23
+ 'flexablelib (cut, pad, borders, frame diffs, images).',
24
24
  long_description=readme(),
25
25
  long_description_content_type='text/markdown',
26
26
  author='Benjamin Ramirez',
@@ -30,9 +30,13 @@ setup(
30
30
  packages=['flexablelib_speedups'],
31
31
  ext_modules=[Extension('flexablelib_speedups._frames',
32
32
  sources=['flexablelib_speedups/_frames.c'],
33
- depends=['flexablelib_speedups/_width_tables.h'],
33
+ depends=['flexablelib_speedups/_width_tables.h',
34
+ 'flexablelib_speedups/_ascii_table.h'],
34
35
  # No compiler: the install still succeeds and
35
36
  # flexablelib keeps its pure-Python frame code.
37
+ optional=True),
38
+ Extension('flexablelib_speedups._images',
39
+ sources=['flexablelib_speedups/_images.c'],
36
40
  optional=True)],
37
41
  python_requires='>=3.8',
38
42
  classifiers=[