mcc2 1.5.0

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 (120) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.autotest +23 -0
  4. data/.gitignore +15 -0
  5. data/.rubocop.yml +3 -0
  6. data/Gemfile +13 -0
  7. data/History.txt +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +51 -0
  10. data/Rakefile +12 -0
  11. data/config/i18n-tasks.yml +193 -0
  12. data/exe/mcc2 +18 -0
  13. data/exe/mcc2-generate-preview +17 -0
  14. data/exe/mcc2print +6 -0
  15. data/lib/libguib16.rb +1620 -0
  16. data/lib/mcc2/advanced_data_edit_dlg.rb +139 -0
  17. data/lib/mcc2/cc_dialogbox.rb +52 -0
  18. data/lib/mcc2/constants.rb +154 -0
  19. data/lib/mcc2/dataman/base.rb +61 -0
  20. data/lib/mcc2/dataman/csv.rb +49 -0
  21. data/lib/mcc2/dataman/database.rb +8 -0
  22. data/lib/mcc2/dataman/dbf.rb +45 -0
  23. data/lib/mcc2/dataman/none.rb +25 -0
  24. data/lib/mcc2/dataview.rb +348 -0
  25. data/lib/mcc2/dataview_dlg.rb +232 -0
  26. data/lib/mcc2/debug_tools.rb +20 -0
  27. data/lib/mcc2/fx_colored_item.rb +101 -0
  28. data/lib/mcc2/generate_preview.rb +68 -0
  29. data/lib/mcc2/gui/advanced_data_edit_dlg.rbin +0 -0
  30. data/lib/mcc2/gui/dataview_dlg.pp +522 -0
  31. data/lib/mcc2/gui/layoutview_dlg.pp +5115 -0
  32. data/lib/mcc2/gui/mcc2_mainwin.pp +838 -0
  33. data/lib/mcc2/gui/picture_data_edit_dlg.rbin +0 -0
  34. data/lib/mcc2/gui/printerview_dlg.pp +3311 -0
  35. data/lib/mcc2/images/Image-Arrow-Left.gif +0 -0
  36. data/lib/mcc2/images/Image-Arrow-Right.gif +0 -0
  37. data/lib/mcc2/images/Image-FastArrowLeft.gif +0 -0
  38. data/lib/mcc2/images/Image-FastArrowRight.bmp +0 -0
  39. data/lib/mcc2/images/Image-FastArrowRight.gif +0 -0
  40. data/lib/mcc2/images/application-exit.png +0 -0
  41. data/lib/mcc2/images/copy.png +0 -0
  42. data/lib/mcc2/images/country_de.png +0 -0
  43. data/lib/mcc2/images/country_gb.png +0 -0
  44. data/lib/mcc2/images/country_rs.png +0 -0
  45. data/lib/mcc2/images/cut.png +0 -0
  46. data/lib/mcc2/images/distribute-horizontal-baseline.png +0 -0
  47. data/lib/mcc2/images/distribute-vertical-baseline.png +0 -0
  48. data/lib/mcc2/images/document-close.png +0 -0
  49. data/lib/mcc2/images/document-edit.png +0 -0
  50. data/lib/mcc2/images/document-new.png +0 -0
  51. data/lib/mcc2/images/document-open.png +0 -0
  52. data/lib/mcc2/images/document-print-frame.png +0 -0
  53. data/lib/mcc2/images/document-print.png +0 -0
  54. data/lib/mcc2/images/document-save-as.png +0 -0
  55. data/lib/mcc2/images/document-save.png +0 -0
  56. data/lib/mcc2/images/documentation.png +0 -0
  57. data/lib/mcc2/images/draw-rectangle.png +0 -0
  58. data/lib/mcc2/images/dummy.gif +0 -0
  59. data/lib/mcc2/images/exchange-positions.png +0 -0
  60. data/lib/mcc2/images/insert-image.png +0 -0
  61. data/lib/mcc2/images/insert-text.png +0 -0
  62. data/lib/mcc2/images/libreoffice-oasis-master-document.png +0 -0
  63. data/lib/mcc2/images/measure.png +0 -0
  64. data/lib/mcc2/images/messagebox_warning.png +0 -0
  65. data/lib/mcc2/images/paste.png +0 -0
  66. data/lib/mcc2/images/process-stop.png +0 -0
  67. data/lib/mcc2/images/zoom-in.png +0 -0
  68. data/lib/mcc2/layout.rb +323 -0
  69. data/lib/mcc2/layout_file.rb +794 -0
  70. data/lib/mcc2/layout_object/barcode.rb +33 -0
  71. data/lib/mcc2/layout_object/box.rb +18 -0
  72. data/lib/mcc2/layout_object/chip.rb +54 -0
  73. data/lib/mcc2/layout_object/embed.rb +16 -0
  74. data/lib/mcc2/layout_object/go_attribut.rb +16 -0
  75. data/lib/mcc2/layout_object/graphic.rb +59 -0
  76. data/lib/mcc2/layout_object/magstripe.rb +13 -0
  77. data/lib/mcc2/layout_object/picture.rb +22 -0
  78. data/lib/mcc2/layout_object/text.rb +29 -0
  79. data/lib/mcc2/layout_win.rb +2052 -0
  80. data/lib/mcc2/layoutview.rb +845 -0
  81. data/lib/mcc2/layoutview_dlg.rb +2058 -0
  82. data/lib/mcc2/locales.rb +27 -0
  83. data/lib/mcc2/mcc2_mainwin.rb +453 -0
  84. data/lib/mcc2/mcc2gui.rb +295 -0
  85. data/lib/mcc2/mcc2print.rb +54 -0
  86. data/lib/mcc2/picture_data_edit_dlg.rb +139 -0
  87. data/lib/mcc2/print_dlg.rb +520 -0
  88. data/lib/mcc2/printer/base.rb +148 -0
  89. data/lib/mcc2/printer/card_printer.rb +45 -0
  90. data/lib/mcc2/printer/file_printer.rb +135 -0
  91. data/lib/mcc2/printer/gdi_card_printer.rb +14 -0
  92. data/lib/mcc2/printer/gdi_printer.rb +197 -0
  93. data/lib/mcc2/printer/linux_cups_printer.rb +92 -0
  94. data/lib/mcc2/printer/preview.rb +277 -0
  95. data/lib/mcc2/printer/printer_cab.rb +118 -0
  96. data/lib/mcc2/printer/printer_cx320_direct.rb +286 -0
  97. data/lib/mcc2/printer/printer_epl.rb +118 -0
  98. data/lib/mcc2/printer/printer_ttp34x.rb +124 -0
  99. data/lib/mcc2/printer/printer_zebra.rb +324 -0
  100. data/lib/mcc2/printer/svg_drawer.rb +689 -0
  101. data/lib/mcc2/printerview.rb +604 -0
  102. data/lib/mcc2/printerview_dlg.rb +1248 -0
  103. data/lib/mcc2/version.rb +3 -0
  104. data/lib/mcc2.rb +11 -0
  105. data/locales/de.yml +6 -0
  106. data/locales/en.yml +502 -0
  107. data/locales/rs.yml +502 -0
  108. data/mcc2.gemspec +41 -0
  109. data/test/dummy.rb +19 -0
  110. data/test/layouts/bart_simpson.png +0 -0
  111. data/test/layouts/color_test.xml +151 -0
  112. data/test/layouts/ms.xml +67 -0
  113. data/test/ruby_print.rb +380 -0
  114. data/test/test.csv +4 -0
  115. data/test/test.dbf +0 -0
  116. data/test/test.str +4 -0
  117. data/test/test_mcc2.rb +8 -0
  118. data.tar.gz.sig +6 -0
  119. metadata +421 -0
  120. metadata.gz.sig +0 -0
@@ -0,0 +1,689 @@
1
+ require "mcc2/dataman/base"
2
+ require "i18n"
3
+ require "ttfunk"
4
+ require "pango"
5
+
6
+ module Mcc2
7
+ module SvgDrawer
8
+ class ObjRectangle
9
+ def initialize(v1, v2, v3, v4, obj_id)
10
+ @v1 = v1
11
+ @v2 = v2
12
+ @v3 = v3
13
+ @v4 = v4
14
+ @obj_id = obj_id
15
+ end
16
+
17
+ private def sign(p1, p2, p3)
18
+ (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
19
+ end
20
+
21
+ def point_inside(pt)
22
+ d1 = sign(pt, @v1, @v2)
23
+ d2 = sign(pt, @v2, @v3)
24
+ d3 = sign(pt, @v3, @v4)
25
+ d4 = sign(pt, @v4, @v1)
26
+
27
+ has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0) || (d4 < 0)
28
+ has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0) || (d4 > 0)
29
+
30
+ !(has_neg && has_pos)
31
+ end
32
+
33
+ attr_reader :obj_id
34
+ end
35
+
36
+ class ObjRectangeList
37
+ def initialize
38
+ @objs = []
39
+ end
40
+
41
+ def add(rect)
42
+ if rect.is_a?(self.class)
43
+ @objs += rect.objs
44
+ else
45
+ @objs << rect
46
+ end
47
+ end
48
+
49
+ def point_inside(pt)
50
+ @objs.select do |obj|
51
+ obj.point_inside(pt)
52
+ end
53
+ end
54
+
55
+ attr_reader :objs
56
+ end
57
+
58
+ def width_with_margins(width)
59
+ width + card_margin_left + card_margin_right
60
+ end
61
+
62
+ def height_with_margins(height)
63
+ height + card_margin_bottom + card_margin_top
64
+ end
65
+
66
+ FontInfo = Data.define :font_family, :font_weight, :font_style, :font_stretch
67
+
68
+ private def fontinfo_from_file(font_path)
69
+ ttf = TTFunk::File.open(font_path)
70
+
71
+ # Extract names from the 'name' table
72
+ # ttfunk groups strings by name_id. We fetch the English (US) strings primarily.
73
+ names = ttf.name
74
+
75
+ # Helper to get the English string for a specific Name ID
76
+ get_name = lambda do |instances|
77
+ return nil if instances.empty?
78
+
79
+ # Try to find English (Language ID 1033 or 0)
80
+ raw_string = instances.find { |s| s.language_id == 1033 || s.language_id == 0 } || instances.first
81
+ # Determine Ruby-compatible encoding based on TTF Platform Specs
82
+ ruby_encoding = case raw_string.platform_id
83
+ when 0, 3 # Unicode (0) or Microsoft (3) use UTF-16BE/UCS-2
84
+ 'UTF-16BE'
85
+ when 1 # Macintosh (1) typically uses MacRoman
86
+ 'MacRoman'
87
+ else
88
+ 'ASCII-8BIT' # Fallback for raw byte manipulation
89
+ end
90
+ utf8_string = raw_string.encode('UTF-8', ruby_encoding, invalid: :replace, undef: :replace)
91
+ # Strip null bytes that occasionally bleed through from malformed sub-tables
92
+ utf8_string.gsub("\u0000", '').strip
93
+ end
94
+
95
+ # Fetch the specific Name IDs
96
+ font_family_id1 = get_name.call(names.font_family) # Font Family Name (Legacy)
97
+ font_subfamily_id2 = get_name.call(names.font_subfamily) # Font Subfamily Name (Legacy)
98
+ pref_family_id16 = get_name.call(names.preferred_family) # Typographic/Preferred Family
99
+ pref_subfamily_id17 = get_name.call(names.preferred_subfamily) # Typographic/Preferred Subfamily
100
+
101
+ # 1. Map font-family (Prefer ID 16, fallback to ID 1)
102
+ css_family = pref_family_id16 || font_family_id1
103
+
104
+ # 2. Map font-weight (Read directly from the numeric OS/2 table usWeightClass)
105
+ # Fallback to checking the Subfamily strings if OS/2 table is missing
106
+ css_weight = 400
107
+ if ttf.os2
108
+ css_weight = ttf.os2.weight_class
109
+ css_width = {
110
+ 1 => 'ultra-condensed',
111
+ 2 => 'extra-condensed',
112
+ 3 => 'condensed',
113
+ 4 => 'semi-condensed',
114
+ 5 => 'normal',
115
+ 6 => 'semi-expanded',
116
+ 7 => 'expanded',
117
+ 8 => 'extra-expanded',
118
+ 9 => 'ultra-expanded',
119
+ }[ttf.os2.width_class]
120
+ else
121
+ subfamily_str = (pref_subfamily_id17 || font_subfamily_id2 || '').downcase
122
+ css_weight = 700 if subfamily_str.include?('bold')
123
+ css_weight = 300 if subfamily_str.include?('light')
124
+ end
125
+
126
+ # 3. Map font-style (Check head table macStyle or parse Subfamily strings)
127
+ if ttf.header.mac_style
128
+ is_italic = (ttf.header.mac_style & 2) != 0 # Bit 1 represents Italic
129
+ else
130
+ subfamily_str = (pref_subfamily_id17 || font_subfamily_id2 || '').downcase
131
+ is_italic = subfamily_str.include?('italic') || subfamily_str.include?('oblique')
132
+ end
133
+ css_style = is_italic ? 'italic' : 'normal'
134
+
135
+ FontInfo.new(css_family, css_weight, css_style, css_width || '')
136
+ end
137
+
138
+ @@builtin_fonts = nil
139
+ def self.builtin_font_names
140
+ @@builtin_fonts ||= begin
141
+ fm = PangoCairo::FontMap.default
142
+ fm.families.flat_map do |f|
143
+ f.faces.map do |ff|
144
+ [ff.describe.to_s, FontInfo.new(f.name, ff.describe.weight.to_i, ff.describe.style.nick, ff.describe.stretch.nick)]
145
+ end
146
+ end.to_h.freeze
147
+ end
148
+ end
149
+
150
+ @@font_map = SvgDrawer.builtin_font_names.dup
151
+ @@pango_font_map = Pango::CairoFontMap.default
152
+
153
+ def add_fonts_from_layout(layout_file)
154
+ layout_file.sorted_id_list.each do |id|
155
+ go = layout_file.find_graphic_object_by_id(id)
156
+ font = go[GOA_FONT_TYP]
157
+ if font == "from_file"
158
+ font_path = go.layout_file.expand_path(go[GOA_FONT_FILE])
159
+
160
+ @@font_map[font_path] ||= begin
161
+ @@pango_font_map.add_font_file(font_path)
162
+ fontinfo_from_file(font_path)
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ def font_map
169
+ @@font_map
170
+ end
171
+ def pango_font_map
172
+ @@pango_font_map
173
+ end
174
+
175
+ def flip_flop_mirrors(svg, tra, layout_file, preview, page)
176
+ flip = true if page % 2 == 0 && layout_file.get_attr_value(S_FRONT_FLIP) == S_TRUE
177
+ flop = true if page % 2 == 0 && layout_file.get_attr_value(S_FRONT_FLOP) == S_TRUE
178
+ flip = true if page % 2 != 0 && layout_file.get_attr_value(S_BACK_FLIP) == S_TRUE
179
+ flop = true if page % 2 != 0 && layout_file.get_attr_value(S_BACK_FLOP) == S_TRUE
180
+
181
+ return yield if (!flip && !flop) || preview
182
+
183
+ width = layout_file.get_attr_value(S_WIDTH).to_f
184
+ height = layout_file.get_attr_value(S_HEIGHT).to_f
185
+ image_width = mm(width_with_margins(width)).round-1
186
+ image_height = mm(height_with_margins(height)).round-1
187
+
188
+ svg.g(transform: "translate(#{tra.x}, #{tra.y}) scale(#{flip ? -1.0 : 1.0} #{flop ? -1.0 : 1.0}) translate(#{flip ? -image_width : 0} #{flop ? -image_height : 0}) translate(#{-tra.x}, #{-tra.y})") do
189
+ return yield
190
+ end
191
+ end
192
+
193
+ def draw(drawing, transform, layout_file, preview, selected_object_ids, row, page_nr, layers: [0])
194
+ add_fonts_from_layout(layout_file)
195
+
196
+ flip_flop_mirrors(drawing, transform, layout_file, preview, page_nr) do
197
+ barcode_dpi = layout_file.get_attr_value(S_BARCODE_DPI).to_f
198
+ rect_list = ObjRectangeList.new
199
+
200
+ width = layout_file.get_attr_value(S_WIDTH).to_f
201
+ height = layout_file.get_attr_value(S_HEIGHT).to_f
202
+ if preview
203
+ # todo -0.5, damit die rechten und unteren Ränder zu sehen sind, jetzt sitzt die Karte aber unsymetrisch auf dem Rechteck
204
+ # drawing.fill("none")
205
+ # drawing.stroke("black")
206
+ drawing.rect(x: transform.x, y: transform.y, width: mm(width_with_margins(width)).round-1, height: mm(height_with_margins(height)).round-1, fill: "white", stroke: "black", "stroke-width": 1)
207
+ # drawing.rectangle(transform.x, transform.y, cpos.x, cpos.y)
208
+ # canvas.rect(mm(width_with_margins(width - 0.5)), mm(height_with_margins(height - 0.5))).styles(fill: "none", stroke: "black")
209
+ end
210
+
211
+ tra = transform.translate(mm(card_margin_left).round, mm(card_margin_top).round)
212
+ # Fill card background for preview
213
+ rx = mm(3).round
214
+ if preview
215
+ # drawing.fill("lightgray")
216
+ # drawing.stroke("none")
217
+ drawing.rect(x: tra.x, y: tra.y, width: mm(width).round, height: mm(height).round, fill: "lightgray", rx: , stroke: "blue", "stroke-width": 1)
218
+ # drawing = drawing.draw_rect([200,200,200], tra.x, tra.y, cpos.x-tra.x, cpos.y-tra.y, fill: true)
219
+ # drawing.roundrectangle(tra.x, tra.y, cpos.x, cpos.y, rx, rx)
220
+ # canvas1.rect(mm(width), mm(height)).round(rx).styles(fill: "lightgray", stroke: "none")
221
+ end
222
+ # die graphischen Elemnte zeichnen
223
+
224
+ # Text-Objekte
225
+ sorted_id_list = layout_file.sorted_id_list
226
+ sorted_id_list.each do |id|
227
+ go = layout_file.find_graphic_object_by_id(id)
228
+ # nur die Objekte zeichnen, die auf diese Seite sollen
229
+ if go[GOA_PAGE_NR].to_i == page_nr && (layers == :all || layers.include?(go[GOA_LAYER].to_i))
230
+
231
+ # die allgemeinen Attribute ermitteln
232
+ x = go[GOA_X].to_f
233
+ y = go[GOA_Y].to_f
234
+ go_width = go[GOA_WIDTH].to_f
235
+ go_height = go[GOA_HEIGHT].to_f
236
+ rotation = go[GOA_ROTATION].to_f
237
+ typ = go[GOA_TYP]
238
+
239
+ if !go[GOA_DATA]
240
+ data = nil
241
+ elsif row && (row.keys - DataMan.meta_headers).any?
242
+ data = DataMan.replace_variables(go[GOA_DATA], row)
243
+ elsif go[GOA_DATA]
244
+ data_nosub = go[GOA_DATA].to_s
245
+ data = go[GOA_SAMPLE_DATA].to_s
246
+ end
247
+
248
+ if tra
249
+ rec = tra.translate(mm(x).round, mm(y).round).rotate(rotation)
250
+ rec2 = rec.translate(mm(go_width), 0)
251
+ rec3 = rec.translate(mm(go_width), mm(go_height))
252
+ rec4 = rec.translate(0, mm(go_height))
253
+ orec = ObjRectangle.new(rec.vector2d, rec2.vector2d, rec3.vector2d, rec4.vector2d, id)
254
+ rect_list.add(orec)
255
+ end
256
+
257
+ drawing.g transform: "rotate(#{rec.a})" do
258
+ opos = rec.inverse
259
+
260
+ begin
261
+ # die Objekt-spezifischen Attribute ermitteln
262
+ if typ == TEXTOBJECT
263
+ draw_textobject(drawing, opos, go, go_width, go_height, data.empty? ? data_nosub : data)
264
+ elsif typ == PICTUREOBJECT
265
+ draw_pictureobject(drawing, opos, go, go_width, go_height, data)
266
+ elsif typ == BARCODEOBJECT
267
+ data = data.gsub(/\\x([0-9a-f][0-9a-f])/i) { [$1.hex].pack("C") }
268
+ draw_barcode(drawing, opos, go, go_width, go_height, data, barcode_dpi)
269
+ elsif typ == BOXOBJECT
270
+ draw_boxobject(drawing, opos, go, go_width, go_height)
271
+ elsif typ == EMBEDOBJECT
272
+ draw_embedobject(drawing, opos, go, go_width, go_height, data, row ? row["row_number"] : "1", barcode_dpi)
273
+ end
274
+ rescue => err
275
+ raise err, "#{go[GOA_NAME]}: #{err}"
276
+ end
277
+
278
+ # in Vorschau einen Rahmen um das selektierte Objekt zeichnen
279
+ if preview && selected_object_ids.include?(id)
280
+ # drawing.fill("none")
281
+ # drawing.stroke("red")
282
+ # drawing.stroke_width(1)
283
+ # cpos = opos.translate(mm(go_width), mm(go_height))
284
+ # drawing.rectangle(opos.x, opos.y, cpos.x, cpos.y)
285
+ drawing.rect(x: opos.x, y: opos.y, width: [mm(go_width), 0.5].max, height: [mm(go_height), 0.5].max, fill: "none", stroke: "red", "stroke-width": 1)
286
+ # body.rect(mm(go_width), mm(go_height)).styles(fill: "none", stroke: "red")
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ if preview
293
+ # paint magstripe for preview
294
+ if page_nr == 1 && layout_file.get_attr_value(S_MAGSTRIPE) == S_TRUE
295
+ rs_rect = tra.translate(mm(0), mm(4))
296
+ drawing.rect(x: rs_rect.x, y: rs_rect.y, width: mm(width).round, height: mm(12.5).round, fill: "black", stroke: "none", opacity: 0.5)
297
+
298
+ ms_obj = layout_file.magstripe_object
299
+ (1..3).each do |spurnr|
300
+ spur_data = ms_obj["ms_spur" + spurnr.to_s]
301
+ spurpos = rs_rect.translate(mm(3), mm((3.5 * spurnr) + 0.5))
302
+ drawing.text_("Mag #{spurnr}: #{spur_data}",
303
+ x: spurpos.x, y: spurpos.y,
304
+ "font-family": "Courier-New Bold Mono",
305
+ "font-size": pt(10),
306
+ fill: "white")
307
+ end
308
+ end
309
+ # paint contactless chip for preview
310
+ if page_nr == 0 && layout_file.get_attr_value(S_CONTACTLESS) == S_TRUE
311
+ clpa = tra.translate(mm(3), mm(3))
312
+ drawing.rect(x: clpa.x, y: clpa.y, width: mm(5), height: mm(5), rx: mm(0.5), fill: "gray", stroke: "none", opacity: 0.5)
313
+ clpa = tra.translate(mm(4), mm(4))
314
+ drawing.rect(x: clpa.x, y: clpa.y, width: mm(75), height: mm(45), rx: mm(3), fill: "none", stroke: "gray", stroke_opacity: 0.5)
315
+
316
+ clpa = tra.translate(mm(5), mm(5))
317
+ drawing.rect(x: clpa.x, y: clpa.y, width: mm(73), height: mm(43), rx: mm(3), fill: "none", stroke: "gray", stroke_opacity: 0.5)
318
+
319
+ clpa = tra.translate(mm(6), mm(6))
320
+ drawing.rect(x: clpa.x, y: clpa.y, width: mm(71), height: mm(41), rx: mm(3), fill: "none", stroke: "gray", stroke_opacity: 0.5)
321
+ end
322
+ # paint contact chip for preview
323
+ if page_nr == 0 && layout_file.get_attr_value(S_CONTACT) == S_TRUE
324
+ clpa = tra.translate(mm(8), mm(18))
325
+ drawing.rect(x: clpa.x, y: clpa.y, width: mm(13), height: mm(12), rx: mm(2), fill: "yellow", stroke: "gray")
326
+ end
327
+ end
328
+
329
+ rect_list
330
+ end
331
+ end
332
+
333
+ def draw_textobject(drawing, tra, go, go_width, go_height, data)
334
+ font = go[GOA_FONT_TYP]
335
+ if font == "from_file"
336
+ font = go.layout_file.expand_path(go[GOA_FONT_FILE])
337
+ end
338
+ fontsize = go[GOA_FONT_HEIGHT].to_f
339
+ font_color = ["#", go[GOA_FONT_COLOR]].join
340
+ just = go[GOA_JUSTIFICATION]
341
+ if just == S_JUST_RIGHT
342
+ pjust = Pango::Alignment::RIGHT
343
+ elsif just == S_JUST_MID
344
+ pjust = Pango::Alignment::CENTER
345
+ else
346
+ pjust = Pango::Alignment::LEFT
347
+ end
348
+ just_block = go[GOA_JUSTIFICATION_BLOCK] == "true"
349
+
350
+ if data && !data.empty?
351
+ font_info = get_font_info(font, go)
352
+ drawing.text_(fill: font_color,
353
+ 'text-anchor': just,
354
+ 'font-size': pt(fontsize),
355
+ 'font-family': font_info.font_family,
356
+ 'font-weight': font_info.font_weight,
357
+ 'font-style': font_info.font_style,
358
+ 'font-stretch': font_info.font_stretch,
359
+ 'xml:space': "preserve") do
360
+
361
+ context = pango_font_map.create_context
362
+ layout = Pango::Layout.new(context)
363
+
364
+ pango_font = get_pango_font(font_info, pt(fontsize))
365
+ layout.font_description = pango_font
366
+ layout.width = mm(go_width) * Pango::SCALE
367
+ layout.height = mm(go_height) * Pango::SCALE
368
+ layout.wrap = Pango::WrapMode::WORD_CHAR
369
+ layout.justify = just_block
370
+ layout.alignment = pjust
371
+ layout.ellipsize = Pango::EllipsizeMode::END
372
+ layout.text = data # set the text to render
373
+
374
+ # iterate all lines of the text box
375
+ iter = layout.iter
376
+ loop do
377
+
378
+ # Pango layout coordinates are absolute from the top-left of the layout
379
+ _ink_rect, logical_rect = iter.cluster_extents
380
+ line_x = logical_rect.x.to_f / Pango::SCALE
381
+ # Use the logical rectangle for reliable SVG positioning coordinates
382
+ line_y_baseline = iter.baseline.to_f / Pango::SCALE
383
+
384
+ run = iter.run_readonly
385
+ current_byte_index = iter.index
386
+ # for justified text the signle cluster positions matter
387
+ if just_block ? iter.next_cluster : iter.next_run
388
+ next_byte_index = iter.index
389
+ cluster_text = data.byteslice(current_byte_index...next_byte_index)
390
+ else
391
+ cluster_text = data.byteslice(current_byte_index .. -1)
392
+ end
393
+
394
+ if run
395
+ pango_item = run.item
396
+ if pango_item.analysis.flags & Pango::ANALYSIS_FLAG_IS_ELLIPSIS != 0
397
+ overflow = {fill: "red"}
398
+ end
399
+ end
400
+
401
+ drawing.tspan(cluster_text, x: tra.x + line_x, y: tra.y + line_y_baseline, **overflow)
402
+
403
+ break unless next_byte_index
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ def draw_pictureobject(drawing, tra, go, go_width, go_height, data)
410
+ if data.nil? || data.empty? || (data.tr("\\", "/") =~ /\/$/)
411
+ # kein Bildname angegeben -> dieses Objekt ignorieren
412
+ elsif (path = go.layout_file.expand_path(data)) && FileTest.file?(path)
413
+ require "rmagick"
414
+
415
+ self_dpi = dpi
416
+ image_page = go[GOA_IMAGE_PAGE].to_i - 1
417
+ go_image = Magick::ImageList.new(path) { |img|
418
+ # use view DPI for vector-graphics (PDF)
419
+ img.density = self_dpi
420
+ }[image_page]
421
+
422
+ # workaround: ImageMagick uses wrong colors when inserting into RVG
423
+ if defined?(Magick::ColorSeparationMatteType)
424
+ if [Magick::ColorSeparationType, Magick::ColorSeparationMatteType].include?(go_image.image_type)
425
+ go_image.image_type = Magick::TrueColorMatteType
426
+ end
427
+ end
428
+
429
+ # if object size is not given, try to determine object size based on the DPI of the image
430
+ im_width = (go_width == 0 && go_image.x_resolution > 0) ? go_image.columns / go_image.x_resolution * 25.4 : go_width
431
+ im_height = (go_height == 0 && go_image.y_resolution > 0) ? go_image.rows / go_image.y_resolution * 25.4 : go_height
432
+
433
+ aspect_ratio_img = go_image.columns.to_f / go_image.rows.to_f
434
+ aspect_ratio_box = im_width / im_height
435
+ if aspect_ratio_box > aspect_ratio_img
436
+ # box is wider than image -> add space left and right
437
+ space = im_width - aspect_ratio_img * im_height
438
+ im_width -= space
439
+ apos = tra.translate(mm(space / 2.0), 0.0)
440
+
441
+ elsif aspect_ratio_box < aspect_ratio_img
442
+ # image is wider than box -> add space top and bottom
443
+ space = im_height - im_width / aspect_ratio_img
444
+ im_height -= space
445
+ apos = tra.translate(0.0, mm(space / 2.0))
446
+
447
+ else
448
+ apos = tra
449
+ end
450
+
451
+ cpos = apos.translate(mm(im_width), mm(im_height))
452
+ # drawing.composite(apos.x.round, apos.y.round, (cpos.x - apos.x).round, (cpos.y - apos.y).round, go_image)
453
+ # body.image(go_image, mm(im_width), mm(im_height))
454
+ pngdata = go_image.to_blob do |i|
455
+ i.format = "PNG"
456
+ end
457
+ drawing.image(x: apos.x.round, y: apos.y.round, width: mm(im_width), height: mm(im_height), "href": "data:image/png;base64,#{[pngdata].pack("m")}")
458
+ else
459
+ raise Printer::Base::ResourceNotFoundError, I18n.t(:image_not_found, default: "Das Bild '%{path}' konnte nicht gefunden werden.", path: )
460
+ end
461
+ end
462
+
463
+ BARCODE_ALIAS = {
464
+ EAN13: :Eanx,
465
+ EAN8: :Eanx
466
+ }
467
+
468
+ HALIGNMENT = {
469
+ 0 => :middle,
470
+ 1 => :start,
471
+ 2 => :end
472
+ }
473
+
474
+ def draw_barcode(drawing, tra, go, go_width, go_height, data, barcode_dpi)
475
+ return if data.nil? || data.empty?
476
+
477
+ barcode_typ = go[GOA_BARCODE_TYP]
478
+ option1 = go[GOA_BARCODE_OPTION_1].to_i
479
+ option2 = go[GOA_BARCODE_OPTION_2].to_i
480
+ option3 = go[GOA_BARCODE_OPTION_3].to_i
481
+ font = go[GOA_FONT_TYP]
482
+ if font == "from_file"
483
+ font = go.layout_file.expand_path(go[GOA_FONT_FILE])
484
+ end
485
+ fontsize = go[GOA_FONT_HEIGHT].to_f
486
+ font_color = ["#", go[GOA_FONT_COLOR]].join
487
+
488
+ font_info = get_font_info(font, go)
489
+
490
+ begin
491
+ barcode = Zint.const_get(BARCODE_ALIAS[barcode_typ.to_sym] || barcode_typ).new(value: data)
492
+ rescue => e
493
+ raise Printer::Base::InvalidBarcodeData, I18n.t(:error_generating_barcode, default: "Fehler beim Generieren des Barcodes %{data}: %{e}", data: data.inspect, e: )
494
+ else
495
+ # TODO: bar_ratio wird nicht gesetzt
496
+ # barcode.narrow_width = 1
497
+ # barcode.wide_width = bar_ratio
498
+ barcode.input_mode = Zint::DATA_MODE # Zint::UNICODE_MODE | Zint::ESCAPE_MODE
499
+ # barcode.output_options = Zint::SMALL_TEXT
500
+
501
+ # 50 is the default x-dimension for most of the 1D barcodes:
502
+ barcode.height = go[GOA_BARCODE_HEIGHT].to_f if go[GOA_BARCODE_HEIGHT].to_f.round(3) != 0
503
+ barcode.show_hrt = 0 if fontsize == 0
504
+ barcode.option_1 = option1
505
+ barcode.option_2 = option2
506
+ barcode.option_3 = option3
507
+ # barcode.fontsize = fontsize # not used in zint
508
+ # p [go_height, go_width, barcode.height]
509
+ barcode.whitespace_width = 0
510
+ begin
511
+ bcvector = barcode.to_vector
512
+ rescue Zint::ErrorInvalidData => err
513
+ raise err, "#{err.message} Barcode text: #{data.inspect}"
514
+ end
515
+
516
+ %w[EAN13 EAN8].include?(barcode_typ)
517
+
518
+ # bcvector values are always a multiple of 2 or more, but we need a multiple of 1 for to_i below.
519
+ # So all bcvector measurements are divided by 2 or more.
520
+ xgran = bcvector.width.round / barcode.width
521
+ ygran = bcvector.height.round / barcode.rows
522
+ xbars, ybars = bcvector.width / xgran, bcvector.height / ygran
523
+ # p vector_width: bcvector.width, vector_height: bcvector.height
524
+
525
+ optimized = (go[GOA_BARCODE_OPTIMIZED] || true).to_s == "true"
526
+ px_width, px_height = barcode_mm_to_px(go_width, barcode_dpi), barcode_mm_to_px(go_height, barcode_dpi)
527
+ xdim, ydim = px_width / xbars, px_height / ybars
528
+ if optimized
529
+ xdim = xdim.to_i
530
+ ydim = ydim.to_i if barcode.rows > 1 # optimize y dimension only on 2D codes
531
+ end
532
+ go_width, go_height = barcode_px_to_mm(xdim * xbars, barcode_dpi), barcode_px_to_mm(ydim * ybars, barcode_dpi)
533
+ px_width, px_height = mm(go_width), mm(go_height)
534
+ xdim = px_width / xbars / xgran
535
+ ydim = px_height / ybars / ygran
536
+
537
+ pathd = +""
538
+ bcvector.each_rectangle do |bar|
539
+ # p [bar.x, bar.y, bar.width, bar.height]
540
+ # p [xdim*bar.width, ydim*bar.height, xdim*bar.x, ydim*bar.y]
541
+ x = tra.x + xdim * bar.x
542
+ y = tra.y + ydim * bar.y
543
+ # drawing.rectangle(
544
+ # x,
545
+ # y,
546
+ # x + [xdim * bar.width - 1, 0].max,
547
+ # y + [ydim * bar.height - 1, 0].max
548
+ # )
549
+ # drawing.rect(x: x, y: y, width: xdim * bar.width, height: ydim * bar.height, fill: font_color, stroke: "none")
550
+ # body.rect([xdim * bar.width / 2 - 1, 0].max, [ydim * bar.height / 2 - 1, 0].max, xdim * bar.x / 2, ydim * bar.y / 2)
551
+ w = xdim * bar.width
552
+ h = ydim * bar.height
553
+ pathd << "M#{x} #{y}h#{w}v#{h}h-#{w}Z"
554
+ end
555
+ drawing.path(d: pathd, stroke: "none", fill: font_color)
556
+ bcvector.each_string do |str|
557
+ # p [str.x, str.y, str.text, str.halign, str.fsize]
558
+ # drawing.text_anchor(HALIGNMENT[str.halign])
559
+ # apos = tra.translate(xdim * str.x / xgran, )
560
+ # drawing.text(tra.x + xdim * str.x, tra.y + ydim * str.y, str.text)
561
+ # body.text(xdim * str.x / 2, ydim * str.y / 2, str.text).styles(text_anchor: HALIGNMENT[str.halign])
562
+ drawing.text_(str.text, x: tra.x + xdim * str.x, y: tra.y + ydim * str.y,
563
+ 'text-anchor': HALIGNMENT[str.halign],
564
+ fill: font_color,
565
+ 'font-size': pt(fontsize),
566
+ 'font-family': font_info.font_family,
567
+ 'font-weight': font_info.font_weight,
568
+ 'font-style': font_info.font_style,
569
+ 'font-stretch': font_info.font_stretch,
570
+ 'xml:space': "preserve")
571
+ end
572
+ end
573
+ end
574
+
575
+ def barcode_mm_to_px(l, barcode_dpi)
576
+ barcode_dpi / 25.4 * l
577
+ end
578
+
579
+ def barcode_px_to_mm(l, barcode_dpi)
580
+ l / barcode_dpi * 25.4
581
+ end
582
+
583
+ def draw_boxobject(drawing, tra, go, go_width, go_height)
584
+ corner_radius = go[GOA_CORNER_RADIUS].to_f
585
+ fill_color = go[GOA_FILL_COLOR]
586
+ border_color = go[GOA_BORDER_COLOR]
587
+ border_width = go[GOA_BORDER_WIDTH].to_f
588
+
589
+ drawing.rect(
590
+ fill: "#" + fill_color,
591
+ stroke: "#" + border_color,
592
+ "stroke-width": mm(border_width).round,
593
+ x: tra.x, y: tra.y, width: mm(go_width).round, height: mm(go_height).round,
594
+ rx: mm(corner_radius), ry: mm(corner_radius))
595
+ end
596
+
597
+ def draw_embedobject(drawing, tra, go, go_width, go_height, data, row_number, barcode_dpi)
598
+ if data.nil? || data.empty? || (data.tr("\\", "/") =~ /\/$/)
599
+ # kein Layout angegeben -> dieses Objekt ignorieren
600
+ elsif (path = go.layout_file.expand_path(data)) && FileTest.file?(path)
601
+ layout_file = LayoutFile.load_from_xml(path, go.layout_file.xpath_changes)
602
+
603
+ barcode_dpi2 = layout_file.get_attr_value(S_BARCODE_DPI).to_f
604
+ unless ((barcode_dpi - 0.01)..(barcode_dpi + 0.01)) === barcode_dpi2
605
+ raise Printer::Base::DpiDoesntMatch, I18n.t(:embedded_object_dpi_differs, default: "Barcode-DPI in der Vorschau (%{document_dpi}) und %{obj} (%{embedded_obj_dpi}) sind verschieden. Bitte DPI anpassen und Barcodes neu prüfen!", document_dpi: format("%0.1f", barcode_dpi), obj: go[GOA_NAME].inspect, embedded_obj_dpi: format("%0.1f", barcode_dpi2))
606
+ end
607
+
608
+ obj_w = layout_file.get_attr_value(S_WIDTH).to_f
609
+ obj_h = layout_file.get_attr_value(S_HEIGHT).to_f
610
+
611
+ num_w = (go_width / obj_w).round
612
+ num_h = (go_height / obj_h).round
613
+ num_up = num_h * num_w
614
+
615
+ dataman = layout_file.build_dataman_instance
616
+ row_num = row_number.to_i - 1
617
+
618
+ page_nr = 0
619
+ num_w.times do |idx_w|
620
+ num_h.times do |idx_h|
621
+ if row_num >= 0
622
+ row = dataman.row(row_num * num_up + idx_h * num_w + idx_w)
623
+ if row
624
+ elpos = tra.translate(mm(idx_w * obj_w).round, mm(idx_h * obj_h).round)
625
+ draw(drawing, elpos, layout_file, false, false, row, page_nr, layers: [0])
626
+ end
627
+ end
628
+ end
629
+ end
630
+ else
631
+ raise Printer::Base::ResourceNotFoundError, I18n.t(:embedded_object_not_found, default: "Das Layout '%{path}' konnte nicht gefunden werden.", path: )
632
+ end
633
+ end
634
+
635
+ def get_font_info(font, obj)
636
+ font_info = @@font_map[font]
637
+ unless font_info
638
+ raise Printer::Base::ResourceNotFoundError, I18n.t(:font_not_found, default: "Die Schriftart '%{font}' in Object %{obj} konnte nicht gefunden werden.", font:, obj: obj[GOA_NAME].inspect )
639
+ end
640
+ font_info
641
+ end
642
+
643
+ def get_pango_font(font_info, font_size)
644
+ desc = Pango::FontDescription.new
645
+
646
+ # 1. Family
647
+ desc.family = font_info.font_family
648
+ # 2. Size (Convert pixels/raw user units to Pango absolute size units)
649
+ desc.absolute_size = font_size.to_f * Pango::SCALE
650
+ # 3. Weight
651
+ weight_str = font_info.font_weight.to_s.strip.downcase
652
+ desc.weight = case weight_str
653
+ when '100', 'thin' then Pango::Weight::THIN
654
+ when '200', 'ultralight' then Pango::Weight::ULTRALIGHT
655
+ when '300', 'light' then Pango::Weight::LIGHT
656
+ when '400', 'normal' then Pango::Weight::NORMAL
657
+ when '500', 'medium' then Pango::Weight::MEDIUM
658
+ when '600', 'semibold' then Pango::Weight::SEMIBOLD
659
+ when '700', 'bold' then Pango::Weight::BOLD
660
+ when '800', 'ultrabold' then Pango::Weight::ULTRABOLD
661
+ when '900', 'heavy' then Pango::Weight::HEAVY
662
+ when /\A\d+\z/ then weight_str.to_i
663
+ else Pango::Weight::NORMAL
664
+ end
665
+ # 4. Style
666
+ style_str = font_info.font_style.to_s.strip.downcase
667
+ desc.style = case style_str
668
+ when 'italic' then Pango::Style::ITALIC
669
+ when 'oblique' then Pango::Style::OBLIQUE
670
+ else Pango::Style::NORMAL
671
+ end
672
+ # 5. Stretch
673
+ stretch_str = font_info.font_stretch.to_s.strip.downcase.gsub('-', '_')
674
+ desc.stretch = case stretch_str
675
+ when 'ultra_condensed' then Pango::Stretch::ULTRA_CONDENSED
676
+ when 'extra_condensed' then Pango::Stretch::EXTRA_CONDENSED
677
+ when 'condensed' then Pango::Stretch::CONDENSED
678
+ when 'semi_condensed' then Pango::Stretch::SEMI_CONDENSED
679
+ when 'semi_expanded' then Pango::Stretch::SEMI_EXPANDED
680
+ when 'expanded' then Pango::Stretch::EXPANDED
681
+ when 'extra_expanded' then Pango::Stretch::EXTRA_EXPANDED
682
+ when 'ultra_expanded' then Pango::Stretch::ULTRA_EXPANDED
683
+ else Pango::Stretch::NORMAL
684
+ end
685
+
686
+ desc
687
+ end
688
+ end
689
+ end