tractatus/pptx-env/lib/python3.12/site-packages/weasyprint/pdf/stream.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

304 lines
10 KiB
Python

"""PDF stream."""
from contextlib import contextmanager
import pydyf
from ..logger import LOGGER
from ..matrix import Matrix
from ..text.ffi import ffi
from ..text.fonts import get_pango_font_key
from .fonts import Font
class Stream(pydyf.Stream):
"""PDF stream object with extra features."""
def __init__(self, fonts, page_rectangle, resources, images, tags, *args, **kwargs):
super().__init__(*args, **kwargs)
self.page_rectangle = page_rectangle
self._fonts = fonts
self._resources = resources
self._images = images
self._tags = tags
self._current_color = self._current_color_stroke = None
self._current_alpha = self._current_alpha_stroke = None
self._current_font = self._current_font_size = None
self._old_font = self._old_font_size = None
self._ctm_stack = [Matrix()]
# These objects are used in text.show_first_line
self.length = ffi.new('unsigned int *')
self.ink_rect = ffi.new('PangoRectangle *')
self.logical_rect = ffi.new('PangoRectangle *')
def clone(self, **kwargs):
if 'fonts' not in kwargs:
kwargs['fonts'] = self._fonts
if 'page_rectangle' not in kwargs:
kwargs['page_rectangle'] = self.page_rectangle
if 'resources' not in kwargs:
kwargs['resources'] = self._resources
if 'images' not in kwargs:
kwargs['images'] = self._images
if 'tags' not in kwargs:
kwargs['tags'] = self._tags
if 'compress' not in kwargs:
kwargs['compress'] = self.compress
return Stream(**kwargs)
@property
def ctm(self):
return self._ctm_stack[-1]
def push_state(self):
super().push_state()
self._ctm_stack.append(self.ctm)
def pop_state(self):
if self.stream and self.stream[-1] == b'q':
self.stream.pop()
else:
super().pop_state()
self._current_color = self._current_color_stroke = None
self._current_alpha = self._current_alpha_stroke = None
self._current_font = None
self._ctm_stack.pop()
assert self._ctm_stack
def transform(self, a=1, b=0, c=0, d=1, e=0, f=0):
super().set_matrix(a, b, c, d, e, f)
self._ctm_stack[-1] = Matrix(a, b, c, d, e, f) @ self.ctm
def begin_text(self):
if self.stream and self.stream[-1] == b'ET':
self._current_font = self._old_font
self.stream.pop()
else:
super().begin_text()
def end_text(self):
self._old_font, self._current_font = self._current_font, None
super().end_text()
def set_color(self, color, stroke=False):
*channels, alpha = color
self.set_alpha(alpha, stroke)
if stroke:
if (color.space, *channels) == self._current_color_stroke:
return
else:
self._current_color_stroke = (color.space, *channels)
else:
if (color.space, *channels) == self._current_color:
return
else:
self._current_color = (color.space, *channels)
if color.space in ('srgb', 'hsl', 'hwb'):
self.set_color_rgb(*color.to('srgb').coordinates, stroke)
elif color.space in ('xyz-d65', 'oklab', 'oklch'):
self.set_color_space('lab-d65', stroke)
lightness, a, b = color.to('lab').coordinates
self.set_color_special(None, stroke, lightness, a, b)
elif color.space in ('xyz-d50', 'lab', 'lch'):
self.set_color_space('lab-d50', stroke)
lightness, a, b = color.to('lab').coordinates
self.set_color_special(None, stroke, lightness, a, b)
else:
LOGGER.warning('Unsupported color space %s, use sRGB instead', color.space)
self.set_color_rgb(*channels, stroke)
def set_font_size(self, font, size):
if (font, size) == self._current_font:
return
self._current_font = (font, size)
super().set_font_size(font, size)
def set_state(self, state):
key = f's{len(self._resources["ExtGState"])}'
self._resources['ExtGState'][key] = state
super().set_state(key)
def set_alpha(self, alpha, stroke=False, fill=None):
if fill is None:
fill = not stroke
if stroke:
key = f'A{alpha}'
if key != self._current_alpha_stroke:
self._current_alpha_stroke = key
if key not in self._resources['ExtGState']:
self._resources['ExtGState'][key] = pydyf.Dictionary({'CA': alpha})
super().set_state(key)
if fill:
key = f'a{alpha}'
if key != self._current_alpha:
self._current_alpha = key
if key not in self._resources['ExtGState']:
self._resources['ExtGState'][key] = pydyf.Dictionary({'ca': alpha})
super().set_state(key)
def set_alpha_state(self, x, y, width, height, mode='luminosity'):
alpha_stream = self.add_group(x, y, width, height)
alpha_state = pydyf.Dictionary({
'Type': '/ExtGState',
'SMask': pydyf.Dictionary({
'Type': '/Mask',
'S': f'/{mode.capitalize()}',
'G': alpha_stream,
}),
'ca': 1,
'AIS': 'false',
})
self.set_state(alpha_state)
return alpha_stream
def set_blend_mode(self, mode):
self.set_state(pydyf.Dictionary({
'Type': '/ExtGState',
'BM': f'/{mode}',
}))
def add_font(self, pango_font):
key, description, font_size = get_pango_font_key(pango_font)
if key not in self._fonts:
self._fonts[key] = Font(pango_font, description, font_size)
return self._fonts[key], font_size
def add_group(self, x, y, width, height):
resources = pydyf.Dictionary({
'ExtGState': pydyf.Dictionary(),
'XObject': pydyf.Dictionary(),
'Pattern': pydyf.Dictionary(),
'Shading': pydyf.Dictionary(),
'ColorSpace': self._resources['ColorSpace'],
'Font': None, # Will be set by _use_references
})
extra = pydyf.Dictionary({
'Type': '/XObject',
'Subtype': '/Form',
'BBox': pydyf.Array((x, y, x + width, y + height)),
'Resources': resources,
'Group': pydyf.Dictionary({
'Type': '/Group',
'S': '/Transparency',
'I': 'true',
'CS': '/DeviceRGB',
}),
})
group = self.clone(resources=resources, extra=extra)
group.id = f'x{len(self._resources["XObject"])}'
self._resources['XObject'][group.id] = group
return group
def add_image(self, image, interpolate, ratio):
image_name = f'i{image.id}{int(interpolate)}'
self._resources['XObject'][image_name] = None # Set by write_pdf
if image_name in self._images:
# Reuse image already stored in document
self._images[image_name]['dpi_ratios'].add(ratio)
return image_name
self._images[image_name] = {
'image': image,
'interpolate': interpolate,
'dpi_ratios': {ratio},
'x_object': None, # Set by write_pdf
}
return image_name
def add_pattern(self, x, y, width, height, repeat_width, repeat_height, matrix):
resources = pydyf.Dictionary({
'ExtGState': pydyf.Dictionary(),
'XObject': pydyf.Dictionary(),
'Pattern': pydyf.Dictionary(),
'Shading': pydyf.Dictionary(),
'ColorSpace': self._resources['ColorSpace'],
'Font': None, # Will be set by _use_references
})
extra = pydyf.Dictionary({
'Type': '/Pattern',
'PatternType': 1,
'BBox': pydyf.Array([x, y, x + width, y + height]),
'XStep': repeat_width,
'YStep': repeat_height,
'TilingType': 1,
'PaintType': 1,
'Matrix': pydyf.Array(matrix.values),
'Resources': resources,
})
pattern = self.clone(resources=resources, extra=extra)
pattern.id = f'p{len(self._resources["Pattern"])}'
self._resources['Pattern'][pattern.id] = pattern
return pattern
def add_shading(self, shading_type, color_space, domain, coords, extend,
function):
shading = pydyf.Dictionary({
'ShadingType': shading_type,
'ColorSpace': f'/Device{color_space}',
'Domain': pydyf.Array(domain),
'Coords': pydyf.Array(coords),
'Function': function,
})
if extend:
shading['Extend'] = pydyf.Array((b'true', b'true'))
shading.id = f's{len(self._resources["Shading"])}'
self._resources['Shading'][shading.id] = shading
return shading
@contextmanager
def stacked(self):
"""Save and restore stream context when used with the ``with`` keyword."""
self.push_state()
try:
yield
finally:
self.pop_state()
@contextmanager
def marked(self, box, tag):
if self._tags is not None:
property_list = None
mcid = len(self._tags)
assert box not in self._tags
self._tags[box] = {'tag': tag, 'mcid': mcid}
property_list = pydyf.Dictionary({'MCID': mcid})
super().begin_marked_content(tag, property_list)
try:
yield
finally:
if self._tags is not None:
super().end_marked_content()
@contextmanager
def artifact(self):
if self._tags is not None:
super().begin_marked_content('Artifact')
try:
yield
finally:
if self._tags is not None:
super().end_marked_content()
@staticmethod
def create_interpolation_function(domain, c0, c1, n):
return pydyf.Dictionary({
'FunctionType': 2,
'Domain': pydyf.Array(domain),
'C0': pydyf.Array(c0),
'C1': pydyf.Array(c1),
'N': n,
})
@staticmethod
def create_stitching_function(domain, encode, bounds, sub_functions):
return pydyf.Dictionary({
'FunctionType': 3,
'Domain': pydyf.Array(domain),
'Encode': pydyf.Array(encode),
'Bounds': pydyf.Array(bounds),
'Functions': pydyf.Array(sub_functions),
})