Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/generic/_appearance_stream.py: 15%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

249 statements  

1from __future__ import annotations 

2 

3import re 

4from dataclasses import dataclass 

5from enum import IntEnum 

6from io import BytesIO 

7from typing import TYPE_CHECKING, Any, cast 

8 

9from .._codecs import fill_from_encoding 

10from .._codecs.core_font_metrics import CORE_FONT_METRICS 

11from .._font import Font 

12from .._utils import logger_warning 

13from ..constants import AnnotationDictionaryAttributes, BorderStyles, FieldDictionaryAttributes, PageAttributes 

14from ..errors import PdfReadError 

15from ..generic import ( 

16 DecodedStreamObject, 

17 DictionaryObject, 

18 IndirectObject, 

19 NameObject, 

20 NumberObject, 

21 RectangleObject, 

22) 

23from ..generic._base import ByteStringObject, TextStringObject 

24 

25if TYPE_CHECKING: 

26 from pypdf._writer import PdfWriter 

27 

28 from .._page import PageObject 

29 

30DEFAULT_FONT_SIZE_IN_MULTILINE = 12 

31 

32 

33@dataclass 

34class BaseStreamConfig: 

35 """A container representing the basic layout of an appearance stream.""" 

36 rectangle: RectangleObject | tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0) 

37 border_width: int = 1 # The width of the border in points 

38 border_style: str = BorderStyles.SOLID 

39 

40 

41class BaseStreamAppearance(DecodedStreamObject): 

42 """A class representing the very base of an appearance stream, that is, a rectangle and a border.""" 

43 

44 def __init__(self, layout: BaseStreamConfig | None) -> None: 

45 """ 

46 Takes the appearance stream layout as an argument. 

47 

48 Args: 

49 layout: The basic layout parameters. 

50 """ 

51 super().__init__() 

52 self._layout = layout or BaseStreamConfig() 

53 self[NameObject("/Type")] = NameObject("/XObject") 

54 self[NameObject("/Subtype")] = NameObject("/Form") 

55 self[NameObject("/BBox")] = RectangleObject(self._layout.rectangle) 

56 

57 

58class TextAlignment(IntEnum): 

59 """Defines the alignment options for text within a form field's appearance stream.""" 

60 

61 LEFT = 0 

62 CENTER = 1 

63 RIGHT = 2 

64 

65 

66class TextStreamAppearance(BaseStreamAppearance): 

67 """ 

68 A class representing the appearance stream for a text-based form field. 

69 

70 This class generates the content stream (the `ap_stream_data`) that dictates 

71 how text is rendered within a form field's bounding box. It handles properties 

72 like font, font size, color, multiline text, and text selection highlighting. 

73 """ 

74 

75 def _scale_text( 

76 self, 

77 font: Font, 

78 font_size: float, 

79 leading_factor: float, 

80 field_width: float, 

81 field_height: float, 

82 paragraphs: list[str], 

83 min_font_size: float, 

84 font_size_step: float = 0.2 

85 ) -> tuple[list[tuple[float, str]], float]: 

86 """ 

87 Takes a piece of text and scales it to field_width or field_height, given font_name 

88 and font_size. Wraps text where necessary. 

89 

90 Args: 

91 font: The font to be used. 

92 font_size: The font size in points. 

93 leading_factor: The line distance. 

94 field_width: The width of the field in which to fit the text. 

95 field_height: The height of the field in which to fit the text. 

96 paragraphs: The text paragraphs to fit with the field. 

97 min_font_size: The minimum font size at which to scale the text. 

98 font_size_step: The amount by which to decrement font size per step while scaling. 

99 

100 Returns: 

101 The text in the form of list of tuples, each tuple containing the length of a line 

102 and its contents, and the font_size for these lines and lengths. 

103 """ 

104 wrapped_lines = [] 

105 current_line_words: list[str] = [] 

106 current_line_width: float = 0 

107 space_width = font.space_width * font_size / 1000 

108 for paragraph in paragraphs: 

109 if not paragraph.strip(): 

110 wrapped_lines.append((0.0, "")) 

111 continue 

112 words = paragraph.split(font.space_char) 

113 for i, word in enumerate(words): 

114 word_width = font.get_text_width(word) * font_size / 1000 

115 test_width = current_line_width + word_width + (space_width if i else 0) 

116 if test_width > field_width and current_line_words: 

117 wrapped_lines.append((current_line_width, font.space_char.join(current_line_words))) 

118 current_line_words = [word] 

119 current_line_width = word_width 

120 elif not current_line_words and word_width > field_width: 

121 wrapped_lines.append((word_width, word)) 

122 current_line_words = [] 

123 current_line_width = 0 

124 else: 

125 if current_line_words: 

126 current_line_width += space_width 

127 current_line_words.append(word) 

128 current_line_width += word_width 

129 if current_line_words: 

130 wrapped_lines.append((current_line_width, font.space_char.join(current_line_words))) 

131 current_line_words = [] 

132 current_line_width = 0 

133 # Estimate total height. 

134 estimated_total_height = font_size + (len(wrapped_lines) - 1) * leading_factor * font_size 

135 if estimated_total_height > field_height: 

136 # Text overflows height; Retry with smaller font size. 

137 new_font_size = font_size - font_size_step 

138 if new_font_size >= min_font_size: 

139 return self._scale_text( 

140 font, 

141 new_font_size, 

142 leading_factor, 

143 field_width, 

144 field_height, 

145 paragraphs, 

146 min_font_size, 

147 font_size_step 

148 ) 

149 return wrapped_lines, round(font_size, 1) 

150 

151 def _generate_appearance_stream_data( 

152 self, 

153 text: str, 

154 selection: list[str] | None , 

155 font: Font, 

156 font_name: str = "/Helv", 

157 font_size: float = 0.0, 

158 font_color: str = "0 g", 

159 is_multiline: bool = False, 

160 alignment: TextAlignment = TextAlignment.LEFT, 

161 is_comb: bool = False, 

162 max_length: int | None = None 

163 ) -> bytes: 

164 """ 

165 Generates the raw bytes of the PDF appearance stream for a text field. 

166 

167 This private method assembles the PDF content stream operators to draw 

168 the provided text within the specified rectangle. It handles text positioning, 

169 font application, color, and special formatting like selected text. 

170 

171 Args: 

172 text: The text to be rendered in the form field. 

173 selection: An optional list of strings that should be highlighted as selected. 

174 font: The font to use. 

175 font_name: The name of the font resource to use (e.g., "/Helv"). 

176 font_size: The font size. If 0, it is automatically calculated 

177 based on whether the field is multiline or not. 

178 font_color: The color to apply to the font, represented as a PDF 

179 graphics state string (e.g., "0 g" for black). 

180 is_multiline: A boolean indicating if the text field is multiline. 

181 alignment: Text alignment, can be TextAlignment.LEFT, .RIGHT, or .CENTER. 

182 is_comb: Boolean that designates fixed-length fields, where every character 

183 fills one "cell", such as in a postcode. 

184 max_length: Used if is_comb is set. The maximum number of characters for a fixed- 

185 length field. 

186 

187 Returns: 

188 A byte string containing the PDF content stream data. 

189 

190 """ 

191 rectangle = self._layout.rectangle 

192 if isinstance(rectangle, tuple): 

193 rectangle = RectangleObject(rectangle) 

194 leading_factor = (font.font_descriptor.bbox[3] - font.font_descriptor.bbox[1]) / 1000.0 

195 

196 # Set margins based on border width and style, but never less than 1 point 

197 factor = 2 if self._layout.border_style in {"/B", "/I"} else 1 

198 margin = max(self._layout.border_width * factor, 1) 

199 field_height = rectangle.height - 2 * margin 

200 field_width = rectangle.width - 4 * margin 

201 

202 reverse_cmap, encoding_cmap = font._get_typographic_maps() 

203 

204 def _unicode_to_glyph_id(text: str, reverse_cmap: dict[str, str]) -> str: 

205 return "".join(reverse_cmap.get(character, character) for character in text) 

206 

207 def _glyph_id_to_bytes(glyphs: str, encoding_cmap: dict[str, bytes]) -> list[bytes]: 

208 return [encoding_cmap.get( 

209 glyph_id, bytes((ord(glyph_id),)) if ord(glyph_id) < 256 else b"?" 

210 ) for glyph_id in glyphs] 

211 

212 # If font_size is 0, apply the logic for multiline or large-as-possible font 

213 if font_size == 0: 

214 min_font_size = 4.0 # The mininum font size 

215 if selection: # Don't wrap text when dealing with a /Ch field, in order to prevent problems 

216 is_multiline = False # with matching "selection" with "line" later on. 

217 if is_multiline: 

218 font_size = DEFAULT_FONT_SIZE_IN_MULTILINE 

219 glyph_paragraphs = [ 

220 _unicode_to_glyph_id(paragraph, reverse_cmap) for paragraph in text.splitlines() 

221 ] 

222 lines, font_size = self._scale_text( 

223 font, 

224 font_size, 

225 leading_factor, 

226 field_width, 

227 field_height, 

228 glyph_paragraphs, 

229 min_font_size 

230 ) 

231 else: 

232 max_vertical_size = field_height / leading_factor 

233 glyphs = _unicode_to_glyph_id(text, reverse_cmap) 

234 text_width_unscaled = font.get_text_width(glyphs) / 1000 

235 max_horizontal_size = field_width / (text_width_unscaled or 1) 

236 font_size = round(max(min(max_vertical_size, max_horizontal_size), min_font_size), 1) 

237 lines = [(text_width_unscaled * font_size, glyphs)] 

238 elif is_comb: 

239 if max_length and len(text) > max_length: 

240 logger_warning( 

241 ( 

242 "Length of text %(text)s exceeds maximum length (%(max_length)d) " 

243 "of field, input truncated." 

244 ), 

245 source=__name__, 

246 text=text, 

247 max_length=max_length, 

248 ) 

249 # We act as if each character is one line, because we draw it separately later on 

250 lines = [] 

251 for index, char in enumerate(text): 

252 if index < (max_length or len(text)): 

253 glyphs = _unicode_to_glyph_id(char, reverse_cmap) 

254 lines.append((font.get_text_width(glyphs) * font_size / 1000, glyphs)) 

255 else: 

256 lines = [] 

257 for line in text.splitlines(): 

258 glyphs = _unicode_to_glyph_id(line, reverse_cmap) 

259 lines.append((font.get_text_width(glyphs) * font_size / 1000, glyphs)) 

260 

261 # Set the vertical offset 

262 if is_multiline: 

263 y_offset = rectangle.height + margin - font.font_descriptor.bbox[3] * font_size / 1000.0 

264 else: 

265 y_offset = margin + ((field_height - font.font_descriptor.ascent * font_size / 1000) / 2) 

266 default_appearance = f"{font_name} {font_size} Tf {font_color}" 

267 

268 ap_stream = ( 

269 f"q\n/Tx BMC \nq\n{2 * margin} {margin} {field_width} {field_height} " 

270 f"re\nW\nBT\n{default_appearance}\n" 

271 ).encode() 

272 current_x_pos: float = 0 # Initial virtual position within the text object. 

273 

274 for line_number, (line_width, line) in enumerate(lines): 

275 if selection and line in _unicode_to_glyph_id("".join(selection), reverse_cmap): 

276 # Might be improved, but cannot find how to get fill working => replaced with lined box 

277 ap_stream += ( 

278 f"1 {y_offset - (line_number * font_size * leading_factor) - 1} " 

279 f"{rectangle.width - 2} {font_size + 2} re\n" 

280 f"0.5 0.5 0.5 rg s\n{default_appearance}\n" 

281 ).encode() 

282 

283 # Calculate the desired absolute starting X for the current line 

284 desired_abs_x_start: float = 0 

285 if is_comb and max_length: 

286 # Calculate the width of a cell for one character 

287 cell_width = rectangle.width / max_length 

288 # Space from the left edge of the cell to the character's baseline start 

289 # line_width here is the *actual* character width in points for the single character 'line' 

290 centering_offset_in_cell = (cell_width - line_width) / 2 

291 # Absolute start X = (Cell Index, i.e., line_number * Cell Width) + Centering Offset 

292 desired_abs_x_start = (line_number * cell_width) + centering_offset_in_cell 

293 elif alignment == TextAlignment.RIGHT: 

294 desired_abs_x_start = rectangle.width - margin * 2 - line_width 

295 elif alignment == TextAlignment.CENTER: 

296 desired_abs_x_start = (rectangle.width - line_width) / 2 

297 else: # Left aligned; default 

298 desired_abs_x_start = margin * 2 

299 # Calculate x_rel_offset: how much to move from the current_x_pos 

300 # to reach the desired_abs_x_start. 

301 x_rel_offset = desired_abs_x_start - current_x_pos 

302 

303 # Y-offset: 

304 y_rel_offset: float = 0 

305 if line_number == 0: 

306 y_rel_offset = y_offset # Initial vertical position 

307 elif is_comb: 

308 y_rel_offset = 0.0 # DO NOT move vertically for subsequent characters 

309 else: 

310 y_rel_offset = - font_size * leading_factor # Move down by line height 

311 

312 # Td is a relative translation (Tx and Ty). 

313 # It updates the current text position. 

314 ap_stream += f"{x_rel_offset} {y_rel_offset} Td\n".encode() 

315 # Update current_x_pos based on the Td operation for the next iteration. 

316 # This is the X position where the *current line* will start. 

317 current_x_pos = desired_abs_x_start 

318 

319 encoded_line = _glyph_id_to_bytes(line, encoding_cmap) 

320 if any(len(c) >= 2 for c in encoded_line): 

321 ap_stream += b"<" + (b"".join(encoded_line)).hex().encode() + b"> Tj\n" 

322 else: 

323 ap_stream += b"(" + b"".join(encoded_line) + b") Tj\n" 

324 ap_stream += b"ET\nQ\nEMC\nQ\n" 

325 

326 return ap_stream 

327 

328 def __init__( 

329 self, 

330 layout: BaseStreamConfig | None = None, 

331 text: str = "", 

332 selection: list[str] | None = None, 

333 font: Font | None = None, 

334 font_resource: DictionaryObject | IndirectObject | None = None, 

335 font_name: str = "/Helv", 

336 font_size: float = 0.0, 

337 font_color: str = "0 g", 

338 is_multiline: bool = False, 

339 alignment: TextAlignment = TextAlignment.LEFT, 

340 is_comb: bool = False, 

341 max_length: int | None = None 

342 ) -> None: 

343 """ 

344 Initializes a TextStreamAppearance object. 

345 

346 This constructor creates a new PDF stream object configured as an XObject 

347 of subtype Form. It uses the `_appearance_stream_data` method to generate 

348 the content for the stream. 

349 

350 Args: 

351 layout: The basic layout parameters. 

352 text: The text to be rendered in the form field. 

353 selection: An optional list of strings that should be highlighted as selected. 

354 font: A Font object. Falls back to Type 1 Helvetica if not given. 

355 font_resource: An optional variable that represents a PDF font dictionary. Falls back 

356 to Type 1 Helvetica if not given. 

357 font_name: The name of the font resource, e.g., "/Helv". 

358 font_size: The font size. If 0, it's auto-calculated. 

359 font_color: The font color string. 

360 is_multiline: A boolean indicating if the text field is multiline. 

361 alignment: Text alignment, can be TextAlignment.LEFT, .RIGHT, or .CENTER. 

362 is_comb: Boolean that designates fixed-length fields, where every character 

363 fills one "cell", such as in a postcode. 

364 max_length: Used if is_comb is set. The maximum number of characters for a fixed- 

365 length field. 

366 

367 """ 

368 super().__init__(layout) 

369 

370 if not font or not font_resource: 

371 font_name = "/Helv" 

372 core_font_metrics = CORE_FONT_METRICS["Helvetica"] 

373 win_ansi_encoding_list = fill_from_encoding("cp1252") # WinAnsiEncoding 

374 font = Font( 

375 name="Helvetica", 

376 character_map={}, 

377 encoding=dict(zip(range(256), win_ansi_encoding_list)), 

378 sub_type="Type1", 

379 font_descriptor=core_font_metrics.font_descriptor, 

380 character_widths={ 

381 chr(code): core_font_metrics.character_widths[value] for code, value in enumerate( 

382 win_ansi_encoding_list 

383 ) if value in core_font_metrics.character_widths 

384 }, 

385 ) 

386 font.character_widths["default"] = core_font_metrics.character_widths["default"] 

387 font_resource = font.as_font_resource() 

388 

389 ap_stream_data = self._generate_appearance_stream_data( 

390 text, 

391 selection, 

392 font, 

393 font_name=font_name, 

394 font_size=font_size, 

395 font_color=font_color, 

396 is_multiline=is_multiline, 

397 alignment=alignment, 

398 is_comb=is_comb, 

399 max_length=max_length 

400 ) 

401 

402 self.set_data(ByteStringObject(ap_stream_data)) 

403 self[NameObject("/Length")] = NumberObject(len(ap_stream_data)) 

404 # Update Resources with font information 

405 self[NameObject("/Resources")] = DictionaryObject({ 

406 NameObject("/Font"): DictionaryObject({ 

407 NameObject(font_name): getattr(font_resource, "indirect_reference", font_resource) 

408 }) 

409 }) 

410 

411 @staticmethod 

412 def _find_annotation_font_resource( 

413 font_name: str, 

414 annotation: DictionaryObject, 

415 acro_form: DictionaryObject, 

416 text: str 

417 ) -> tuple[str, Font]: 

418 # Try to find a resource dictionary for the font by examining the annotation and, if that fails, 

419 # the AcroForm resources dictionary 

420 acro_form_resources: Any = cast( 

421 DictionaryObject, 

422 annotation.get_inherited( 

423 "/DR", 

424 acro_form.get("/DR", DictionaryObject()), 

425 ), 

426 ) 

427 acro_form_font_resources = acro_form_resources.get("/Font", DictionaryObject()) 

428 font_resource = acro_form_font_resources.get(font_name, None) 

429 if font_resource: 

430 font = Font.from_font_resource(font_resource) 

431 else: 

432 # Normally, we should have found a font resource by now. However, when a user has provided a specific 

433 # font name, we may not have found the associated font resource among the AcroForm resources. Also, in 

434 # case of the 14 Adobe Core fonts, we may be expected to construct a font resource ourselves. 

435 if font_name.removeprefix("/") not in CORE_FONT_METRICS: 

436 # Default to Helvetica if we haven't found a font resource and cannot construct one ourselves. 

437 logger_warning( 

438 "Font dictionary for %(font_name)s not found; defaulting to Helvetica.", 

439 source=__name__, 

440 font_name=font_name, 

441 ) 

442 font_name = "/Helvetica" 

443 core_font_metrics = CORE_FONT_METRICS[font_name.removeprefix("/")] 

444 font = Font( 

445 name=font_name.removeprefix("/"), 

446 character_map={}, 

447 encoding=dict(zip(range(256), fill_from_encoding("cp1252"))), # WinAnsiEncoding 

448 sub_type="Type1", 

449 font_descriptor=core_font_metrics.font_descriptor, 

450 character_widths=core_font_metrics.character_widths 

451 ) 

452 

453 # If we have found a font resource, it still might not be able to encode the text value we received. 

454 encodable = font.can_encode(text) 

455 

456 if not encodable: 

457 # If we have a font file, we can try to produce a new font resource with an encoding 

458 # that does include the necessary characters. 

459 if font.font_descriptor.font_file and font.sub_type == "TrueType": 

460 try: 

461 font = font.from_truetype_font_file(BytesIO(font.font_descriptor.font_file.get_data())) 

462 font_name = "/PYPDF1" # This means we most probably do not clash with an existing font name 

463 encodable = font.can_encode(text) 

464 except (ImportError, PdfReadError) as e: 

465 logger_warning("Unable to use embedded font for encoding: %(e)s", source=__name__, e=e) 

466 

467 if not encodable: 

468 logger_warning( 

469 ( 

470 "Text string '%(text)s' contains characters not supported by font encoding. " 

471 "This may result in text corruption. " 

472 "Consider calling writer.update_page_form_field_values with auto_regenerate=True." 

473 ), 

474 source=__name__, 

475 text=text, 

476 ) 

477 

478 return font_name, font 

479 

480 @staticmethod 

481 def _sync_appearance_stream_font_resources( 

482 writer: PdfWriter, 

483 font_name: str, 

484 font: Font, 

485 target_resource_dict: DictionaryObject, 

486 page: PageObject | None = None 

487 ) -> IndirectObject: 

488 """ 

489 Unified helper to sync fonts from an AP stream to a target resource dictionary (e.g., AcroForm /DR). 

490 Will sync to page resources as well when page is added to the arguments. 

491 """ 

492 target_fonts = target_resource_dict.setdefault(NameObject("/Font"), DictionaryObject()).get_object() 

493 if font_name not in target_fonts: 

494 font_resource_reference = font._add_to_writer( 

495 writer, 

496 target_fonts, 

497 NameObject(font_name) 

498 ) 

499 else: 

500 font_resource_reference = target_fonts[font_name] 

501 

502 if page: 

503 page_fonts_resource = cast(DictionaryObject, page[PageAttributes.RESOURCES]).setdefault( 

504 NameObject("/Font"), DictionaryObject() 

505 ).get_object() 

506 if font_name not in page_fonts_resource: 

507 page_fonts_resource[NameObject(font_name)] = getattr( 

508 font_resource_reference, "indirect_reference", font_resource_reference 

509 ) 

510 

511 return font_resource_reference 

512 

513 @classmethod 

514 def from_text_annotation( 

515 cls, 

516 writer: PdfWriter, 

517 page: PageObject, 

518 flatten: bool, 

519 acro_form: DictionaryObject, # _root_object[CatalogDictionary.ACRO_FORM]) 

520 field: DictionaryObject, 

521 annotation: DictionaryObject, 

522 user_font_name: str = "", 

523 user_font_size: float = -1, 

524 ) -> TextStreamAppearance: 

525 """ 

526 Creates a TextStreamAppearance object from a text field annotation. 

527 

528 This class method is a factory for creating a `TextStreamAppearance` 

529 instance by extracting all necessary information (bounding box, font, 

530 text content, etc.) from the PDF field and annotation dictionaries. 

531 It respects inheritance for properties like default appearance (`/DA`). 

532 

533 Args: 

534 writer: The PdfWriter instance that we are creating text stream appearances for. 

535 page: The page that we are processing annotations for. 

536 flatten: Whether we flatten text annotations or not. If true, add new font resource 

537 to the page font resources. Otherwise, add them to the AcroForm resources. 

538 acro_form: The root AcroForm dictionary from the PDF catalog. 

539 field: The field dictionary object. 

540 annotation: The widget annotation dictionary object associated with the field. 

541 user_font_name: An optional user-provided font name to override the 

542 default. Defaults to an empty string. 

543 user_font_size: An optional user-provided font size to override the 

544 default. A value of -1 indicates no override. 

545 

546 Returns: 

547 A new `TextStreamAppearance` instance configured for the given field. 

548 

549 """ 

550 # Calculate rectangle dimensions 

551 _rectangle = cast(RectangleObject, annotation[AnnotationDictionaryAttributes.Rect]) 

552 rectangle = RectangleObject((0, 0, abs(_rectangle[2] - _rectangle[0]), abs(_rectangle[3] - _rectangle[1]))) 

553 

554 # Get default appearance dictionary from annotation 

555 default_appearance = annotation.get_inherited( 

556 AnnotationDictionaryAttributes.DA, 

557 acro_form.get(AnnotationDictionaryAttributes.DA, None), 

558 ) 

559 if not default_appearance: 

560 # Create a default appearance if none was found in the annotation 

561 default_appearance = TextStringObject("/Helv 0 Tf 0 g") 

562 else: 

563 default_appearance = default_appearance.get_object() 

564 

565 # Retrieve field text and selected values 

566 field_flags = field.get(FieldDictionaryAttributes.Ff, 0) 

567 if ( 

568 field.get(FieldDictionaryAttributes.FT, "/Tx") == "/Ch" and 

569 field_flags & FieldDictionaryAttributes.FfBits.Combo == 0 

570 ): 

571 text = "\n".join(annotation.get_inherited(FieldDictionaryAttributes.Opt, [])) 

572 selection = field.get("/V", []) 

573 if not isinstance(selection, list): 

574 selection = [selection] 

575 else: # /Tx 

576 text = field.get("/V", "") 

577 selection = [] 

578 

579 # Escape parentheses (PDF 1.7 reference, table 3.2, Literal Strings) 

580 text = text.replace("\\", "\\\\").replace("(", r"\(").replace(")", r"\)") 

581 

582 # Derive font name, size and color from the default appearance. Also set 

583 # user-provided font name and font size in the default appearance, if given. 

584 # For a font name, this presumes that we can find an associated font resource 

585 # dictionary. Uses the variable font_properties as an intermediate. 

586 # As per the PDF spec: 

587 # "At a minimum, the string [that is, default_appearance] shall include a Tf (text 

588 # font) operator along with its two operands, font and size" (Section 12.7.4.3 

589 # "Variable text" of the PDF 2.0 specification). 

590 font_properties = [prop for prop in re.split(r"\s", default_appearance) if prop] 

591 da_font_name = font_properties.pop(font_properties.index("Tf") - 2) 

592 font_size = float(font_properties.pop(font_properties.index("Tf") - 1)) 

593 font_properties.remove("Tf") 

594 font_color = " ".join(font_properties) 

595 # Determine the font name to use, prioritizing the user's input 

596 if user_font_name: 

597 font_name = user_font_name 

598 else: 

599 font_name = da_font_name 

600 # Determine the font size to use, prioritizing the user's input 

601 if user_font_size > 0: 

602 font_size = user_font_size 

603 

604 font_name, font = cls._find_annotation_font_resource(font_name, annotation, acro_form, text) 

605 

606 # Change the /DA information if we changed the font name 

607 if font_name != da_font_name: 

608 annotation[NameObject("/DA")] = TextStringObject(default_appearance.replace(da_font_name, font_name)) 

609 

610 # Synchronise font resources 

611 font_resource_reference = cls._sync_appearance_stream_font_resources( 

612 writer, 

613 font_name, 

614 font, 

615 acro_form.setdefault(NameObject("/DR"), DictionaryObject()), 

616 page if flatten else None, 

617 ) 

618 

619 # Retrieve formatting information 

620 is_comb = False 

621 max_length = None 

622 if field_flags & FieldDictionaryAttributes.FfBits.Comb: 

623 is_comb = True 

624 max_length = annotation.get("/MaxLen") 

625 is_multiline = False 

626 if field_flags & FieldDictionaryAttributes.FfBits.Multiline: 

627 is_multiline = True 

628 alignment = field.get("/Q", TextAlignment.LEFT) 

629 border_width = 1 

630 border_style = BorderStyles.SOLID 

631 if "/BS" in field: 

632 border_width = cast(DictionaryObject, field["/BS"]).get("/W", border_width) 

633 border_style = cast(DictionaryObject, field["/BS"]).get("/S", border_style) 

634 

635 # Create the TextStreamAppearance instance 

636 layout = BaseStreamConfig(rectangle=rectangle, border_width=border_width, border_style=border_style) 

637 new_appearance_stream = cls( 

638 layout, 

639 text, 

640 selection, 

641 font, 

642 font_resource_reference, 

643 font_name=font_name, 

644 font_size=font_size, 

645 font_color=font_color, 

646 is_multiline=is_multiline, 

647 alignment=alignment, 

648 is_comb=is_comb, 

649 max_length=max_length 

650 ) 

651 

652 if AnnotationDictionaryAttributes.AP in annotation: 

653 for key, value in ( 

654 cast(DictionaryObject, annotation[AnnotationDictionaryAttributes.AP]).get("/N", {}).items() 

655 ): 

656 if key in {"/BBox", "/Length", "/Subtype", "/Type", "/Filter"}: 

657 continue 

658 # Don't overwrite font resources added by TextAppearanceStream.__init__ 

659 if key == "/Resources": 

660 if "/Font" not in value: 

661 value.get_object()[NameObject("/Font")] = DictionaryObject() 

662 value["/Font"].get_object()[NameObject(font_name)] = getattr( 

663 font_resource_reference, "indirect_reference", font_resource_reference 

664 ) 

665 else: 

666 new_appearance_stream[key] = value 

667 

668 return new_appearance_stream