id2pdf 0.1.0__py3-none-any.whl

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.
id2pdf/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """Scan both sides of an ID card onto one A4 page, as a PDF.
2
+
3
+ The card may lie anywhere on the scanner glass: a quick scan of the whole glass finds it,
4
+ then only that spot is scanned at full resolution, straightened and cut out.
5
+ """
6
+
7
+ from .detect import CARD_MM, card_from_image, cut_out, find_cards
8
+ from .i18n import set_language
9
+ from .layout import CARD_DPI, IMAGE_SUFFIXES, PAGE_DPI, id_card_page, images_to_pdf, open_image, save_pdf
10
+ from .scanner import ScanError, find_scanner, scan, scan_card_side, scan_id_card, scan_pages
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = [
15
+ "CARD_DPI",
16
+ "CARD_MM",
17
+ "IMAGE_SUFFIXES",
18
+ "PAGE_DPI",
19
+ "ScanError",
20
+ "card_from_image",
21
+ "cut_out",
22
+ "find_cards",
23
+ "find_scanner",
24
+ "id_card_page",
25
+ "images_to_pdf",
26
+ "open_image",
27
+ "save_pdf",
28
+ "scan",
29
+ "scan_card_side",
30
+ "scan_id_card",
31
+ "scan_pages",
32
+ "set_language",
33
+ ]
id2pdf/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
id2pdf/cli.py ADDED
@@ -0,0 +1,71 @@
1
+ """The id2pdf command."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from . import __version__
8
+ from .detect import card_from_image
9
+ from .i18n import _
10
+ from .layout import CARD_DPI, id_card_page, open_image, save_pdf
11
+ from .scanner import ScanError, diagnose, find_scanner, scan_id_card, scan_pages
12
+
13
+
14
+ def parser() -> argparse.ArgumentParser:
15
+ p = argparse.ArgumentParser(
16
+ prog="id2pdf",
17
+ description=_(
18
+ "Scan both sides of an ID card onto one A4 page, as a PDF. The card may lie "
19
+ "anywhere on the scanner glass; it is found, straightened and cut out."
20
+ ),
21
+ )
22
+ p.add_argument("output", type=Path, nargs="?", help=_("the PDF to write (with --diagnose: a folder for the scan)"))
23
+ mode = p.add_mutually_exclusive_group()
24
+ mode.add_argument(
25
+ "--images",
26
+ nargs=2,
27
+ type=Path,
28
+ metavar=("FRONT", "BACK"),
29
+ help=_("use existing scans or photos of the two sides instead of scanning"),
30
+ )
31
+ mode.add_argument("--pages", type=int, metavar="N", help=_("scan N plain A4 pages instead of a card"))
32
+ mode.add_argument(
33
+ "--diagnose", action="store_true", help=_("scan the glass and print what is found (numbers only)")
34
+ )
35
+ p.add_argument("--device", help=_("SANE device (default: the first scanner in 'scanimage -L')"))
36
+ p.add_argument("-f", "--force", action="store_true", help=_("overwrite the output without asking"))
37
+ p.add_argument("--version", action="version", version=f"id2pdf {__version__}")
38
+ return p
39
+
40
+
41
+ def main(argv: list[str] | None = None) -> None:
42
+ args = parser().parse_args(argv)
43
+ if not args.output and not args.diagnose:
44
+ parser().error(_("the output PDF is required"))
45
+ if args.output and args.output.exists() and not args.diagnose and not args.force:
46
+ answer = input(_("{path} already exists. Overwrite? [y/N] ").format(path=args.output)).strip().lower()
47
+ if answer not in (_("y"), "y"):
48
+ sys.exit(_("Nothing was changed."))
49
+ try:
50
+ if args.images:
51
+ sides = [card_from_image(open_image(path)) for path in args.images]
52
+ save_pdf([id_card_page(*sides)], args.output, CARD_DPI)
53
+ else:
54
+ device = args.device or find_scanner()
55
+ print(_("Scanner: {device}").format(device=device))
56
+ if args.diagnose:
57
+ diagnose(device, args.output)
58
+ return
59
+ if args.pages:
60
+ save_pdf(scan_pages(device, args.pages), args.output)
61
+ else:
62
+ save_pdf([scan_id_card(device)], args.output, CARD_DPI)
63
+ except ScanError as error:
64
+ sys.exit(str(error))
65
+ except (EOFError, KeyboardInterrupt):
66
+ sys.exit("\n" + _("Cancelled, nothing was saved."))
67
+ print(_("Saved: {path}").format(path=args.output))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
id2pdf/detect.py ADDED
@@ -0,0 +1,173 @@
1
+ """Finding ID-1 cards (85.6 x 54 mm: ID cards, driving licences, ...) on scans."""
2
+
3
+ import cv2
4
+ import numpy as np
5
+ from PIL import Image, ImageDraw
6
+
7
+ CARD_MM = (85.6, 54.0)
8
+ CARD_CORNER_MM = 3.18
9
+ PREVIEW_DPI = 100 # quick scan of the whole glass to find the card
10
+ DARKER = 15 # grey levels below the lid that count as part of a card (the lid's shading is less)
11
+ GLASS_EDGE_MM = 2 # strip along the edges of the glass ignored when looking for the card
12
+
13
+ Rect = tuple[tuple[float, float], tuple[float, float], float] # cv2.minAreaRect: centre, size, angle
14
+
15
+ # Why card_candidates() did not take an object for a card.
16
+ SHAPE, SIZE, TOO_SMALL = "shape", "size", "too small"
17
+
18
+
19
+ def mm_to_px(mm: float, dpi: float) -> int:
20
+ return round(mm / 25.4 * dpi)
21
+
22
+
23
+ def px_to_mm(px: float, dpi: float) -> float:
24
+ return px / dpi * 25.4
25
+
26
+
27
+ def card_candidates(image: Image.Image, dpi: float | None, glass: bool = False) -> list[tuple[Rect, str]]:
28
+ """Every object on a scan with why it is not a card ("" = it is one), largest first.
29
+
30
+ A card shows as sharp edges (its outline and shadow, its print) and areas clearly darker
31
+ than the lid; the lid's shading is gradual and only slightly darker, and dust is too
32
+ small to make a card shape. glass: the image is the whole glass, whose edges are ignored.
33
+ dpi None skips the size check (photos).
34
+ """
35
+ gray = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2GRAY)
36
+ smooth = cv2.GaussianBlur(gray, (5, 5), 0)
37
+ lid = float(np.median(gray))
38
+ mask = ((cv2.Canny(smooth, 20, 60) > 0) | (smooth < lid - DARKER)).astype(np.uint8) * 255
39
+ if glass:
40
+ edge = mm_to_px(GLASS_EDGE_MM, dpi or PREVIEW_DPI)
41
+ mask[:edge, :] = mask[-edge:, :] = 0
42
+ mask[:, :edge] = mask[:, -edge:] = 0
43
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((2, 2), np.uint8))
44
+ k = max(3, mm_to_px(1.5, dpi or PREVIEW_DPI))
45
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)))
46
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
47
+ found = []
48
+ for contour in contours:
49
+ rect = cv2.minAreaRect(cv2.convexHull(contour))
50
+ long, short = sorted(rect[1], reverse=True)
51
+ if short < 10:
52
+ continue
53
+ reason = ""
54
+ # A thick card lifts the lid: its shadow makes the object up to ~10 mm larger all round.
55
+ if abs(long / short - CARD_MM[0] / CARD_MM[1]) > 0.25:
56
+ reason = SHAPE
57
+ elif dpi is not None and not 0.85 < long / mm_to_px(CARD_MM[0], dpi) < 1.45:
58
+ reason = SIZE
59
+ elif dpi is None and long * short < 0.05 * gray.size:
60
+ reason = TOO_SMALL
61
+ found.append((rect, reason))
62
+ return sorted(found, key=lambda c: -c[0][1][0] * c[0][1][1])
63
+
64
+
65
+ def perimeter(width: float, height: float, corner: float, spacing: float) -> list[tuple[np.ndarray, np.ndarray]]:
66
+ """For each straight side of a width x height rectangle centred on 0: its points and normal."""
67
+ xs = np.arange(-width / 2 + corner, width / 2 - corner, spacing)
68
+ ys = np.arange(-height / 2 + corner, height / 2 - corner, spacing)
69
+ return [
70
+ (np.stack([xs, np.full_like(xs, -height / 2)], 1), np.array([0.0, 1.0])),
71
+ (np.stack([xs, np.full_like(xs, height / 2)], 1), np.array([0.0, 1.0])),
72
+ (np.stack([np.full_like(ys, -width / 2), ys], 1), np.array([1.0, 0.0])),
73
+ (np.stack([np.full_like(ys, width / 2), ys], 1), np.array([1.0, 0.0])),
74
+ ]
75
+
76
+
77
+ def fit_card(gray: np.ndarray, blob: Rect, dpi: float) -> Rect:
78
+ """The card inside an object found on the scan, which may include the card's shadow.
79
+
80
+ Places a card-sized rectangle (within 3 %) on the sharpest edges near the object: the
81
+ card's edge is a sharp step, its shadow a soft one. Every side has to lie on an edge, so
82
+ the score is the weakest side's mean sharpness across it, blurred a little so that a
83
+ slightly misplaced edge still counts. The angle comes from the object, whose shadow is
84
+ about even all round; on edges alone it is poorly determined.
85
+ """
86
+ smooth = cv2.GaussianBlur(gray, (3, 3), 0).astype(np.float32)
87
+ sigma = max(1.0, mm_to_px(0.3, dpi))
88
+ across_x = cv2.GaussianBlur(np.abs(cv2.Sobel(smooth, cv2.CV_32F, 1, 0)), (0, 0), sigma)
89
+ across_y = cv2.GaussianBlur(np.abs(cv2.Sobel(smooth, cv2.CV_32F, 0, 1)), (0, 0), sigma)
90
+ card_w, card_h = (mm_to_px(mm, dpi) for mm in CARD_MM)
91
+ corner = mm_to_px(CARD_CORNER_MM + 0.5, dpi)
92
+ spacing = max(1.0, dpi / 100)
93
+ (cx, cy), (w, h), angle = blob
94
+ if w < h:
95
+ w, h, angle = h, w, angle + 90
96
+ angle = (angle + 90) % 180 - 90
97
+ rows, cols = gray.shape
98
+
99
+ def best(angles, xs, ys, scales):
100
+ top = (-1.0, cx, cy, angle, 1.0, 1.0)
101
+ for a in angles:
102
+ t = np.deg2rad(a)
103
+ turn = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]])
104
+ for sx in scales:
105
+ for sy in scales:
106
+ weakest = None
107
+ for points, normal in perimeter(card_w * sx, card_h * sy, corner, spacing):
108
+ r = points @ turn.T
109
+ nx, ny = np.abs(turn @ normal)
110
+ gx = np.clip(np.rint(xs[:, None, None] + r[None, None, :, 0]).astype(int), 0, cols - 1)
111
+ gy = np.clip(np.rint(ys[None, :, None] + r[None, None, :, 1]).astype(int), 0, rows - 1)
112
+ side = (across_x[gy, gx] * nx + across_y[gy, gx] * ny).mean(axis=2)
113
+ weakest = side if weakest is None else np.minimum(weakest, side)
114
+ i, j = np.unravel_index(np.argmax(weakest), weakest.shape)
115
+ if weakest[i, j] > top[0]:
116
+ top = (float(weakest[i, j]), float(xs[i]), float(ys[j]), float(a), sx, sy)
117
+ return top
118
+
119
+ step = max(1, mm_to_px(0.5, dpi))
120
+ span_x = max(0.0, (w - card_w) / 2) + mm_to_px(2, dpi)
121
+ span_y = max(0.0, (h - card_h) / 2) + mm_to_px(2, dpi)
122
+ _, x, y, a, _, _ = best(
123
+ np.array([angle]),
124
+ np.arange(cx - span_x, cx + span_x + 1, step),
125
+ np.arange(cy - span_y, cy + span_y + 1, step),
126
+ (1.0,),
127
+ )
128
+ fine = max(1.0, step / 3)
129
+ _, x, y, a, sx, sy = best(
130
+ np.arange(a - 0.5, a + 0.51, 0.25),
131
+ np.arange(x - step, x + step + 1, fine),
132
+ np.arange(y - step, y + step + 1, fine),
133
+ np.arange(0.97, 1.031, 0.01),
134
+ )
135
+ return (x, y), (card_w * sx, card_h * sy), a
136
+
137
+
138
+ def find_cards(image: Image.Image, dpi: float | None, glass: bool = False) -> list[Rect]:
139
+ """Cards on a scan, largest first; with a known dpi each is fitted to the card's real edges."""
140
+ blobs = [rect for rect, reason in card_candidates(image, dpi, glass) if not reason]
141
+ if dpi is None:
142
+ return blobs
143
+ gray = np.array(image.convert("L"))
144
+ return [fit_card(gray, blob, dpi) for blob in blobs]
145
+
146
+
147
+ def cut_out(image: Image.Image, rect: Rect) -> Image.Image:
148
+ """The card straightened (long side horizontal), with white at its rounded corners."""
149
+ (cx, cy), (w, h), angle = rect
150
+ if w < h: # make the angle the one of the long side
151
+ w, h, angle = h, w, angle + 90
152
+ angle = (angle + 90) % 180 - 90 # the smallest turn that makes the long side horizontal
153
+ turn = cv2.getRotationMatrix2D((cx, cy), angle, 1.0)
154
+ turn[0, 2] += w / 2 - cx
155
+ turn[1, 2] += h / 2 - cy
156
+ card = cv2.warpAffine(
157
+ np.array(image.convert("RGB")),
158
+ turn,
159
+ (round(w), round(h)),
160
+ flags=cv2.INTER_CUBIC,
161
+ borderValue=(255, 255, 255),
162
+ )
163
+ card = Image.fromarray(card)
164
+ outline = Image.new("L", card.size, 0)
165
+ radius = round(CARD_CORNER_MM / CARD_MM[1] * card.height)
166
+ ImageDraw.Draw(outline).rounded_rectangle((0, 0, card.width - 1, card.height - 1), radius=radius, fill=255)
167
+ return Image.composite(card, Image.new("RGB", card.size, "white"), outline)
168
+
169
+
170
+ def card_from_image(image: Image.Image, dpi: float | None = None) -> Image.Image:
171
+ """The card cut out of a scan or photo, or the image as it is if no card is found."""
172
+ cards = find_cards(image, dpi)
173
+ return cut_out(image, cards[0]) if cards else image
id2pdf/i18n.py ADDED
@@ -0,0 +1,23 @@
1
+ """Translations of the messages (gettext, domain "id2pdf").
2
+
3
+ The language follows the usual environment variables (LANGUAGE, LC_ALL, LC_MESSAGES,
4
+ LANG); a program that uses id2pdf as a library can pick one with set_language().
5
+ """
6
+
7
+ import gettext
8
+ from pathlib import Path
9
+
10
+ LOCALE_DIR = Path(__file__).parent / "locale"
11
+
12
+ _translation: gettext.NullTranslations = gettext.translation("id2pdf", LOCALE_DIR, fallback=True)
13
+
14
+
15
+ def set_language(language: str | None) -> None:
16
+ """Use this language ("hr", "en", ...); None goes back to the environment's."""
17
+ global _translation
18
+ languages = [language] if language else None
19
+ _translation = gettext.translation("id2pdf", LOCALE_DIR, languages=languages, fallback=True)
20
+
21
+
22
+ def _(message: str) -> str:
23
+ return _translation.gettext(message)
id2pdf/layout.py ADDED
@@ -0,0 +1,49 @@
1
+ """Putting cards and pages into a PDF."""
2
+
3
+ from pathlib import Path
4
+
5
+ from PIL import Image, ImageOps
6
+
7
+ from .detect import CARD_MM, mm_to_px
8
+
9
+ CARD_DPI = 300
10
+ PAGE_DPI = 200 # enough for documents, keeps a page around 1 MB
11
+ A4_MM = (210, 297)
12
+ GAP_MM = 15
13
+ IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".bmp"}
14
+
15
+
16
+ def card_size(image: Image.Image) -> Image.Image:
17
+ """Scale to the real card size (landscape) so the printout is 1:1."""
18
+ if image.height > image.width:
19
+ image = image.rotate(90, expand=True)
20
+ return ImageOps.contain(image, (mm_to_px(CARD_MM[0], CARD_DPI), mm_to_px(CARD_MM[1], CARD_DPI)))
21
+
22
+
23
+ def id_card_page(front: Image.Image, back: Image.Image) -> Image.Image:
24
+ """Both sides on one A4 page, at real size like a photocopy."""
25
+ front, back = card_size(front), card_size(back)
26
+ page = Image.new("RGB", (mm_to_px(A4_MM[0], CARD_DPI), mm_to_px(A4_MM[1], CARD_DPI)), "white")
27
+ gap = mm_to_px(GAP_MM, CARD_DPI)
28
+ top = (page.height - front.height - gap - back.height) // 3 # a bit above the middle
29
+ for image, y in ((front, top), (back, top + front.height + gap)):
30
+ page.paste(image, ((page.width - image.width) // 2, y))
31
+ return page
32
+
33
+
34
+ def save_pdf(images: list[Image.Image], output: Path, dpi: int = PAGE_DPI) -> None:
35
+ output.parent.mkdir(parents=True, exist_ok=True)
36
+ first, *rest = images
37
+ first.save(output, "PDF", resolution=dpi, save_all=True, append_images=rest, quality=85)
38
+
39
+
40
+ def open_image(path: Path) -> Image.Image:
41
+ """An image file, turned the way the camera's EXIF data says."""
42
+ return ImageOps.exif_transpose(Image.open(path)).convert("RGB")
43
+
44
+
45
+ def images_to_pdf(paths: list[Path], output: Path) -> None:
46
+ """Photos or scans from elsewhere into one PDF, one image per page."""
47
+ images = [open_image(p) for p in paths]
48
+ longest = max(max(i.size) for i in images)
49
+ save_pdf(images, output, dpi=max(72, round(longest / (A4_MM[1] / 25.4))))
Binary file
@@ -0,0 +1,210 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the id2pdf package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: id2pdf\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2026-10-18 19:52+0200\n"
12
+ "PO-Revision-Date: 2026-10-18 20:00+0200\n"
13
+ "Last-Translator: anaisoob <anaisoob@tutamail.com>\n"
14
+ "Language-Team: Croatian\n"
15
+ "Language: hr\n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
20
+ "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
21
+
22
+ #: src/id2pdf/cli.py:18
23
+ msgid ""
24
+ "Scan both sides of an ID card onto one A4 page, as a PDF. The card may lie "
25
+ "anywhere on the scanner glass; it is found, straightened and cut out."
26
+ msgstr ""
27
+ "Skenira obje strane osobne iskaznice na jednu A4 stranicu, kao PDF. Osobna "
28
+ "može ležati bilo gdje na staklu skenera; skripta je pronađe, izravna i "
29
+ "izreže."
30
+
31
+ #: src/id2pdf/cli.py:22
32
+ msgid "the PDF to write (with --diagnose: a folder for the scan)"
33
+ msgstr "PDF koji se sprema (uz --diagnose: mapa za sken)"
34
+
35
+ #: src/id2pdf/cli.py:29
36
+ msgid "use existing scans or photos of the two sides instead of scanning"
37
+ msgstr ""
38
+ "koristi postojeće skenove ili fotografije obiju strana umjesto skeniranja"
39
+
40
+ #: src/id2pdf/cli.py:31
41
+ msgid "scan N plain A4 pages instead of a card"
42
+ msgstr "skenira N običnih A4 stranica umjesto kartice"
43
+
44
+ #: src/id2pdf/cli.py:33
45
+ msgid "scan the glass and print what is found (numbers only)"
46
+ msgstr "skenira staklo i ispiše što je pronađeno (samo brojevi)"
47
+
48
+ #: src/id2pdf/cli.py:35
49
+ msgid "SANE device (default: the first scanner in 'scanimage -L')"
50
+ msgstr "SANE uređaj (zadano: prvi skener iz 'scanimage -L')"
51
+
52
+ #: src/id2pdf/cli.py:36
53
+ msgid "overwrite the output without asking"
54
+ msgstr "zamijeni postojeću datoteku bez pitanja"
55
+
56
+ #: src/id2pdf/cli.py:44
57
+ msgid "the output PDF is required"
58
+ msgstr "potreban je izlazni PDF"
59
+
60
+ #: src/id2pdf/cli.py:46
61
+ #, python-brace-format
62
+ msgid "{path} already exists. Overwrite? [y/N] "
63
+ msgstr "{path} već postoji. Zamijeniti? [d/N] "
64
+
65
+ #: src/id2pdf/cli.py:47
66
+ msgid "y"
67
+ msgstr "d"
68
+
69
+ #: src/id2pdf/cli.py:48
70
+ msgid "Nothing was changed."
71
+ msgstr "Ništa nije promijenjeno."
72
+
73
+ #: src/id2pdf/cli.py:55
74
+ #, python-brace-format
75
+ msgid "Scanner: {device}"
76
+ msgstr "Skener: {device}"
77
+
78
+ #: src/id2pdf/cli.py:66
79
+ msgid "Cancelled, nothing was saved."
80
+ msgstr "Prekinuto, ništa nije spremljeno."
81
+
82
+ #: src/id2pdf/cli.py:67
83
+ #, python-brace-format
84
+ msgid "Saved: {path}"
85
+ msgstr "Spremljeno: {path}"
86
+
87
+ #: src/id2pdf/scanner.py:34
88
+ msgid "The scanimage command is missing (package sane)."
89
+ msgstr "Nema naredbe scanimage (paket sane)."
90
+
91
+ #: src/id2pdf/scanner.py:36
92
+ msgid "'scanimage -L' failed:"
93
+ msgstr "'scanimage -L' nije uspio:"
94
+
95
+ #: src/id2pdf/scanner.py:40
96
+ msgid "No scanner found. Check 'scanimage -L'."
97
+ msgstr "Skener nije pronađen. Provjerite 'scanimage -L'."
98
+
99
+ #: src/id2pdf/scanner.py:57
100
+ msgid "The scanner is still busy, trying again..."
101
+ msgstr "Skener je još zauzet, pokušavam ponovno..."
102
+
103
+ #: src/id2pdf/scanner.py:60
104
+ msgid "Scanning failed:"
105
+ msgstr "Skeniranje nije uspjelo:"
106
+
107
+ #: src/id2pdf/scanner.py:65
108
+ msgid "Then press Enter (Ctrl+C to cancel) "
109
+ msgstr "Zatim pritisnite Enter (Ctrl+C za prekid) "
110
+
111
+ #: src/id2pdf/scanner.py:71
112
+ msgid ""
113
+ "Put the card on the glass front side down, anywhere (text upright), and "
114
+ "close the lid."
115
+ msgstr ""
116
+ "Stavite osobnu prednjom stranom prema staklu, bilo gdje (tekst uspravno), i "
117
+ "zatvorite poklopac."
118
+
119
+ #: src/id2pdf/scanner.py:73
120
+ msgid ""
121
+ "Put the card on the glass back side down, anywhere (text upright), and close "
122
+ "the lid."
123
+ msgstr ""
124
+ "Stavite osobnu stražnjom stranom prema staklu, bilo gdje (tekst uspravno), i "
125
+ "zatvorite poklopac."
126
+
127
+ #: src/id2pdf/scanner.py:75
128
+ msgid "Looking for the card on the glass..."
129
+ msgstr "Tražim osobnu na staklu..."
130
+
131
+ #: src/id2pdf/scanner.py:80
132
+ msgid ""
133
+ "No card found. Put it away from the edge of the glass, flat, and close the "
134
+ "lid."
135
+ msgstr ""
136
+ "Osobna nije pronađena. Stavite je dalje od ruba stakla, ravno, i zatvorite "
137
+ "poklopac."
138
+
139
+ #: src/id2pdf/scanner.py:82
140
+ msgid "There is more than one card on the glass, taking the largest."
141
+ msgstr "Na staklu je više kartica, uzimam najveću."
142
+
143
+ #: src/id2pdf/scanner.py:91
144
+ msgid "Scanning the card..."
145
+ msgstr "Skeniram osobnu..."
146
+
147
+ #: src/id2pdf/scanner.py:118
148
+ #, python-brace-format
149
+ msgid "Put page {n} of {pages} face down on the glass and close the lid."
150
+ msgstr ""
151
+ "Stavite {n}. od {pages} stranica licem prema staklu i zatvorite poklopac."
152
+
153
+ #: src/id2pdf/scanner.py:119
154
+ msgid "Scanning..."
155
+ msgstr "Skeniram..."
156
+
157
+ #: src/id2pdf/scanner.py:126
158
+ msgid "Put the card on the glass as usual and close the lid."
159
+ msgstr "Stavite osobnu na staklo kao inače i zatvorite poklopac."
160
+
161
+ #: src/id2pdf/scanner.py:131
162
+ #, python-brace-format
163
+ msgid "Glass: {width:.1f} x {height:.1f} mm, lid (median grey): {lid:.0f}"
164
+ msgstr ""
165
+ "Staklo: {width:.1f} x {height:.1f} mm, poklopac (medijan sive): {lid:.0f}"
166
+
167
+ #: src/id2pdf/scanner.py:136
168
+ #, python-brace-format
169
+ msgid "Objects found: {count} (largest first; a card is 85.6 x 54 mm)"
170
+ msgstr "Pronađeno objekata: {count} (najveći prvi; osobna je 85.6 x 54 mm)"
171
+
172
+ #: src/id2pdf/scanner.py:137
173
+ msgid "shape"
174
+ msgstr "oblik"
175
+
176
+ #: src/id2pdf/scanner.py:137
177
+ msgid "size"
178
+ msgstr "veličina"
179
+
180
+ #: src/id2pdf/scanner.py:137
181
+ msgid "too small"
182
+ msgstr "premalo"
183
+
184
+ #: src/id2pdf/scanner.py:142
185
+ #, python-brace-format
186
+ msgid "rejected ({reason})"
187
+ msgstr "odbijeno ({reason})"
188
+
189
+ #: src/id2pdf/scanner.py:146
190
+ #, python-brace-format
191
+ msgid "CARD, without its shadow: {size} mm"
192
+ msgstr "OSOBNA, bez sjene: {size} mm"
193
+
194
+ #: src/id2pdf/scanner.py:150
195
+ #, python-brace-format
196
+ msgid ""
197
+ "{long:6.1f} x {short:5.1f} mm ratio {ratio:4.2f} centre ({where}) mm "
198
+ "angle {angle:5.1f} {verdict}"
199
+ msgstr ""
200
+ "{long:6.1f} x {short:5.1f} mm omjer {ratio:4.2f} sredina ({where}) mm kut "
201
+ "{angle:5.1f} {verdict}"
202
+
203
+ #: src/id2pdf/scanner.py:157
204
+ #, python-brace-format
205
+ msgid ""
206
+ "The scan is saved only on this computer: {path} (it shows the card, do not "
207
+ "share it)."
208
+ msgstr ""
209
+ "Sken je spremljen samo na ovom računalu: {path} (na njemu je osobna, ne "
210
+ "šaljite ga)."