tractatus/pptx-env/lib/python3.12/site-packages/weasyprint/draw/__init__.py
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

539 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Take an "after layout" box tree and draw it onto a pydyf stream."""
import operator
from math import floor
from xml.etree import ElementTree
from ..formatting_structure import boxes
from ..images import SVGImage
from ..layout import replaced
from ..layout.background import BackgroundLayer
from ..matrix import Matrix
from ..stacking import StackingContext
from .border import draw_border, draw_line, draw_outline, rounded_box, set_mask_border
from .color import styled_color
from .text import draw_text
def draw_page(page, stream):
"""Draw the given PageBox."""
marks = page.style['marks']
stacking_context = StackingContext.from_page(page)
draw_background(
stream, stacking_context.box.background, clip_box=False, bleed=page.bleed,
marks=marks)
set_mask_border(stream, page)
draw_background(stream, page.canvas_background, clip_box=False)
draw_border(stream, page)
draw_stacking_context(stream, stacking_context)
def draw_stacking_context(stream, stacking_context):
"""Draw a ``stacking_context`` on ``stream``."""
# See https://www.w3.org/TR/CSS2/zindex.html.
with stream.stacked():
box = stacking_context.box
# Apply the viewport_overflow to the html box, see #35.
if box.is_for_root_element and (
stacking_context.page.style['overflow'] != 'visible'):
rounded_box(stream, stacking_context.page.rounded_padding_box())
stream.clip()
stream.end()
if box.is_absolutely_positioned() and box.style['clip']:
top, right, bottom, left = box.style['clip']
if top == 'auto':
top = 0
if right == 'auto':
right = 0
if bottom == 'auto':
bottom = box.border_height()
if left == 'auto':
left = box.border_width()
stream.rectangle(
box.border_box_x() + right, box.border_box_y() + top,
left - right, bottom - top)
stream.clip()
stream.end()
if box.style['opacity'] < 1:
original_stream = stream
stream = stream.add_group(*stream.page_rectangle)
if box.transformation_matrix:
if box.transformation_matrix.determinant:
stream.transform(*box.transformation_matrix.values)
else:
return
# Point 1 is done in draw_page.
# Point 2.
if isinstance(box, (boxes.BlockBox, boxes.MarginBox, boxes.InlineBlockBox,
boxes.TableCellBox, boxes.FlexContainerBox,
boxes.GridContainerBox, boxes.ReplacedBox)):
set_mask_border(stream, box)
# The canvas background was removed by layout_backgrounds.
draw_background(stream, box.background)
draw_border(stream, box)
with stream.stacked():
# Dont clip the page box, see #35.
clip = (
box.style['overflow'] != 'visible' and
not isinstance(box, boxes.PageBox))
if clip:
# Only clip the content and the children:
# - the background is already clipped,
# - the border must *not* be clipped.
rounded_box(stream, box.rounded_padding_box())
stream.clip()
stream.end()
# Point 3.
for child_context in stacking_context.negative_z_contexts:
draw_stacking_context(stream, child_context)
# Point 4.
for block in stacking_context.block_level_boxes:
set_mask_border(stream, block)
if isinstance(block, boxes.TableBox):
draw_table(stream, block)
else:
draw_background(stream, block.background)
draw_border(stream, block)
# Point 5.
for child_context in stacking_context.float_contexts:
draw_stacking_context(stream, child_context)
# Point 6.
if isinstance(box, boxes.InlineBox):
draw_inline_level(stream, stacking_context.page, box)
# Point 7.
draw_block_level(
stacking_context.page, stream, {box: stacking_context.blocks_and_cells})
# Point 8.
for child_context in stacking_context.zero_z_contexts:
draw_stacking_context(stream, child_context)
# Point 9.
for child_context in stacking_context.positive_z_contexts:
draw_stacking_context(stream, child_context)
# Point 10.
draw_outline(stream, box)
if box.style['opacity'] < 1:
group_id = stream.id
stream = original_stream
with stream.stacked():
stream.set_alpha(box.style['opacity'], stroke=True, fill=True)
stream.draw_x_object(group_id)
def draw_background(stream, bg, clip_box=True, bleed=None, marks=()):
"""Draw the background color and image to a ``pdf.stream.Stream``.
If ``clip_box`` is set to ``False``, the background is not clipped to the
border box of the background, but only to the painting area.
"""
if bg is None:
return
with stream.stacked():
if clip_box:
for box in bg.layers[-1].clipped_boxes:
rounded_box(stream, box)
stream.clip()
stream.end()
# Draw background color.
if bg.color.alpha > 0:
with stream.artifact(), stream.stacked():
stream.set_color(bg.color)
painting_area = bg.layers[-1].painting_area
stream.rectangle(*painting_area)
stream.clip()
stream.end()
stream.rectangle(*painting_area)
stream.fill()
# Draw crop marks and crosses.
if bleed and marks:
x, y, width, height = bg.layers[-1].painting_area
half_bleed = {key: value * 0.5 for key, value in bleed.items()}
svg = f'''
<svg height="{height}" width="{width}"
fill="transparent" stroke="black" stroke-width="1"
xmlns="http://www.w3.org/2000/svg">
'''
if 'crop' in marks:
svg += f'''
<path d="M0,{bleed['top']} h{half_bleed['left']}" />
<path d="M0,{bleed['top']} h{half_bleed['right']}"
transform="translate({width},0) scale(-1,1)" />
<path d="M0,{bleed['bottom']} h{half_bleed['right']}"
transform="translate({width},{height}) scale(-1,-1)" />
<path d="M0,{bleed['bottom']} h{half_bleed['left']}"
transform="translate(0,{height}) scale(1,-1)" />
<path d="M{bleed['left']},0 v{half_bleed['top']}" />
<path d="M{bleed['right']},0 v{half_bleed['bottom']}"
transform="translate({width},{height}) scale(-1,-1)" />
<path d="M{bleed['left']},0 v{half_bleed['bottom']}"
transform="translate(0,{height}) scale(1,-1)" />
<path d="M{bleed['right']},0 v{half_bleed['top']}"
transform="translate({width},0) scale(-1,1)" />
'''
if 'cross' in marks:
svg += f'''
<circle r="{half_bleed['top']}" transform="scale(0.5)
translate({width},{half_bleed['top']}) scale(0.5)" />
<path transform="scale(0.5) translate({width},0)" d="
M-{half_bleed['top']},{half_bleed['top']} h{bleed['top']}
M0,0 v{bleed['top']}" />
<circle r="{half_bleed['bottom']}" transform="
translate(0,{height}) scale(0.5)
translate({width},-{half_bleed['bottom']}) scale(0.5)" />
<path d="M-{half_bleed['bottom']},-{half_bleed['bottom']}
h{bleed['bottom']} M0,0 v-{bleed['bottom']}" transform="
translate(0,{height}) scale(0.5) translate({width},0)" />
<circle r="{half_bleed['left']}" transform="scale(0.5)
translate({half_bleed['left']},{height}) scale(0.5)" />
<path d="M{half_bleed['left']},-{half_bleed['left']}
v{bleed['left']} M0,0 h{bleed['left']}"
transform="scale(0.5) translate(0,{height})" />
<circle r="{half_bleed['right']}" transform="
translate({width},0) scale(0.5)
translate(-{half_bleed['right']},{height}) scale(0.5)" />
<path d="M-{half_bleed['right']},-{half_bleed['right']}
v{bleed['right']} M0,0 h-{bleed['right']}" transform="
translate({width},0) scale(0.5) translate(0,{height})" />
'''
svg += '</svg>'
tree = ElementTree.fromstring(svg)
image = SVGImage(tree, None, None, stream)
# Painting area is the PDF media box
size = (width, height)
position = (x, y)
repeat = ('no-repeat', 'no-repeat')
unbounded = True
painting_area = position + size
positioning_area = (0, 0, width, height)
clipped_boxes = []
layer = BackgroundLayer(
image, size, position, repeat, unbounded, painting_area,
positioning_area, clipped_boxes)
bg.layers.insert(0, layer)
# Paint in reversed order: first layer is "closest" to the viewer.
for layer in reversed(bg.layers):
draw_background_image(stream, layer, bg.image_rendering)
def draw_background_image(stream, layer, image_rendering):
if layer.image is None or 0 in layer.size:
return
painting_x, painting_y, painting_width, painting_height = layer.painting_area
positioning_x, positioning_y, positioning_width, positioning_height = (
layer.positioning_area)
position_x, position_y = layer.position
repeat_x, repeat_y = layer.repeat
image_width, image_height = layer.size
if repeat_x == 'no-repeat' and repeat_y == 'no-repeat':
with stream.artifact():
# We don't use a pattern when we don't need to because some viewers
# (e.g., Preview on Mac) introduce unnecessary pixelation when vector
# images are used in patterns.
if not layer.unbounded:
stream.rectangle(
painting_x, painting_y, painting_width, painting_height)
stream.clip()
stream.end()
# Put the image in a group so that masking outside the image and
# masking within the image don't conflict.
group = stream.add_group(*stream.page_rectangle)
group.transform(e=position_x + positioning_x, f=position_y + positioning_y)
layer.image.draw(group, image_width, image_height, image_rendering)
stream.draw_x_object(group.id)
return
if repeat_x == 'no-repeat':
# We want at least the whole image_width drawn on sub_surface, but we
# want to be sure it will not be repeated on the painting_width. We
# double the painting width to ensure viewers don't incorrectly bleed
# the edge of the pattern into the painting area. (See #1539.)
repeat_width = max(image_width, 2 * painting_width)
elif repeat_x in ('repeat', 'round'):
# We repeat the image each image_width.
repeat_width = image_width
else:
assert repeat_x == 'space'
n_repeats = floor(positioning_width / image_width)
if n_repeats >= 2:
# The repeat width is the whole positioning width with one image
# removed, divided by (the number of repeated images - 1). This
# way, we get the width of one image + one space. We ignore
# background-position for this dimension.
repeat_width = (positioning_width - image_width) / (n_repeats - 1)
position_x = 0
else:
# We don't repeat the image.
repeat_width = positioning_width
# Comments above apply here too.
if repeat_y == 'no-repeat':
repeat_height = max(image_height, 2 * painting_height)
elif repeat_y in ('repeat', 'round'):
repeat_height = image_height
else:
assert repeat_y == 'space'
n_repeats = floor(positioning_height / image_height)
if n_repeats >= 2:
repeat_height = (positioning_height - image_height) / (n_repeats - 1)
position_y = 0
else:
repeat_height = positioning_height
matrix = Matrix(e=position_x + positioning_x, f=position_y + positioning_y)
matrix @= stream.ctm
pattern = stream.add_pattern(
0, 0, image_width, image_height, repeat_width, repeat_height, matrix)
group = pattern.add_group(0, 0, repeat_width, repeat_height)
with stream.artifact(), stream.stacked():
layer.image.draw(group, image_width, image_height, image_rendering)
with pattern.artifact():
pattern.draw_x_object(group.id)
stream.set_color_space('Pattern')
stream.set_color_special(pattern.id)
if layer.unbounded:
x1, y1, x2, y2 = stream.page_rectangle
stream.rectangle(x1, y1, x2 - x1, y2 - y1)
else:
stream.rectangle(painting_x, painting_y, painting_width, painting_height)
stream.fill()
def draw_table(stream, table):
# Draw backgrounds.
draw_background(stream, table.background)
for column_group in table.column_groups:
draw_background(stream, column_group.background)
for column in column_group.children:
draw_background(stream, column.background)
for row_group in table.children:
draw_background(stream, row_group.background)
for row in row_group.children:
draw_background(stream, row.background)
for cell in row.children:
draw_cell_background = (
table.style['border_collapse'] == 'collapse' or
cell.style['empty_cells'] == 'show' or
not cell.empty)
if draw_cell_background:
draw_background(stream, cell.background)
# Draw borders.
if table.style['border_collapse'] == 'collapse':
return draw_collapsed_borders(stream, table)
draw_border(stream, table)
for row_group in table.children:
for row in row_group.children:
for cell in row.children:
if cell.style['empty_cells'] == 'show' or not cell.empty:
draw_border(stream, cell)
def draw_collapsed_borders(stream, table):
"""Draw borders of table cells when they collapse."""
row_heights = [
row.height for row_group in table.children
for row in row_group.children]
column_widths = table.column_widths
if not (row_heights and column_widths):
# One of the list is empty: dont bother with empty tables.
return
row_positions = [
row.position_y for row_group in table.children
for row in row_group.children]
column_positions = list(table.column_positions)
grid_height = len(row_heights)
grid_width = len(column_widths)
assert grid_width == len(column_positions)
vertical_borders, horizontal_borders = table.collapsed_border_grid
# Add the end of the last column.
column_positions.append(column_positions[-1] + column_widths[-1])
# Add the end of the last row.
row_positions.append(row_positions[-1] + row_heights[-1])
if table.children[0].is_header:
header_rows = len(table.children[0].children)
else:
header_rows = 0
if table.children[-1].is_footer:
footer_rows = len(table.children[-1].children)
else:
footer_rows = 0
skipped_rows = table.skipped_rows
if skipped_rows:
body_rows_offset = skipped_rows - header_rows
else:
body_rows_offset = 0
original_grid_height = len(vertical_borders)
footer_rows_offset = original_grid_height - grid_height
def row_number(y, horizontal):
# Examples in comments for 2 headers rows, 5 body rows, 3 footer rows.
if header_rows and y < header_rows + int(horizontal):
# Row in header: y < 2 for vertical, y < 3 for horizontal.
return y
elif footer_rows and y >= grid_height - footer_rows - int(horizontal):
# Row in footer: y >= 7 for vertical, y >= 6 for horizontal.
return y + footer_rows_offset
else:
# Row in body: 2 >= y > 7 for vertical, 3 >= y > 6 for horizontal.
return y + body_rows_offset
segments = []
def half_max_width(border_list, yx_pairs, vertical=True):
result = 0
for y, x in yx_pairs:
if vertical:
inside = 0 <= y < grid_height and 0 <= x <= grid_width
else:
inside = 0 <= y <= grid_height and 0 <= x < grid_width
if inside:
yy = row_number(y, horizontal=not vertical)
_, (_, width, _) = border_list[yy][x]
result = max(result, width)
return result / 2
def add_vertical(x, y):
yy = row_number(y, horizontal=False)
score, (style, width, color) = vertical_borders[yy][x]
if width == 0 or color.alpha == 0:
return
pos_x = column_positions[x]
pos_y1 = row_positions[y]
if y != 0 or not table.skip_cell_border_top:
pos_y1 -= half_max_width(
horizontal_borders, [(y, x - 1), (y, x)], vertical=False)
pos_y2 = row_positions[y + 1]
if y != grid_height - 1 or not table.skip_cell_border_bottom:
pos_y2 += half_max_width(
horizontal_borders, [(y + 1, x - 1), (y + 1, x)], vertical=False)
segments.append((
score, style, width, color, 'left', (pos_x, pos_y1, 0, pos_y2 - pos_y1)))
def add_horizontal(x, y):
if y == 0 and table.skip_cell_border_top:
return
if y == grid_height and table.skip_cell_border_bottom:
return
yy = row_number(y, horizontal=True)
score, (style, width, color) = horizontal_borders[yy][x]
if width == 0 or color.alpha == 0:
return
pos_y = row_positions[y]
shift_before = half_max_width(vertical_borders, [(y - 1, x), (y, x)])
shift_after = half_max_width(vertical_borders, [(y - 1, x + 1), (y, x + 1)])
pos_x1 = column_positions[x] - shift_before
pos_x2 = column_positions[x + 1] + shift_after
segments.append((
score, style, width, color, 'top', (pos_x1, pos_y, pos_x2 - pos_x1, 0)))
for x in range(grid_width):
add_horizontal(x, 0)
for y in range(grid_height):
add_vertical(0, y)
for x in range(grid_width):
add_vertical(x + 1, y)
add_horizontal(x, y + 1)
# Sort bigger scores last (painted later, on top).
segments.sort(key=operator.itemgetter(0))
for segment in segments:
_, style, width, color, side, border_box = segment
bx, by, bw, bh = border_box
color = styled_color(style, color, side)
with stream.artifact(), stream.stacked():
draw_line(stream, bx, by, bx + bw, by + bh, width, style, color)
def draw_replacedbox(stream, box):
"""Draw the given :class:`boxes.ReplacedBox` to a ``pdf.stream.Stream``."""
if box.style['visibility'] != 'visible' or not box.width or not box.height:
return
draw_width, draw_height, draw_x, draw_y = replaced.replacedbox_layout(box)
if draw_width <= 0 or draw_height <= 0:
return
with stream.stacked():
stream.set_alpha(1)
stream.transform(e=draw_x, f=draw_y)
with stream.stacked():
# TODO: Use the real intrinsic size here, not affected by
# 'image-resolution'?
box.replacement.draw(
stream, draw_width, draw_height, box.style['image_rendering'])
def draw_inline_level(stream, page, box, offset_x=0, text_overflow='clip',
block_ellipsis='none'):
if isinstance(box, StackingContext):
stacking_context = box
allowed_boxes = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
assert isinstance(stacking_context.box, allowed_boxes)
draw_stacking_context(stream, stacking_context)
else:
set_mask_border(stream, box)
draw_background(stream, box.background)
draw_border(stream, box)
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
if isinstance(box, boxes.LineBox):
text_overflow = box.text_overflow
block_ellipsis = box.block_ellipsis
ellipsis = 'none'
for i, child in enumerate(box.children):
if i == len(box.children) - 1:
# Last child
ellipsis = block_ellipsis
if isinstance(child, StackingContext):
child_offset_x = offset_x
else:
child_offset_x = offset_x + child.position_x - box.position_x
if isinstance(child, boxes.TextBox):
with stream.marked(child, 'Span'):
draw_text(
stream, child, child_offset_x, text_overflow, ellipsis)
else:
draw_inline_level(
stream, page, child, child_offset_x, text_overflow, ellipsis)
elif isinstance(box, boxes.InlineReplacedBox):
with stream.marked(box, 'Figure'):
draw_replacedbox(stream, box)
else:
assert isinstance(box, boxes.TextBox)
# Should only happen for list markers.
draw_text(stream, box, offset_x, text_overflow)
def draw_block_level(page, stream, blocks_and_cells):
for block, blocks_and_cells in blocks_and_cells.items():
if isinstance(block, boxes.ReplacedBox):
with stream.marked(block, 'Figure'):
draw_replacedbox(stream, block)
elif block.children:
if isinstance(block.children[-1], boxes.LineBox):
for child in block.children:
draw_inline_level(stream, page, child)
draw_block_level(page, stream, blocks_and_cells)