Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions features/iss-16-advanced-text.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Feature: Issue #16 — advanced text, auto-fit & internationalization
In order to author rich, international, well-fitted text
As a developer using python-pptx-extended
I need run typography, CJK/complex-script fonts, columns, vertical
text, RTL paragraphs, overflow detection and crash-free auto-fit

Scenario: Author and read back superscript
Given a blank slide text frame with one run
When I set the run superscript
Then the run reports superscript true

Scenario: Author and read back double strikethrough
Given a blank slide text frame with one run
When I set the run strike to double
Then the run reports strike double after round-trip

Scenario: Author and read back a yellow highlight
Given a blank slide text frame with one run
When I set the run highlight to FFFF00
Then the run reports highlight FFFF00 after round-trip

Scenario: Author and read back character spacing
Given a blank slide text frame with one run
When I set the run character spacing to 2 points
Then the run reports character spacing 2 points

Scenario: East-Asian font set leaves Latin untouched
Given a blank slide text frame with one run
When I set east_asian to MS Gothic and name to Calibri
Then latin is Calibri and east_asian is MS Gothic and they are independent

Scenario: Two-column text box
Given a blank slide text frame with one run
When I set the text frame to 2 columns spaced 36 points
Then the text frame reports 2 columns after round-trip

Scenario: Vertical text direction
Given a blank slide text frame with one run
When I set the text direction to east asian vertical
Then the text frame reports east asian vertical after round-trip

Scenario: Arabic right-to-left paragraph
Given a blank slide text frame with one run
When I set the paragraph to Arabic right-to-left
Then the paragraph reports rtl true after round-trip

Scenario: Overflow detection flags oversized content
Given a tiny text frame stuffed with text
Then will_overflow reports true

Scenario: fit_text survives a single unbreakable long word
Given a tiny text frame with one very long word
When I call fit_text on it
Then no error is raised and auto_size is set

Scenario: shrink_text_to_fit eagerly reduces the font scale
Given a tiny text frame stuffed with text
When I call shrink_text_to_fit
Then the normAutofit fontScale is below 100
182 changes: 182 additions & 0 deletions features/steps/iss16.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"""Step implementations for features/iss-16-advanced-text.feature (issue #16).

Self-contained: every scenario builds an in-memory blank presentation.
"""

import io
import os

from behave import given, then, when

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import MSO_AUTO_SIZE, MSO_TEXT_DIRECTION, MSO_TEXT_STRIKE_TYPE
from pptx.util import Inches, Pt

TEST_FONT = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "..", "tests", "test_files", "calibriz.ttf"
)
TEST_FONT = os.path.abspath(TEST_FONT)


def _blank_run(context):
prs = Presentation()
s = prs.slides.add_slide(prs.slide_layouts[6])
tf = s.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(2)).text_frame
r = tf.paragraphs[0].add_run()
r.text = "Sample"
context.prs = prs
context.tf = tf
context.run = r


def _roundtrip(context):
buf = io.BytesIO()
context.prs.save(buf)
buf.seek(0)
context.prs2 = Presentation(buf)
context.tf2 = list(context.prs2.slides[0].shapes)[0].text_frame
return context.tf2


@given("a blank slide text frame with one run")
def given_blank_run(context):
_blank_run(context)


@given("a tiny text frame stuffed with text")
def given_tiny_stuffed(context):
prs = Presentation()
s = prs.slides.add_slide(prs.slide_layouts[6])
tf = s.shapes.add_textbox(Inches(0), Inches(0), Inches(1), Inches(0.3)).text_frame
tf.text = "Supercalifragilistic " * 14
for r in tf.paragraphs[0].runs:
r.font.size = Pt(18)
context.prs = prs
context.tf = tf


@given("a tiny text frame with one very long word")
def given_tiny_longword(context):
prs = Presentation()
s = prs.slides.add_slide(prs.slide_layouts[6])
tf = s.shapes.add_textbox(Inches(0), Inches(0), Inches(0.5), Inches(0.5)).text_frame
tf.text = "Supercalifragilisticexpialidocious"
context.prs = prs
context.tf = tf


@when("I set the run superscript")
def when_superscript(context):
context.run.font.superscript = True


@then("the run reports superscript true")
def then_superscript(context):
assert context.run.font.superscript is True


@when("I set the run strike to double")
def when_strike_double(context):
context.run.font.strike = MSO_TEXT_STRIKE_TYPE.DOUBLE


@then("the run reports strike double after round-trip")
def then_strike_double(context):
f2 = _roundtrip(context).paragraphs[0].runs[0].font
assert f2.strike == MSO_TEXT_STRIKE_TYPE.DOUBLE


@when("I set the run highlight to FFFF00")
def when_highlight(context):
context.run.font.highlight.rgb = RGBColor(0xFF, 0xFF, 0x00)


@then("the run reports highlight FFFF00 after round-trip")
def then_highlight(context):
f2 = _roundtrip(context).paragraphs[0].runs[0].font
assert f2.highlight.rgb == RGBColor(0xFF, 0xFF, 0x00)


@when("I set the run character spacing to 2 points")
def when_spacing(context):
context.run.font.character_spacing = Pt(2)


@then("the run reports character spacing 2 points")
def then_spacing(context):
assert context.run.font.character_spacing.pt == 2.0


@when("I set east_asian to MS Gothic and name to Calibri")
def when_trio(context):
context.run.font.east_asian = "MS Gothic"
context.run.font.name = "Calibri"


@then("latin is Calibri and east_asian is MS Gothic and they are independent")
def then_trio(context):
f2 = _roundtrip(context).paragraphs[0].runs[0].font
assert f2.name == "Calibri" and f2.east_asian == "MS Gothic"
assert f2.latin == "Calibri"


@when("I set the text frame to 2 columns spaced 36 points")
def when_columns(context):
context.tf.columns = 2
context.tf.column_spacing = Pt(36)


@then("the text frame reports 2 columns after round-trip")
def then_columns(context):
assert _roundtrip(context).columns == 2


@when("I set the text direction to east asian vertical")
def when_direction(context):
context.tf.text_direction = MSO_TEXT_DIRECTION.EAST_ASIAN_VERTICAL


@then("the text frame reports east asian vertical after round-trip")
def then_direction(context):
assert _roundtrip(context).text_direction == MSO_TEXT_DIRECTION.EAST_ASIAN_VERTICAL


@when("I set the paragraph to Arabic right-to-left")
def when_rtl(context):
p = context.tf.paragraphs[0]
p.text = "اللغة العربية"
p.rtl = True


@then("the paragraph reports rtl true after round-trip")
def then_rtl(context):
p2 = _roundtrip(context).paragraphs[0]
assert p2.rtl is True


@then("will_overflow reports true")
def then_will_overflow(context):
assert context.tf.will_overflow(font_file=TEST_FONT) is True


@when("I call fit_text on it")
def when_fit_text(context):
context.tf.fit_text(font_file=TEST_FONT)


@then("no error is raised and auto_size is set")
def then_fit_ok(context):
assert context.tf.auto_size is not None


@when("I call shrink_text_to_fit")
def when_shrink(context):
context.tf.shrink_text_to_fit(font_file=TEST_FONT)


@then("the normAutofit fontScale is below 100")
def then_shrink(context):
na = context.tf._txBody.bodyPr.normAutofit
assert na is not None and na.fontScale < 100
assert context.tf.auto_size == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
48 changes: 48 additions & 0 deletions src/pptx/enum/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,51 @@ class PP_AUTO_NUMBER_STYLE(BaseXmlEnum):

ALPHA_LC_PAREN_BOTH = (12, "alphaLcParenBoth", "Lowercase letters in parentheses: (a) (b) (c)")
"""Lowercase letters in parentheses: (a) (b) (c)"""


class MSO_TEXT_STRIKE_TYPE(BaseXmlEnum):
"""Specifies the strikethrough style of text.

Used with :attr:`.Font.strike`. Maps to the OOXML `a:rPr/@strike`
attribute (`ST_TextStrikeType`). Issue #16 SF2.

MS API Name: (no direct VBA equivalent — modeled on `MsoTextStrike`).
"""

NONE = (0, "noStrike", "No strikethrough.")
"""No strikethrough."""

SINGLE = (1, "sngStrike", "A single-line strikethrough.")
"""A single-line strikethrough."""

DOUBLE = (2, "dblStrike", "A double-line strikethrough.")
"""A double-line strikethrough."""


class MSO_TEXT_DIRECTION(BaseXmlEnum):
"""Specifies the flow direction of text in a text frame.

Used with :attr:`.TextFrame.text_direction`. Maps to the OOXML
`a:bodyPr/@vert` attribute (`ST_TextVerticalType`). Issue #16 SF7.
"""

HORIZONTAL = (0, "horz", "Horizontal text (the default).")
"""Horizontal text (the default)."""

VERTICAL = (1, "vert", "Vertical text, rotated 90° clockwise.")
"""Vertical text, rotated 90° clockwise."""

VERTICAL_270 = (2, "vert270", "Vertical text, rotated 270° clockwise.")
"""Vertical text, rotated 270° clockwise."""

WORD_ART_VERTICAL = (3, "wordArtVert", "WordArt-style stacked vertical text.")
"""WordArt-style stacked vertical text."""

EAST_ASIAN_VERTICAL = (4, "eaVert", "East-Asian vertical text.")
"""East-Asian vertical text."""

MONGOLIAN_VERTICAL = (5, "mongolianVert", "Mongolian vertical text.")
"""Mongolian vertical text."""

WORD_ART_VERTICAL_RTL = (6, "wordArtVertRtl", "Right-to-left WordArt vertical text.")
"""Right-to-left WordArt vertical text."""
3 changes: 3 additions & 0 deletions src/pptx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
register_element_cls("a:alphaOff", CT_PositiveFixedPercentage)
register_element_cls("a:bgClr", CT_Color)
register_element_cls("a:fgClr", CT_Color)
register_element_cls("a:highlight", CT_Color)
register_element_cls("a:hslClr", CT_HslColor)
register_element_cls("a:lumMod", CT_Percentage)
register_element_cls("a:lumOff", CT_Percentage)
Expand Down Expand Up @@ -583,6 +584,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
register_element_cls("a:endParaRPr", CT_TextCharacterProperties)
register_element_cls("a:fld", CT_TextField)
register_element_cls("a:latin", CT_TextFont)
register_element_cls("a:ea", CT_TextFont)
register_element_cls("a:cs", CT_TextFont)
register_element_cls("a:lnSpc", CT_TextSpacing)
register_element_cls("a:normAutofit", CT_TextNormalAutofit)
register_element_cls("a:r", CT_RegularTextRun)
Expand Down
31 changes: 31 additions & 0 deletions src/pptx/oxml/simpletypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,37 @@ def validate(cls, value):
cls.validate_int_in_range(value, 100, 400000)


class ST_TextPoint(BaseIntType):
"""Signed text point measure in 1/100 pt, e.g. character spacing `spc`.

OOXML ST_TextPointUnqualified is `xsd:int` restricted to
-400000..400000 (-4000..4000 pt). Negative values tighten spacing.
"""

@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, -400000, 400000)


class ST_TextNonNegativePoint(BaseIntType):
"""Non-negative text point measure in 1/100 pt, e.g. kerning `kern`.

OOXML ST_TextNonNegativePoint restricts to 0..400000.
"""

@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 400000)


class ST_TextColumnCount(BaseIntType):
"""Text column count, 1..16 inclusive (OOXML ST_TextColumnCount)."""

@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 1, 16)


class ST_TextIndentLevelType(BaseIntType):
@classmethod
def validate(cls, value):
Expand Down
Loading
Loading