Coverage for papersize/__init__.py: 99%

80 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-22 09:43 +0100

1# Copyright Louis Paternault 2014-2025 

2# 

3# This program is free software: you can redistribute it and/or modify 

4# it under the terms of the GNU General Public License as published by 

5# the Free Software Foundation, either version 3 of the License, or 

6# (at your option) any later version. 

7# 

8# This program is distributed in the hope that it will be useful, 

9# but WITHOUT ANY WARRANTY; without even the implied warranty of 

10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

11# GNU General Public License for more details. 

12# 

13# You should have received a copy of the GNU General Public License 

14# along with this program. If not, see <http://www.gnu.org/licenses/>. 

15 

16"""Paper size related data and functions 

17 

18In this module: 

19 

20- the default unit (input and output) is point (``pt``); 

21- every numbers are returned as :class:`decimal.Decimal` objects. 

22""" 

23 

24import gettext 

25import importlib.resources 

26import re 

27from decimal import Decimal 

28 

29__version__ = "1.6.1" 

30__AUTHOR__ = "Louis Paternault (spalax@gresille.org)" 

31__COPYRIGHT__ = "(C) 2014-2025 Louis Paternault. GNU GPL 3 or later." 

32 

33SIZES = { 

34 # http://www.printernational.org/iso-paper-sizes.php 

35 "4a0": "1682mm x 2378mm", 

36 "2a0": "1189mm x 1682mm", 

37 "a0": "841mm x 1189mm", 

38 "a1": "594mm x 841mm", 

39 "a2": "420mm x 594mm", 

40 "a3": "297mm x 420mm", 

41 "a4": "210mm x 297mm", 

42 "a5": "148mm x 210mm", 

43 "a6": "105mm x 148mm", 

44 "a7": "74mm x 105mm", 

45 "a8": "52mm x 74mm", 

46 "a9": "37mm x 52mm", 

47 "a10": "26mm x 37mm", 

48 "b0": "1000mm x 1414mm", 

49 "b1": "707mm x 1000mm", 

50 "b2": "500mm x 707mm", 

51 "b3": "353mm x 500mm", 

52 "b4": "250mm x 352mm", 

53 "b5": "176mm x 250mm", 

54 "b6": "125mm x 176mm", 

55 "b7": "88mm x 125mm", 

56 "b8": "62mm x 88mm", 

57 "b9": "44mm x 62mm", 

58 "b10": "31mm x 44mm", 

59 "a2extra": "445mm x 619mm", 

60 "a3extra": "322mm x 445mm", 

61 "a3super": "305mm x 508mm", 

62 "supera3": "305mm x 487mm", 

63 "a4extra": "235mm x 322mm", 

64 "a4super": "229mm x 322mm", 

65 "supera4": "227mm x 356mm", 

66 "a4long": "210mm x 348mm", 

67 "a5extra": "173mm x 235mm", 

68 "sob5extra": "202mm x 276mm", 

69 # http://www.engineeringtoolbox.com/office-paper-sizes-d_213.html 

70 "letter": "8.5in x 11in", 

71 "legal": "8.5in x 14in", 

72 "executive": "7in x 10in", 

73 "tabloid": "11in x 17in", 

74 "statement": "5.5in x 8.5in", 

75 "halfletter": "5.5in x 8.5in", 

76 "folio": "8in x 13in", 

77 # http://hplipopensource.com/hplip-web/tech_docs/page_sizes.html 

78 "flsa": "8.5in x 13in", 

79 # http://www.coding-guidelines.com/numbers/ndb/units/area.txt 

80 "flse": "8.5in x 13in", 

81 # http://jexcelapi.sourceforge.net/resources/javadocs/2_6_10/docs/jxl/format/PaperSize.html 

82 "note": "8.5in x 11in", 

83 "11x17": "11in x 17in", 

84 "10x14": "10in x 14in", 

85 # https://en.wikipedia.org/w/index.php?title=Paper_size&oldid=814180250 

86 "c0": "917mm × 1297mm", 

87 "c1": "648mm × 917mm", 

88 "c2": "458mm × 648mm", 

89 "c3": "324mm × 458mm", 

90 "c4": "229mm × 324mm", 

91 "c5": "162mm × 229mm", 

92 "c6": "114mm × 162mm", 

93 "c7": "81mm × 114mm", 

94 "c8": "57mm × 81mm", 

95 "c9": "40mm × 57mm", 

96 "c10": "28mm × 40mm", 

97 "juniorlegal": "5in × 8in", 

98 "memo": "halfletter", 

99 "governmentletter": "8in × 10in", 

100 "governmentlegal": "8.5in × 13in", 

101 "ledger": "17in x 11in", 

102 "arch1": "9in x 12in", 

103 "arch2": "12in x 18in", 

104 "arch3": "18in x 24in", 

105 "arch4": "24in x 36in", 

106 "arch5": "30in x 42in", 

107 "arch6": "36in x 48in", 

108 "archa": "arch1", 

109 "archb": "arch2", 

110 "archc": "arch3", 

111 "archd": "arch4", 

112 "arche1": "arch5", 

113 "arche": "arch6", 

114 "arche2": "26in x 38in", 

115 "arche3": "27in x 39in", 

116} 

117"""Dictionary of named sizes. 

118 

119Keys are names (e.g. ``a4``, ``letter``) and values are strings, 

120human-readable, and parsable by :func:`parse_papersize` (e.g. ``21cm x 

12129.7cm``). 

122""" 

123 

124# Source: http://en.wikibooks.org/wiki/LaTeX/Lengths 

125UNITS = { 

126 "": Decimal("1"), # Default is point (pt) 

127 "pt": Decimal("1"), # point 

128 "mm": Decimal("7227") / Decimal("2540"), # millimeter 

129 "cm": Decimal("7227") / Decimal("254"), # centimeter 

130 "in": Decimal("72.27"), # inch 

131 "bp": Decimal("803") / Decimal("800"), # big point 

132 "pc": Decimal("12"), # pica 

133 "dd": Decimal("1238") / Decimal("1157"), # didot 

134 "cc": Decimal("14856") / Decimal("1157"), # cicero 

135 "nd": Decimal("685") / Decimal("642"), # new didot 

136 "nc": Decimal("1370") / Decimal("107"), # new cicero 

137 "sp": Decimal("1") / Decimal("65536"), # scaled point 

138} 

139"""Dictionary of units. 

140 

141Keys are unit abbreviation (e.g. ``pt`` or ``cm``), and values are their value 

142in points (e.g. ``UNITS['pt']`` is 1, ``UNITS['pc']``] is 12), as 

143:class:`decimal.Decimal` objects. 

144""" 

145 

146 

147def _(text): 

148 """Dummy function to mark strings as translatable by gettext.""" 

149 return text 

150 

151 

152UNITS_HELP = { 

153 "pt": _("point (desktop publishing point: 1/72 inch, or about 0.353mm)"), 

154 "mm": _("millimeter"), 

155 "cm": _("centimeter"), 

156 "in": _("inch (exactly 25.4 mm)"), 

157 "bp": _("big point (25.4/72 mm)"), 

158 "pc": _("pica (12 points, or 1/6 inch)"), 

159 "dd": _("Didot point (1238 pt = 1157 dd)"), 

160 "cc": _("cicero (12 Didot points)"), 

161 "nd": _("new Didot (3/8 mm)"), 

162 "nc": _("new cicero (12 new Didot points)"), 

163 "sp": _("scaled point (1/2^16 pt)"), 

164} 

165"""Human description of each unit. 

166 

167Keys are unit abbreviation (e.g. ``pt`` or ``cm``), 

168and values are strings explaining the meaning of this unit. 

169You can use it to list and explain to your users the available units. 

170 

171Note that the descriptions are :ref:`translated <i18n>`. 

172""" 

173 

174SIZES_HELP = { 

175 # http://www.printernational.org/iso-paper-sizes.php 

176 "4A0": _("168.2cm x 237.8cm (ISO 216, four times an A0)"), 

177 "2A0": _("118.9cm x 168.2cm (ISO 216, twice an A0)"), 

178 "A0": _( 

179 "84.1cm x 1189cm (ISO 216, has an aspect ratio of √2, and an area of 1 m²)" 

180 ), 

181 "A1": _("59.4cm x 84.1cm (ISO 216, half an A0)"), 

182 "A2": _("42cm x 59.4cm (ISO 216, half an A1)"), 

183 "A3": _("29.7cm x 42cm (ISO 216, half an A2)"), 

184 "A4": _("21cm x 29.1cm (ISO 216, half an A3)"), 

185 "A5": _("14.8cm x 21cm (ISO 216, half an A4)"), 

186 "A6": _("10.5cm x 14.8cm (ISO 216, half an A5)"), 

187 "A7": _("7.4cm x 10.5cm (ISO 216, half an A6)"), 

188 "A8": _("5.2cm x 7.4cm (ISO 216, half an A7)"), 

189 "A9": _("3.7cm x 5.2cm (ISO 216, half an A8)"), 

190 "A10": _("2.6cm x 3.7cm (ISO 216, half an A9)"), 

191 "B0": _("100cm x 141.4cm (ISO 216)"), 

192 "B1": _("70.7cm x 100cm (ISO 216, half a B0)"), 

193 "B2": _("50cm x 70.7cm (ISO 216, half a B1)"), 

194 "B3": _("35.3cm x 50cm (ISO 216, half a B2)"), 

195 "B4": _("25cm x 35.2cm (ISO 216, half a B3)"), 

196 "B5": _("17.6cm x 25cm (ISO 216, half a B4)"), 

197 "B6": _("12.5cm x 17.6cm (ISO 216, half a B5)"), 

198 "B7": _("8.8cm x 12.5cm (ISO 216, half a B6)"), 

199 "B8": _("6.2cm x 8.8cm (ISO 216, half a B7)"), 

200 "B9": _("4.4cm x 6.2cm (ISO 216, half a B8)"), 

201 "B10": _("3.1cm x 4.4cm (ISO 216, half a B9)"), 

202 "A2extra": _("445mm x 619mm"), 

203 "A3extra": _("322mm x 445mm"), 

204 "A3super": _("305mm x 508mm"), 

205 "superA3": _("305mm x 487mm"), 

206 "A4extra": _("235mm x 322mm"), 

207 "A4super": _("229mm x 322mm"), 

208 "superA4": _("227mm x 356mm"), 

209 "A4long": _("210mm x 348mm"), 

210 "A5extra": _("173mm x 235mm"), 

211 "sob5extra": _("202mm x 276mm"), 

212 # http://www.engineeringtoolbox.com/office-paper-sizes-d_215.html 

213 "letter": _("8.5in x 11in"), 

214 "legal": _("8.5in x 14in"), 

215 "executive": _("7in x 10in"), 

216 "tabloid": _("11in x 17in"), 

217 "statement": _("5.5in x 8.5in"), 

218 "halfletter": _("5.5in x 8.5in"), 

219 "folio": _("8in x 13in"), 

220 # http://hplipopensource.com/hplip-web/tech_docs/page_sizes.html 

221 "flsa": _("8.5in x 13in"), 

222 # http://www.coding-guidelines.com/numbers/ndb/units/area.txt 

223 "flse": _("8.5in x 13in"), 

224 # http://jexcelapi.sourceforge.net/resources/javadocs/2_6_10/docs/jxl/format/PaperSize.html 

225 "note": _("8.5in x 11in"), 

226 "11x17": _("11in x 17in"), 

227 "10x14": _("10in x 14in"), 

228 # https://en.wikipedia.org/w/index.php?title=Paper_size&oldid=814180250 

229 "C0": _("91.7cm × 129.7cm (ISO 269)"), 

230 "C1": _("64.8cm × 91.7cm (ISO 269, half a C0)"), 

231 "C2": _("45.8cm × 64.8cm (ISO 269, half a C1)"), 

232 "C3": _("32.4cm × 45.8cm (ISO 269, half a C2)"), 

233 "C4": _("22.9cm × 32.4cm (ISO 269, half a C3)"), 

234 "C5": _("16.2cm × 22.9cm (ISO 269, half a C4)"), 

235 "C6": _("11.4cm × 16.2cm (ISO 269, half a C5)"), 

236 "C7": _("8.1cm × 11.4cm (ISO 269, half a C6)"), 

237 "C8": _("5.7cm × 8.1cm (ISO 269, half a C7)"), 

238 "C9": _("4cm × 5.7cm (ISO 269, half a C8)"), 

239 "C10": _("2.8cm × 4cm (ISO 269, half a C9)"), 

240 "juniorlegal": _("5in × 8in"), 

241 "memo": _("synonym for halfletter"), 

242 "governmentletter": _("8in × 10in"), 

243 "governmentlegal": _("8.5in × 13in"), 

244 "ledger": _("17in x 11in"), 

245 "Arch1": _("9in x 12in (architectural size)"), 

246 "Arch2": _("12in x 18in (architectural size)"), 

247 "Arch3": _("18in x 24in (architectural size)"), 

248 "Arch4": _("24in x 36in (architectural size)"), 

249 "Arch5": _("30in x 42in (architectural size)"), 

250 "Arch6": _("36in x 48in (architectural size)"), 

251 "ArchA": _("other name for Arch1 (architectural size)"), 

252 "ArchB": _("other name for Arch2 (architectural size)"), 

253 "ArchC": _("other name for Arch3 (architectural size)"), 

254 "ArchD": _("other name for Arch4 (architectural size)"), 

255 "ArchE1": _("other name for Arch5 (architectural size)"), 

256 "ArchE": _("other name for Arch6 (architectural size)"), 

257 "ArchE2": _("26in x 38in (architectural size)"), 

258 "ArchE3": _("27in x 39in (architectural size)"), 

259} 

260"""Human description of each paper size. 

261 

262Keys are size abbreviation (e.g. ``A4`` or ``letter``), 

263and values are strings explaining the meaning of this size. 

264You can use it to list and explain to your users the available paper sizes. 

265 

266For historical reasons, keys of ``SIZES`` are lower cases, while keys of ``SIZES_HELP`` are not. 

267But case aside, those dictionaries contain exactly the same set of keys. 

268 

269Note that the descriptions are :ref:`translated <i18n>`. 

270""" 

271 

272PORTRAIT = True 

273"""Constant corresponding to the portrait orientation 

274 

275That is, height greater than width. 

276""" 

277 

278LANDSCAPE = False 

279"""Constant corresponding to the landscape orientation 

280 

281That is, width greater than height. 

282""" 

283 

284__UNITS_RE = rf"""({"|".join(UNITS.keys())})""" 

285__SIZE_RE = rf"([\d.]+){__UNITS_RE}" 

286__PAPERSIZE_RE = r"^(?P<width>{size}) *[x× ]? *(?P<height>{size})$".format( 

287 size=__SIZE_RE 

288) 

289 

290__SIZE_COMPILED_RE = re.compile(f"^{__SIZE_RE}$".format("size")) 

291__PAPERSIZE_COMPILED_RE = re.compile(__PAPERSIZE_RE.format("width", "height")) 

292 

293 

294class PapersizeException(Exception): 

295 """All exceptions of this module inherit from this one.""" 

296 

297 

298class CouldNotParse(PapersizeException): 

299 """Raised when a string could not be parsed. 

300 

301 :param str string: String that could not be parsed. 

302 """ 

303 

304 def __init__(self, string): 

305 super().__init__() 

306 self.string = string 

307 

308 def __str__(self): 

309 return f"Could not parse string '{self.string}'." 

310 

311 

312class UnknownOrientation(PapersizeException): 

313 """Raised when type of argument Orientation is wrong. 

314 

315 :param obj string: Object wrongly provided as an orientation. 

316 """ 

317 

318 def __init__(self, string): 

319 super().__init__() 

320 self.string = string 

321 

322 def __str__(self): 

323 return f"'{self.string}' is not one of `papersize.PORTRAIT` or `papersize.LANDSCAPE`" 

324 

325 

326def convert_length(length, orig, dest): 

327 """Convert length from one unit to another. 

328 

329 :param decimal.Decimal length: Length to convert, as any object convertible 

330 to a :class:`decimal.Decimal`. 

331 :param str orig: Unit of ``length``, as a string which is a key of 

332 :data:`UNITS`. 

333 :param str dest: Unit in which ``length`` will be converted, as a string 

334 which is a key of :data:`UNITS`. 

335 

336 Due to floating point arithmetic, there can be small rounding errors. 

337 

338 >>> convert_length(0.1, "cm", "mm") 

339 Decimal('1.000000000000000055511151231') 

340 """ 

341 return (Decimal(UNITS[orig]) * Decimal(length)) / Decimal(UNITS[dest]) 

342 

343 

344def parse_length(string, unit="pt"): 

345 """Return a length corresponding to the string. 

346 

347 :param str string: The string to parse, as a length and a unit, for 

348 instance ``10.2cm``. 

349 :param str unit: The unit of the return value, as a key of :data:`UNITS`. 

350 :return: The length, in an unit given by the ``unit`` argument. 

351 :rtype: :class:`decimal.Decimal` 

352 

353 >>> parse_length("1cm", "mm") 

354 Decimal('1E+1') 

355 >>> parse_length("1cm", "cm") 

356 Decimal('1') 

357 >>> parse_length("10cm") 

358 Decimal('284.5275590551181102362204724') 

359 """ 

360 match = __SIZE_COMPILED_RE.match(string) 

361 if match is None: 

362 raise CouldNotParse(string) 

363 return convert_length(Decimal(match.groups()[0]), match.groups()[1], unit) 

364 

365 

366def parse_couple(string, unit="pt"): 

367 """Return a tuple of dimensions. 

368 

369 :param str string: The string to parse, as "LENGTHxLENGTH" (where LENGTH 

370 are length, parsable by :func:`parse_length`). Example: ``21cm x 

371 29.7cm``. The separator can be ``x``, ``×`` or empty, surrounded by an 

372 arbitrary number of spaces. For instance: ``2cmx3cm``, ``2cm x 3cm``, 

373 ``2cm×3cm``, ``2cm 3cm``. 

374 :rtype: :class:`tuple` 

375 :return: A tuple of :class:`decimal.Decimal`, representing the dimensions. 

376 

377 >>> parse_couple("1cm 10cm", "mm") 

378 (Decimal('1E+1'), Decimal('1E+2')) 

379 >>> parse_couple("1mm 10mm", "cm") 

380 (Decimal('0.1'), Decimal('1')) 

381 """ 

382 try: 

383 match = __PAPERSIZE_COMPILED_RE.match(string).groupdict() 

384 return (parse_length(match["width"], unit), parse_length(match["height"], unit)) 

385 except AttributeError as error: 

386 raise CouldNotParse(string) from error 

387 

388 

389def parse_papersize(string, unit="pt"): 

390 """Return the papersize corresponding to string. 

391 

392 :param str string: The string to parse. It can be either a named size (as 

393 keys of constant :data:`SIZES`), or a couple of lengths (that will be 

394 processed by :func:`parse_couple`). The named paper sizes are case 

395 insensitive. The following strings return the same size: ``a4``, 

396 ``A4``, ``21cm 29.7cm``, ``210mmx297mm``, ``21cm × 297mm``… 

397 :param str unit: The unit of the return values. 

398 :return: The paper size, as a couple of :class:`decimal.Decimal`. 

399 :rtype: :class:`tuple` 

400 

401 >>> parse_papersize("A4", "cm") 

402 (Decimal('21.00000000000000000000000000'), Decimal('29.70000000000000000000000000')) 

403 >>> parse_papersize("21cm x 29.7cm", "mm") 

404 (Decimal('210.0000000000000000000000000'), Decimal('297.0000000000000000000000000')) 

405 >>> parse_papersize("10 100") 

406 (Decimal('10'), Decimal('100')) 

407 """ 

408 if string.lower() in SIZES: 

409 return parse_papersize(SIZES[string.lower()], unit) 

410 return parse_couple(string, unit) 

411 

412 

413def is_portrait(width, height, *, strict=False, fuzzy=False, ndigits=7): 

414 """Return whether paper orientation is portrait 

415 

416 That is, height greater or equal to width. 

417 

418 :param width: Width of paper, as any sortable object. 

419 :param height: Height of paper, as any sortable object. 

420 :param bool strict: If ``False``, square format (width equals height) is considered portrait; 

421 if ``True`` square format is not considered portrait. 

422 :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits. 

423 :param int ndigits: Number of digits when using fuzzy comparison. 

424 

425 >>> is_portrait(11, 10) 

426 False 

427 >>> is_portrait(10, 10) 

428 True 

429 >>> is_portrait(10, 11) 

430 True 

431 """ 

432 if strict and is_square(width, height, fuzzy=fuzzy, ndigits=ndigits): 

433 return False 

434 if fuzzy: 

435 return round(height - width, ndigits=ndigits) >= 0 

436 return width <= height 

437 

438 

439def is_landscape(width, height, *, strict=False, fuzzy=False, ndigits=7): 

440 """Return whether paper orientation is landscape 

441 

442 That is, width greater or equal to height. 

443 

444 :param width: Width of paper, as any sortable object. 

445 :param height: Height of paper, as any sortable object. 

446 :param strict: If ``False``, square format (width equals height) is considered landscape; 

447 if ``True`` square format is not considered landscape. 

448 :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits. 

449 :param int ndigits: Number of digits when using fuzzy comparison. 

450 

451 >>> is_landscape(11, 10) 

452 True 

453 >>> is_landscape(10, 10) 

454 True 

455 >>> is_landscape(10, 11) 

456 False 

457 """ 

458 if strict and is_square(width, height): 

459 return False 

460 if fuzzy: 

461 return round(width - height, ndigits=ndigits) >= 0 

462 return height <= width 

463 

464 

465def is_square(width, height, *, fuzzy=False, ndigits=7): 

466 """Return whether paper is a square (width equals height). 

467 

468 :param width: Width of paper, as any sortable object. 

469 :param height: Height of paper, as any sortable object. 

470 :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits. 

471 :param int ndigits: Number of digits when using fuzzy comparison. 

472 

473 >>> is_square(11, 10) 

474 False 

475 >>> is_square(10, 10) 

476 True 

477 >>> is_square(10, 10.00000001, fuzzy=False) 

478 False 

479 >>> is_square(10, 10.00000001, fuzzy=True) 

480 True 

481 >>> is_square(10, 10.00000001, fuzzy=True, ndigits=10) 

482 False 

483 """ 

484 if fuzzy: 

485 return round(width - height, ndigits) == 0 

486 return width == height 

487 

488 

489def rotate(size, orientation): 

490 """Return the size, rotated if necessary to make it portrait or landscape. 

491 

492 :param tuple size: Couple paper of dimension, as sortable objects 

493 (:class:`int`, :class:`float`, :class:`decimal.Decimal`…). 

494 :param orientation: Return format, one of ``PORTRAIT`` or ``LANDSCAPE``. 

495 :return: The size, as a couple of dimensions, of the same type of the 

496 ``size`` parameter. 

497 :rtype: :class:`tuple` 

498 

499 >>> rotate((21, 29.7), PORTRAIT) 

500 (21, 29.7) 

501 >>> rotate((21, 29.7), LANDSCAPE) 

502 (29.7, 21) 

503 """ 

504 if orientation == PORTRAIT: 

505 return (min(size), max(size)) 

506 if orientation == LANDSCAPE: 

507 return (max(size), min(size)) 

508 raise UnknownOrientation(orientation) 

509 

510 

511def translation_directory(): 

512 """Return an context manager proiding a directory in which translation files are located. 

513 

514 .. versionadded:: 1.5.0 

515 """ 

516 return importlib.resources.as_file( 

517 importlib.resources.files(__package__) / "translations" 

518 )