defmodule Pdf.Component.Barcode do @moduledoc """ Barcode PDF component — renders Code 128 barcodes onto a PDF. This is a **thin renderer**. All encoding logic (standard and shaped) lives in `ex_barcode`. This component only scales and draws. ## Standard barcode Pdf.Component.Barcode.render(doc, {50, 700}, %{data: "ABC-12345", width: 200}) ## Shape barcode (creative barcode with silhouette + decorations) Pdf.Component.Barcode.render(doc, {50, 700}, %{ data: "DEMOCAMP", width: 250, height: 100, shape: :rv }) ## Style options ### Core - `:data` — the string to encode (required) - `:width` — barcode width in points (default `200`) - `:height` — bar height in points (default `50`, or auto from aspect ratio) - `:color` — bar color (default `{0, 0, 0}`) - `:background` — optional background color (standard barcodes only) - `:quiet_zone` — modules of white space (default `10` standard, `2` shaped) ### Text - `:show_text` — render data below bars (default `true`) - `:font` / `:font_size` / `:text_color` — text styling - `:label` — optional title above barcode - `:label_font_size` / `:label_color` — label styling ### Image - `:image` — path, URL, or `{:binary, data}` - `:image_size` — `{w, h}` (default `{40, 40}`) - `:image_position` — `:left`, `:right`, `:top` (default `:left`) - `:image_gap` — gap between image and barcode (default `8`) - `:image_border_radius` — rounded clip (default `0`) ### Shape (delegated to `ExBarcode.Shape`) - `:shape` — predefined shape: `:rv`, `:camper`, `:city`, `:wave`, `:diamond`, `:hill` - `:contour_top` — custom top contour `[{x_pct, y_pct}, ...]` - `:contour_bottom` — custom bottom contour - `:bar_min_height` — minimum bar fraction (default `0.0`) - `:decoration_color` — color for decorations (default same as `:color`) - `:decoration_stroke_color` — stroke color for stroke decorations (default lighter) """ @default_width 200 @default_height 50 @default_color {0, 0, 0} @default_font "Helvetica" @default_font_size 8 @kappa 0.5522847498 def render(doc, {x, y}, style \\ %{}) do data = Map.get(style, :data, "") shape = Map.get(style, :shape) contour_top = Map.get(style, :contour_top) if shape || contour_top do render_shaped(doc, {x, y}, style, data) else render_standard(doc, {x, y}, style, data) end end # ── Standard barcode ─────────────────────────────────────────── defp render_standard(doc, {x, y}, style, data) do width = Map.get(style, :width, @default_width) height = Map.get(style, :height, @default_height) color = Map.get(style, :color, @default_color) bg = Map.get(style, :background) show_text = Map.get(style, :show_text, true) font = Map.get(style, :font, @default_font) font_size = Map.get(style, :font_size, @default_font_size) text_color = Map.get(style, :text_color, color) quiet_zone = Map.get(style, :quiet_zone, 10) image = Map.get(style, :image) {img_w, img_h} = Map.get(style, :image_size, {40, 40}) img_pos = Map.get(style, :image_position, :left) img_gap = Map.get(style, :image_gap, 8) img_radius = Map.get(style, :image_border_radius, 0) label = Map.get(style, :label) label_fs = Map.get(style, :label_font_size, 10) label_color = Map.get(style, :label_color, color) case ExBarcode.encode(data) do {:ok, bars} -> {bar_x, bar_y_start, img_x, img_y} = compute_layout(x, y, width, image, {img_w, img_h}, img_pos, img_gap, label, label_fs) doc = render_label(doc, label, bar_x, y, font, label_fs, label_color) baseline = bar_y_start - height total_modules = Enum.sum(bars) + quiet_zone * 2 module_width = width / total_modules doc = if bg do total_h = if show_text, do: height + font_size + 4, else: height doc |> Pdf.save_state() |> Pdf.set_fill_color(bg) |> Pdf.rectangle({bar_x, bar_y_start - total_h}, {width, total_h}) |> Pdf.fill() |> Pdf.restore_state() else doc end doc = Pdf.save_state(doc) |> Pdf.set_fill_color(color) {doc, _} = bars |> Enum.with_index() |> Enum.reduce({doc, bar_x + quiet_zone * module_width}, fn {modules, i}, {d, offset} -> bar_w = modules * module_width d = if rem(i, 2) == 0 do d |> Pdf.rectangle({offset, baseline}, {bar_w, height}) |> Pdf.fill() else d end {d, offset + bar_w} end) doc = Pdf.restore_state(doc) doc = render_text(doc, show_text, data, bar_x, baseline, width, font, font_size, text_color) if image do draw_image(doc, image, {img_x, img_y}, {img_w, img_h}, img_radius) else doc end {:error, _} -> doc end end # ── Shaped barcode ───────────────────────────────────────────── defp render_shaped(doc, {x, y}, style, data) do width = Map.get(style, :width, @default_width) color = Map.get(style, :color, @default_color) bg = Map.get(style, :background, {1, 1, 1}) show_text = Map.get(style, :show_text, true) font = Map.get(style, :font, @default_font) font_size = Map.get(style, :font_size, @default_font_size) text_color = Map.get(style, :text_color, color) label = Map.get(style, :label) label_fs = Map.get(style, :label_font_size, 10) label_color = Map.get(style, :label_color, color) deco_color = Map.get(style, :decoration_color, color) deco_stroke = Map.get(style, :decoration_stroke_color, lighten(color, 0.3)) shape_opts = [ shape: Map.get(style, :shape), contour_top: Map.get(style, :contour_top), contour_bottom: Map.get(style, :contour_bottom), quiet_zone: Map.get(style, :quiet_zone, 2), bar_min: Map.get(style, :bar_min_height, 0.0) ] case ExBarcode.encode_shaped(data, shape_opts) do {:ok, result} -> height = Map.get(style, :height, width / result.aspect_ratio) baseline = y - height doc = render_label(doc, label, x, y, font, label_fs, label_color) case result.style do :silhouette -> render_silhouette(doc, result, x, baseline, width, height, color, bg, deco_color, deco_stroke) :contour -> render_contour(doc, result, x, baseline, width, height, color, deco_color, deco_stroke) end |> render_text(show_text, data, x, baseline, width, font, font_size, text_color) {:error, _} -> doc end end # ── Silhouette renderer ────────────────────────────────────── # 1. Fill silhouette polygons (barcode color) # 2. Fill barcode region (background color = clear) # 3. Draw bars inside region (barcode color) # 4. Draw cutouts (background color details on solid areas) # 5. Draw decorations (barcode color details) defp render_silhouette(doc, result, x, baseline, w, h, color, bg, deco_color, deco_stroke) do # 1. Draw silhouette polygons doc = Enum.reduce(result.silhouette, doc, fn polygon, d -> scaled = Enum.map(polygon, fn {px, py} -> {x + px * w, baseline + py * h} end) render_polygon_fill(d, scaled, color) end) # 2. Clear barcode region with background doc = case result.barcode_region do {rx, ry, rw, rh} -> doc |> Pdf.save_state() |> Pdf.set_fill_color(bg) |> Pdf.rectangle({x + rx * w, baseline + ry * h}, {rw * w, rh * h}) |> Pdf.fill() |> Pdf.restore_state() _ -> doc end # 3. Draw bars doc = Pdf.save_state(doc) |> Pdf.set_fill_color(color) doc = Enum.reduce(result.bars, doc, fn bar, d -> bx = x + bar.x * w by = baseline + bar.y * h bw = bar.w * w bh = bar.h * h if bh > 0.3 do d |> Pdf.rectangle({bx, by}, {bw, bh}) |> Pdf.fill() else d end end) doc = Pdf.restore_state(doc) # 4. Draw cutouts (background-colored shapes on solid areas) doc = render_cutouts(doc, result.cutouts, x, baseline, w, h, bg) # 5. Draw decorations render_decorations(doc, result.decorations, x, baseline, w, h, deco_color, deco_stroke) end # ── Contour renderer (existing behavior) ───────────────────── defp render_contour(doc, result, x, baseline, w, h, color, deco_color, deco_stroke) do doc = Pdf.save_state(doc) |> Pdf.set_fill_color(color) doc = Enum.reduce(result.bars, doc, fn bar, d -> bx = x + bar.x * w by = baseline + bar.y * h bw = bar.w * w bh = bar.h * h if bh > 0.5 do d |> Pdf.rectangle({bx, by}, {bw, bh}) |> Pdf.fill() else d end end) doc = Pdf.restore_state(doc) render_decorations(doc, result.decorations, x, baseline, w, h, deco_color, deco_stroke) end # ── Decoration renderer ──────────────────────────────────────── defp render_decorations(doc, decorations, x, baseline, w, h, fill_color, stroke_color) do Enum.reduce(decorations, doc, fn deco, d -> case deco do {:circle, {cx, cy, r}} -> render_circle(d, x + cx * w, baseline + cy * h, r * min(w, h), fill_color) {:circle_stroke, {cx, cy, r}} -> render_circle_stroke(d, x + cx * w, baseline + cy * h, r * min(w, h), stroke_color) {:polygon, points} -> scaled = Enum.map(points, fn {px, py} -> {x + px * w, baseline + py * h} end) render_polygon_stroke(d, scaled, stroke_color) {:line, {x1, y1, x2, y2}} -> d |> Pdf.save_state() |> Pdf.set_stroke_color(fill_color) |> Pdf.set_line_width(0.8) |> Pdf.line({x + x1 * w, baseline + y1 * h}, {x + x2 * w, baseline + y2 * h}) |> Pdf.stroke() |> Pdf.restore_state() _ -> d end end) end defp render_circle(doc, cx, cy, r, color) do k = r * @kappa doc |> Pdf.save_state() |> Pdf.set_fill_color(color) |> Pdf.move_to({cx + r, cy}) |> Pdf.curve_to({cx + r, cy + k}, {cx + k, cy + r}, {cx, cy + r}) |> Pdf.curve_to({cx - k, cy + r}, {cx - r, cy + k}, {cx - r, cy}) |> Pdf.curve_to({cx - r, cy - k}, {cx - k, cy - r}, {cx, cy - r}) |> Pdf.curve_to({cx + k, cy - r}, {cx + r, cy - k}, {cx + r, cy}) |> Pdf.close_path() |> Pdf.fill() |> Pdf.restore_state() end defp render_circle_stroke(doc, cx, cy, r, color) do k = r * @kappa doc |> Pdf.save_state() |> Pdf.set_stroke_color(color) |> Pdf.set_line_width(0.5) |> Pdf.move_to({cx + r, cy}) |> Pdf.curve_to({cx + r, cy + k}, {cx + k, cy + r}, {cx, cy + r}) |> Pdf.curve_to({cx - k, cy + r}, {cx - r, cy + k}, {cx - r, cy}) |> Pdf.curve_to({cx - r, cy - k}, {cx - k, cy - r}, {cx, cy - r}) |> Pdf.curve_to({cx + k, cy - r}, {cx + r, cy - k}, {cx + r, cy}) |> Pdf.close_path() |> Pdf.stroke() |> Pdf.restore_state() end defp render_polygon_fill(doc, points, color) do case points do [first | rest] -> doc = doc |> Pdf.save_state() |> Pdf.set_fill_color(color) |> Pdf.move_to(first) doc = Enum.reduce(rest, doc, fn pt, d -> Pdf.line_append(d, pt) end) doc |> Pdf.close_path() |> Pdf.fill() |> Pdf.restore_state() _ -> doc end end # ── Cutout renderer (background-colored shapes on solid areas) ── defp render_cutouts(doc, cutouts, x, baseline, w, h, bg_color) do Enum.reduce(cutouts, doc, fn cutout, d -> case cutout do {:circle_bg, {cx, cy, r}} -> render_circle(d, x + cx * w, baseline + cy * h, r * min(w, h), bg_color) {:polygon_bg, points} -> scaled = Enum.map(points, fn {px, py} -> {x + px * w, baseline + py * h} end) render_polygon_fill(d, scaled, bg_color) _ -> d end end) end defp render_polygon_stroke(doc, points, color) do case points do [first | rest] -> doc = doc |> Pdf.save_state() |> Pdf.set_stroke_color(color) |> Pdf.set_line_width(0.5) |> Pdf.move_to(first) doc = Enum.reduce(rest, doc, fn pt, d -> Pdf.line_append(d, pt) end) doc |> Pdf.close_path() |> Pdf.stroke() |> Pdf.restore_state() _ -> doc end end # ── Shared helpers ───────────────────────────────────────────── defp render_label(doc, nil, _x, _y, _font, _fs, _color), do: doc defp render_label(doc, label, x, y, font, fs, color) do doc |> Pdf.set_font(font, fs, bold: true) |> Pdf.set_fill_color(color) |> Pdf.text_at({x, y - fs}, label) end defp render_text(doc, false, _data, _x, _baseline, _w, _font, _fs, _color), do: doc defp render_text(doc, true, data, x, baseline, width, font, font_size, text_color) do text_y = baseline - font_size - 2 text_w = String.length(data) * font_size * 0.6 text_x = x + (width - text_w) / 2 doc |> Pdf.set_font(font, font_size) |> Pdf.set_fill_color(text_color) |> Pdf.text_at({text_x, text_y}, data) end defp compute_layout(x, y, width, image, {img_w, img_h}, img_pos, img_gap, label, label_fs) do label_offset = if label, do: label_fs + 4, else: 0 if image do case img_pos do :left -> {x + img_w + img_gap, y - label_offset, x, y - label_offset} :right -> {x, y - label_offset, x + width + img_gap, y - label_offset} :top -> {x, y - img_h - img_gap - label_offset, x + (width - img_w) / 2, y - label_offset} _ -> {x + img_w + img_gap, y - label_offset, x, y - label_offset} end else {x, y - label_offset, x, y} end end defp draw_image(doc, image, {ix, iy}, {iw, ih}, radius) do img_bottom = iy - ih if radius > 0 do doc |> Pdf.save_state() |> Pdf.rounded_rectangle({ix, img_bottom}, {iw, ih}, radius) |> Pdf.clip() |> Pdf.add_image({ix, img_bottom}, image, width: iw, height: ih) |> Pdf.restore_state() else Pdf.add_image(doc, {ix, img_bottom}, image, width: iw, height: ih) end end defp lighten({r, g, b}, amount) do {r + (1 - r) * amount, g + (1 - g) * amount, b + (1 - b) * amount} end end