diff --git a/pdfstitcher/layerfilter.py b/pdfstitcher/layerfilter.py index fb3e4cc..c45081c 100644 --- a/pdfstitcher/layerfilter.py +++ b/pdfstitcher/layerfilter.py @@ -196,6 +196,7 @@ def convert_layer_props(self): convert the line properties from the GUI to what the PDF needs """ self.pdf_line_props = {} + for layer, lp in self.line_props.items(): w = 1 clp = {} @@ -219,6 +220,29 @@ def convert_layer_props(self): clp["k"] = clp["K"] self.pdf_line_props[layer] = clp + self.pdf_line_props["user_unit"] = 1 + + def adjust_user_unit(self, user_unit): + """ + Updates the width and dash pattern to match the user unit. + """ + # Don't do anything if it's already the same + if self.pdf_line_props["user_unit"] == user_unit: + return + + scale = self.pdf_line_props["user_unit"] / user_unit + + for layer, lp in self.pdf_line_props.items(): + if layer == "user_unit": + continue + + if "w" in lp.keys(): + self.pdf_line_props[layer]["w"][0] *= scale + + if "d" in lp.keys(): + self.pdf_line_props[layer]["d"][0] = [d * scale for d in lp["d"][0]] + + self.pdf_line_props["user_unit"] = user_unit def override_state(self, commands, line_props, transparency=False): """ @@ -491,6 +515,10 @@ def filter_content(self, page, current_layer_name="", do_filter=False): else: self.processed_objects.add(obid) + # Adjust the user unit if necessary + if "/UserUnit" in page.keys(): + self.adjust_user_unit(page.UserUnit) + # the page is either an actual page, or a form xobject is_page = isinstance(page, pikepdf.Page) diff --git a/pdfstitcher/pagefilter.py b/pdfstitcher/pagefilter.py index a159cd1..b61ca7a 100644 --- a/pdfstitcher/pagefilter.py +++ b/pdfstitcher/pagefilter.py @@ -41,11 +41,11 @@ def run(self): if "/UserUnit" in self.in_doc.pages[-1].keys(): new_doc.pages[-1].UserUnit = self.in_doc.pages[-1].UserUnit - user_unit = self.in_doc.pages[-1].UserUnit + user_unit = float(self.in_doc.pages[-1].UserUnit) if self.margin: # if margins were added, expand the new page boxes - margin = Config.general["units"].units_to_px(self.margin / user_unit) + margin = Config.general["units"].units_to_px(self.margin, user_unit) new_page = new_doc.pages[-1] media_box = [ float(new_page.MediaBox[0]) - margin, diff --git a/pdfstitcher/tile_pages.py b/pdfstitcher/tile_pages.py index ece4cb3..36a5528 100644 --- a/pdfstitcher/tile_pages.py +++ b/pdfstitcher/tile_pages.py @@ -56,6 +56,7 @@ def __init__( horizontal_align=SW_ALIGN_H.LEFT, ): self.in_doc = in_doc + self.user_unit = 1 if isinstance(page_range, str): self.page_range = utils.parse_page_range(page_range) @@ -179,7 +180,7 @@ def build_pagelist(self, new_doc: pikepdf.Pdf, trim: list) -> tuple: # get a pointer to the reference page and parse out the width and height ref_page = self.in_doc.pages[p - 1] - ref_width, ref_height = utils.get_page_dims(ref_page, page_rot) + ref_width, ref_height = utils.get_page_dims(ref_page, page_rot, self.user_unit) different_size = set() @@ -224,8 +225,8 @@ def build_pagelist(self, new_doc: pikepdf.Pdf, trim: list) -> tuple: in_trim[2] - rtrim[2], in_trim[3] - rtrim[3], ] - # get the input page height and width - p_width, p_height = utils.get_page_dims(in_doc_page, page_rot) + + p_width, p_height = utils.get_page_dims(in_doc_page, page_rot, self.user_unit) pw.append(p_width) ph.append(p_height) page_names.append(pagekey) @@ -240,6 +241,16 @@ def build_pagelist(self, new_doc: pikepdf.Pdf, trim: list) -> tuple: # magic sauce to copy the info to the new document as an XOBject content_dict[pagekey] = new_doc.copy_foreign(new_page.as_form_xobject()) + # scale the form xobject by the target user unit + if self.user_unit != 1: + if "/Matrix" in content_dict[pagekey].keys(): + xobj_matrix = pikepdf.PdfMatrix(content_dict[pagekey].Matrix) + else: + xobj_matrix = pikepdf.PdfMatrix.identity() + content_dict[pagekey].Matrix = xobj_matrix.scaled( + 1 / self.user_unit, 1 / self.user_unit + ).shorthand + else: # blank page, use the reference for sizes and such page_names.append(None) @@ -340,6 +351,62 @@ def calc_rows_cols(self, n_tiles: int) -> bool: else: return self.cols * self.rows - n_tiles < self.cols + def grid_position(self, tile_i: int) -> tuple[int, int]: + """Determines the placement of the tile in the grid, returning a tuple of (row, col)""" + if self.col_major: + c = math.floor(tile_i / self.rows) + r = tile_i % self.rows + else: + r = math.floor(tile_i / self.cols) + c = tile_i % self.cols + + if self.right_to_left: + c = self.cols - c - 1 + + if self.bottom_to_top: + r = self.rows - r - 1 + + return r, c + + def calc_shift(self, horizontal_space: float, vertical_space: float) -> tuple[float, float]: + """ + Calculates the shift needed to align the tile in the grid. + Returns a tuple of (shift_right, shift_up). + Only used if a tile is smaller than the grid space. + """ + if self.horizontal_align is SW_ALIGN_H.LEFT: + shift_right = 0 + elif self.horizontal_align is SW_ALIGN_H.MID: + shift_right = round(horizontal_space / 2) + elif self.horizontal_align is SW_ALIGN_H.RIGHT: + shift_right = round(horizontal_space) + if self.vertical_align is SW_ALIGN_V.BOTTOM: + shift_up = 0 + elif self.vertical_align is SW_ALIGN_V.MID: + shift_up = round(vertical_space / 2) + elif self.vertical_align is SW_ALIGN_V.TOP: + shift_up = round(vertical_space) + + # invert shift if we are rotating + if self.rotation == SW_ROTATION.CLOCKWISE: + shift_up *= -1 + elif self.rotation == SW_ROTATION.COUNTERCLOCKWISE: + shift_right *= -1 + elif self.rotation == SW_ROTATION.TURNAROUND: + shift_right *= -1 + shift_up *= -1 + + return shift_right, shift_up + + def set_user_unit(self): + """ + Find the maximum user_unit defined in the document, then use this for the new document. + """ + for p in self.page_range: + page = self.in_doc.pages[p - 1] + if "/UserUnit" in page.keys() and page.UserUnit > self.user_unit: + self.user_unit = float(page.UserUnit) + def run( self, rows=None, @@ -378,14 +445,9 @@ def run( # initialize a new document new_doc = utils.init_new_doc(self.in_doc) - # get the user unit from the first page (either 1 or 10, if it's a huge page) - user_unit = 1 - first_page = self.in_doc.pages[self.page_range[0] - 1] - if "/UserUnit" in first_page.keys(): - user_unit = float(first_page.UserUnit) - # define the trim in pdf units, then build the page list - px_trim = [Config.general["units"].units_to_px(t / user_unit) for t in self.trim] + self.set_user_unit() + px_trim = [Config.general["units"].units_to_px(t, self.user_unit) for t in self.trim] content_dict, pw, ph, page_names = self.build_pagelist(new_doc, px_trim) n_tiles = len(page_names) if not self.calc_rows_cols(n_tiles): @@ -423,7 +485,8 @@ def run( page_box_defined = False # create a new document with a page big enough to contain all the tiled pages, plus requested margin - margin = Config.general["units"].units_to_px(self.margin / user_unit) + first_page = self.in_doc.pages[self.page_range[0] - 1] + margin = Config.general["units"].units_to_px(self.margin, self.user_unit) media_box = [ float(first_page.MediaBox[0]), float(first_page.MediaBox[1]), @@ -431,7 +494,7 @@ def run( height + 2 * margin, ] - utils.print_media_box(media_box) + utils.print_media_box(media_box, self.user_unit) # TODO: Refactor this giant loop into two functions (scale to fit and no scaling) i = 0 @@ -443,19 +506,7 @@ def run( if not page_names[i]: continue - if self.col_major: - c = math.floor(i / self.rows) - r = i % self.rows - else: - r = math.floor(i / self.cols) - c = i % self.cols - - if self.right_to_left: - c = self.cols - c - 1 - - if self.bottom_to_top: - r = self.rows - r - 1 - + r, c = self.grid_position(i) scale_factor = 1 if page_box_defined: @@ -492,30 +543,8 @@ def run( horizontal_space = col_width[c] - pw[i] vertical_space = row_height[r] - ph[i] - # calculate shift - if self.horizontal_align is SW_ALIGN_H.LEFT: - shift_right = 0 - elif self.horizontal_align is SW_ALIGN_H.MID: - shift_right = round(horizontal_space / 2) - elif self.horizontal_align is SW_ALIGN_H.RIGHT: - shift_right = round(horizontal_space) - if self.vertical_align is SW_ALIGN_V.BOTTOM: - shift_up = 0 - elif self.vertical_align is SW_ALIGN_V.MID: - shift_up = round(vertical_space / 2) - elif self.vertical_align is SW_ALIGN_V.TOP: - shift_up = round(vertical_space) - - # invert shift if we are rotating - if self.rotation == SW_ROTATION.CLOCKWISE: - shift_up *= -1 - elif self.rotation == SW_ROTATION.COUNTERCLOCKWISE: - shift_right *= -1 - elif self.rotation == SW_ROTATION.TURNAROUND: - shift_right *= -1 - shift_up *= -1 - # apply shift + shift_right, shift_up = self.calc_shift(horizontal_space, vertical_space) x0 += shift_right y0 += shift_up @@ -559,8 +588,8 @@ def run( Resources=pikepdf.Dictionary(XObject=content_dict), Contents=pikepdf.Stream(new_doc, content_txt.encode()), ) - if user_unit != 1: - tiled_page.UserUnit = user_unit + if self.user_unit != 1: + tiled_page.UserUnit = self.user_unit new_doc.pages.append(tiled_page) diff --git a/pdfstitcher/utils.py b/pdfstitcher/utils.py index 19259d3..291c706 100644 --- a/pdfstitcher/utils.py +++ b/pdfstitcher/utils.py @@ -61,27 +61,27 @@ def __str__(self): elif self == UNITS.POINTS: return _("pt") - def units_to_px(self, val): + def units_to_px(self, val: float, user_unit: float = 1): """ Converts from current units to pixels. """ if self == UNITS.INCHES: - return val * 72 + return val * 72 / user_unit elif self == UNITS.CENTIMETERS: - return val * 72 / 2.54 + return val * 72 / user_unit / 2.54 elif self == UNITS.POINTS: - return val + return val / user_unit - def px_to_units(self, val): + def px_to_units(self, val: float, user_unit: float = 1): """ Converts from pixels to current units. """ if self == UNITS.INCHES: - return val / 72 + return user_unit * val / 72 elif self == UNITS.CENTIMETERS: - return val / 72 * 2.54 + return user_unit * val / 72 * 2.54 elif self == UNITS.POINTS: - return val + return user_unit * val def unit_representer(dumper, data): @@ -303,11 +303,13 @@ def init_new_doc(pdf): return new_doc -def get_page_dims(page, global_rotation=0): +def get_page_dims( + page, global_rotation: float = 0, target_user_unit: float = 1 +) -> tuple[float, float]: """ Helper function to calculate the page dimensions Returns width, height as observed by the user - (taking rotation into account) + (taking rotation and UserUnit into account) """ # The mediabox is typically specified as # [lower left x, lower left y, upper left x, upper left y], @@ -316,6 +318,14 @@ def get_page_dims(page, global_rotation=0): page_width = float(abs(mbox[2] - mbox[0])) page_height = float(abs(mbox[3] - mbox[1])) + page_uu = 1 + if "/UserUnit" in page.keys(): + page_uu = float(page.UserUnit) + + # scale according to the page and target user units + page_width *= page_uu / target_user_unit + page_height *= page_uu / target_user_unit + # global_rotation is defined by the document root, but # may be overridden on a specific page if "/Rotate" in page.keys(): @@ -330,7 +340,7 @@ def get_page_dims(page, global_rotation=0): return page_width, page_height -def print_media_box(media_box, user_unit=1): +def print_media_box(media_box, user_unit: float = 1) -> None: """ Display the media box in the requested units. Also checks to see if the size exceeds Adobe's max size. @@ -342,7 +352,7 @@ def print_media_box(media_box, user_unit=1): print(62 * "*") print( _("Warning! Output is larger than {} {}, may not open correctly.").format( - round(Config.general["units"].px_to_units(MAX_SIZE_PX)), + round(Config.general["units"].px_to_units(MAX_SIZE_PX, user_unit)), Config.general["units"], ) ) @@ -351,8 +361,8 @@ def print_media_box(media_box, user_unit=1): print( _("Output size:") + " {:0.2f} x {:0.2f} {}".format( - user_unit * Config.general["units"].px_to_units(width), - user_unit * Config.general["units"].px_to_units(height), + Config.general["units"].px_to_units(width, user_unit), + Config.general["units"].px_to_units(height, user_unit), Config.general["units"], ) ) diff --git a/tests/gs_test.sh b/tests/gs_test.sh index 9e90113..30dcbc6 100755 --- a/tests/gs_test.sh +++ b/tests/gs_test.sh @@ -1,7 +1,13 @@ # env /bin/bash -echo "Running gs on all pdfs in tests folder" -for pdf in *.pdf; do +pdfs=`ls *.pdf` +if [[ $# -eq 0 ]] ; then + echo "Running gs on all pdfs in tests folder" +else + pdfs="$@" +fi + +for pdf in $pdfs; do echo "Checking $pdf..." gs -dNOPAUSE -dBATCH -sDEVICE=nullpage "$pdf" | grep -i 'warn\|err' done -echo "Done" \ No newline at end of file +echo "Done" diff --git a/tests/large-canvas-no-editing-preserved.pdf b/tests/large-canvas-no-editing-preserved.pdf new file mode 100644 index 0000000..f414a7d Binary files /dev/null and b/tests/large-canvas-no-editing-preserved.pdf differ diff --git a/tests/large-canvas.pdf b/tests/large-canvas.pdf new file mode 100644 index 0000000..4a6e0cb Binary files /dev/null and b/tests/large-canvas.pdf differ diff --git a/tests/userunit_10_2page.pdf b/tests/userunit_10_2page.pdf new file mode 100644 index 0000000..d44d66e Binary files /dev/null and b/tests/userunit_10_2page.pdf differ diff --git a/tests/userunit_mixed.pdf b/tests/userunit_mixed.pdf new file mode 100644 index 0000000..b4db93b Binary files /dev/null and b/tests/userunit_mixed.pdf differ