Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pattern feature implementation #1041

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a380c20
Added set_fill_pattern() function to fpdf.py
boushrabnd Dec 4, 2023
dbad42f
Added Tilling Pattern Object (experiment)
boushrabnd Dec 4, 2023
7b1877d
Added default pattern to be tilling (experiment)
boushrabnd Dec 4, 2023
6cde5d2
Added PDFPattern Object to be outputed in PDF
boushrabnd Dec 4, 2023
08f6c34
Pylint and Black
boushrabnd Dec 4, 2023
38cfca1
Fixing pylint errors and black formatting
boushrabnd Dec 4, 2023
ec390b9
Developed basic function that works but not a 100% and only works on …
Dec 4, 2023
477b0c4
Developed a more complex function that produces patterns of rectangle…
Dec 4, 2023
7442c47
Developed basic function that works but not a 100% and only works on …
Dec 5, 2023
f5d460e
Developed a function that produces patterns of rectangles, squares, a…
Dec 5, 2023
d1e4485
Merge branch 'Pattern-Feature-Implementation' of https://github.com/j…
Dec 5, 2023
6ed0563
editing set fill pattern to user input and creating PDFPattern object
hmkhalif Dec 5, 2023
5ead1a4
adding pattern to dictionary of patterns
hmkhalif Dec 5, 2023
cf020d1
adding apply pattern function that draws pattern
hmkhalif Dec 5, 2023
bd5a778
adding error message when applying pattern
hmkhalif Dec 5, 2023
d09c594
editing apply function to fix loop syntax and prevent errors
hmkhalif Dec 5, 2023
c1fff08
adding trial file with pdf result with patterns after these changes
hmkhalif Dec 5, 2023
fd00ac0
trying to merge
hmkhalif Dec 5, 2023
04a42dd
removing print statement after testing code works
smubarak7 Dec 5, 2023
79641f9
finalized the set current pattern that updates patterns
smubarak7 Dec 5, 2023
fae1428
modifying RenderStyle so shapes can have pattern style
smubarak7 Dec 5, 2023
4e6bbc9
editing rectangle shape to allow for pattern style
smubarak7 Dec 5, 2023
1e7ab4d
removing print statement after testing
smubarak7 Dec 5, 2023
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
43 changes: 43 additions & 0 deletions Expolration_set_pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from fpdf import FPDF

# Create instance of FPDF class
pdf = FPDF()

# Add a page
pdf.add_page()

# Set fill color (RGB)
pdf.set_fill_color(255, 205, 0) # Setting the color to red

# pdf.set_page_background('/Users/lamamasri/Desktop/image_13.gif')
w1 = 10
w2 = 10
w3 = 50
w4 = 40
pdf.rect(10, 10, 50, 30)

n = 0
while n <= 50:
pdf.rect(w1, w2, w3 / 10, w3 / 10, "F")
pdf.rect(w1, w2 + 10, w3 / 10, w3 / 10, "F")
pdf.rect(w1, w2 + 20, w3 / 10, w3 / 10, "F")
pdf.rect(w1, w2 + 28, w3 / 10, w3 / 10, "F")
w1 = w1 + w3 / 5

n = n + 10
print("N1: ", n)
print("W1; ,", w1)

# Draw a filled rectangle
# for i in range (w3):
# pdf.rect(w1, w2,w3/10, w3/10, 'F')

# pdf.rect(10, 10, 5, 5, 'F') # 'F' argument is for fill

# pdf.rect(20, 10, 5, 5, 'F')

# pdf.rect(30, 10, 5, 5, 'F')


# Output the PDF
pdf.output("filled_rectangle.pdf")
37 changes: 37 additions & 0 deletions Working_function_rect_circ.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from fpdf import FPDF

pdf = FPDF()

# Add a page
pdf.add_page()

# Set fill color (RGB)
pdf.set_fill_color(255, 205, 0) # Setting the color to yellow

# Draw the outer rectangle
outer_rect_x, outer_rect_y, outer_rect_w, outer_rect_h = 10, 10, 50, 30
pdf.ellipse(
outer_rect_x, outer_rect_y, outer_rect_w, outer_rect_h, "F"
) # Fill the outer rectangle

# Pattern properties
pattern_width, pattern_height = 5, 10 # Width and height of the small rectangles
cols = int(outer_rect_w / pattern_width) # Number of small rectangles horizontally
rows = int(outer_rect_h / pattern_height) # Number of small rectangles vertically

# Set color for the pattern
pdf.set_fill_color(255, 255, 255) # White for the spacing

# Create the pattern
for i in range(cols):
for j in range(rows):
if (i + j) % 2 == 0: # Check for alternate placement
x = outer_rect_x + i * pattern_width
y = outer_rect_y + j * pattern_height
pdf.rect(x, y, pattern_width, pattern_height, "F")

pdf.set_draw_color(0, 0, 0) # Black border
pdf.ellipse(outer_rect_x, outer_rect_y, outer_rect_w, outer_rect_h)

# Output the PDF
pdf.output("pattern_filled_rectangle.pdf")
Binary file added filled_rectangle.pdf
Binary file not shown.
27 changes: 27 additions & 0 deletions fpdf/drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ class GraphicsStyle:
"intersection_rule",
"fill_color",
"fill_opacity",
"fill_pattern",
"stroke_color",
"stroke_opacity",
"blend_mode",
Expand Down Expand Up @@ -1149,6 +1150,7 @@ def __init__(self):
self.intersection_rule = self.INHERIT
self.fill_color = self.INHERIT
self.fill_opacity = self.INHERIT
self.fill_pattern = self.INHERIT
self.stroke_color = self.INHERIT
self.stroke_opacity = self.INHERIT
self.blend_mode = self.INHERIT
Expand Down Expand Up @@ -4206,3 +4208,28 @@ def render_debug(
pfx,
_push_stack=_push_stack,
)


class TilingPattern:
def __init__(
self,
pattern_type=1,
paint_type=1,
tiling_type=1,
bbox=None,
x_step=20,
y_step=20,
resources=None,
matrix=None,
):
self.pattern_type = pattern_type
self.paint_type = paint_type
self.tiling_type = tiling_type
self.bbox = bbox if bbox is not None else [0, 0, 10, 10]
self.x_step = x_step
self.y_step = y_step
self.resources = resources if resources is not None else {}
self.matrix = matrix if matrix is not None else [1, 0, 0, 1, 0, 0]
@staticmethod # remove once the serialize is fully implememented
def serialize():
return "/Pattern cs /P1 scn" # place holder to be fixed
73 changes: 36 additions & 37 deletions fpdf/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,44 +326,43 @@ def should_fill_cell(self, i, j):
raise NotImplementedError


class RenderStyle(CoerciveEnum):
"Defines how to render shapes"

D = intern("DRAW")
"""
Draw lines.
Line color can be controlled with `fpdf.fpdf.FPDF.set_draw_color()`.
Line thickness can be controlled with `fpdf.fpdf.FPDF.set_line_width()`.
"""

F = intern("FILL")
"""
Fill areas.
Filling color can be controlled with `fpdf.fpdf.FPDF.set_fill_color()`.
"""

DF = intern("DRAW_FILL")
"Draw lines and fill areas"

@property
def operator(self):
return {self.D: "S", self.F: "f", self.DF: "B"}[self]

@property
def is_draw(self):
return self in (self.D, self.DF)

@property
def is_fill(self):
return self in (self.F, self.DF)

@classmethod
def coerce(cls, value):
if not value:
return cls.D
if value == "FD":
value = "DF"
return super(cls, cls).coerce(value)
class RenderStyle(CoerciveEnum):
"Defines how to render shapes"
D = intern("DRAW")
"""
Draw lines.
Line color can be controlled with `fpdf.fpdf.FPDF.set_draw_color()`.
Line thickness can be controlled with `fpdf.fpdf.FPDF.set_line_width()`.
"""
F = intern("FILL")
"""
Fill areas.
Filling color can be controlled with `fpdf.fpdf.FPDF.set_fill_color()`.
"""
DF = intern("DRAW_FILL")
"Draw lines and fill areas"
# adding pattern
P = intern("PATTERN")
@property
def operator(self):
return {self.D: "S", self.F: "f", self.DF: "B", self.P: "f"}[self]

@property
def is_draw(self):
return self in (self.D, self.DF, self.P) # Include pattern in drawing
@property
def is_fill(self):
return self in (self.F, self.DF, self.P)
@classmethod
def coerce(cls, value):
if not value:
return cls.D
if value == "FD":
value = "DF"
return super(cls, cls).coerce(value)




class TextMode(CoerciveIntEnum):
Expand Down
94 changes: 93 additions & 1 deletion fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class Image:
PDFPage,
ZOOM_CONFIGS,
stream_content_for_raster_image,
PDFPattern
)
from .recorder import FPDFRecorder
from .sign import Signature
Expand Down Expand Up @@ -263,6 +264,7 @@ def __init__(
self.page = 0 # current page number
self.pages = {} # array of PDFPage objects starting at index 1
self.fonts = {} # map font string keys to an instance of CoreFont or TTFFont
self.patterns = {} # map pattern fill objects
self.links = {} # array of Destination objects starting at index 1
self.embedded_files = [] # array of PDFEmbeddedFile
self.image_cache = ImageCache()
Expand Down Expand Up @@ -311,6 +313,7 @@ def __init__(
)
self.draw_color = self.DEFAULT_DRAW_COLOR
self.fill_color = self.DEFAULT_FILL_COLOR
self.fill_pattern = self.DEFAULT_FILL_PATTERN
self.text_color = self.DEFAULT_TEXT_COLOR
self.page_background = None
self.dash_pattern = dict(dash=0, gap=0, phase=0)
Expand Down Expand Up @@ -1019,6 +1022,48 @@ def set_fill_color(self, r, g=-1, b=-1):
if self.page > 0:
self._out(self.fill_color.serialize().lower())

def set_fill_pattern(self, bbox, x_step, y_step, pattern_type, paint_type=None, tiling_type=1, resources=None, matrix=None):
"""
Defines the pattern used for all filling operations (filled rectangles and cell backgrounds).
It can be expressed in RGB components or grey scale.
The method can be called before the first page is created and the value is retained from page to page.

Args:
r (int, tuple, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): if `g` and `b` are given, this indicates the red component.
Else, this indicates the grey level. The value must be between 0 and 255.
g (int): green component (between 0 and 255)
b (int): blue component (between 0 and 255)
"""
#print("Hello")
pattern = PDFPattern(
pattern_type= pattern_type, # Assuming tiling pattern is always used
paint_type=paint_type,
tiling_type=tiling_type,
bbox=bbox,
x_step=x_step,
y_step=y_step,
resources=resources,
matrix=matrix
)

# Add the pattern to a patterns dictionary in the PDF document
pattern_id = len(self.patterns) + 1
self.patterns[pattern_id] = pattern
#print(self.patterns)
# Set this pattern as the current fill pattern
self.current_fill_pattern = pattern_id
#print(self.current_fill_pattern)

def set_current_pattern(self, pattern_id):

if pattern_id in self.patterns:

self.current_fill_pattern = pattern_id

else:

raise ValueError(f"Pattern ID '{pattern_id}' not found in patterns.")

def set_text_color(self, r, g=-1, b=-1):
"""
Defines the color used for text.
Expand Down Expand Up @@ -1339,6 +1384,39 @@ def dashed_line(self, x1, y1, x2, y2, dash_length=1, space_length=1):
self.line(x1, y1, x2, y2)
self.set_dash_pattern()

def apply_fill_pattern(self, pattern_id, x, y, w, h):
pattern = self.patterns.get(pattern_id)
if pattern:

# Use the pattern's bbox to determine the size of each tile
pattern_width, pattern_height = pattern.bbox[2], pattern.bbox[3]

# Use the pattern's x_step and y_step for spacing
x_step, y_step = pattern.x_step, pattern.y_step

# Calculate how many times the pattern will repeat
x_repeat = int(w / x_step)
y_repeat = int(h / y_step)

# Loop through and draw the pattern
for i in range(x_repeat):
for j in range(y_repeat):
# Calculate the top-left corner of the current pattern tile
pattern_x = x + i * x_step
pattern_y = y + j * y_step
if pattern.pattern_type == "circles":
if pattern.paint_type is not None:
self.set_fill_color(*pattern.paint_type)
self.ellipse(pattern_x, pattern_y, pattern_width, pattern_height, 'F')
self.ellipse(pattern_x, pattern_y, pattern_width, pattern_height, 'D')
elif pattern.pattern_type == "squares":
if pattern.paint_type is not None:
self.set_fill_color(*pattern.paint_type)
self.rect(pattern_x, pattern_y, pattern_width, pattern_height, 'F')
self.rect(pattern_x, pattern_y, pattern_width, pattern_height, 'D')
else:
raise ValueError('Not defined pattern type')

@check_page
def rect(self, x, y, w, h, style=None, round_corners=False, corner_radius=0):
"""
Expand Down Expand Up @@ -1368,8 +1446,19 @@ def rect(self, x, y, w, h, style=None, round_corners=False, corner_radius=0):

corner_radius: Optional radius of the corners
"""

style = RenderStyle.coerce(style)


if style == RenderStyle.P:

# Check if a current pattern is set and apply it
if self.current_fill_pattern is not None:

self.apply_fill_pattern(self.current_fill_pattern, x, y, w, h)

return None

if round_corners is not False:
self._draw_rounded_rect(x, y, w, h, style, round_corners, corner_radius)
else:
Expand Down Expand Up @@ -2581,6 +2670,7 @@ def local_context(
line_width=None,
draw_color=None,
fill_color=None,
fill_pattern=None,
text_color=None,
dash_pattern=None,
**kwargs,
Expand Down Expand Up @@ -2664,6 +2754,8 @@ def local_context(
self.set_draw_color(draw_color)
if fill_color is not None:
self.set_fill_color(fill_color)
if fill_pattern is not None:
self.set_fill_pattern()
if text_color is not None:
self.set_text_color(text_color)
if dash_pattern is not None:
Expand Down
3 changes: 2 additions & 1 deletion fpdf/graphics_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
in non-backward-compatible ways.
"""

from .drawing import DeviceGray
from .drawing import DeviceGray, TilingPattern
from .enums import CharVPos, TextEmphasis, TextMode
from .fonts import FontFace

Expand All @@ -26,6 +26,7 @@ class GraphicsStateMixin:
DEFAULT_DRAW_COLOR = DeviceGray(0)
DEFAULT_FILL_COLOR = DeviceGray(0)
DEFAULT_TEXT_COLOR = DeviceGray(0)
DEFAULT_FILL_PATTERN = TilingPattern()

def __init__(self, *args, **kwargs):
self.__statestack = [
Expand Down
Loading