# HG changeset patch # User bgruening # Date 1725010051 0 # Node ID a34826ae0a73b7d8004257cae90116967a2bdb2b # Parent b61022e1b807f177eb77bcbe6259188868e961fb planemo upload for repository https://github.com/ERGA-consortium/EARs/tree/main commit e293d14e82a903a4cab64dd72dfa3f3798466176 diff -r b61022e1b807 -r a34826ae0a73 macros.xml --- a/macros.xml Tue Jul 09 07:48:46 2024 +0000 +++ b/macros.xml Fri Aug 30 09:27:31 2024 +0000 @@ -1,6 +1,6 @@ - 1.0.0 - 1 + 24.08.26 + 0 23.2 @@ -21,4 +21,26 @@ + +
+ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff -r b61022e1b807 -r a34826ae0a73 make_EAR.py --- a/make_EAR.py Tue Jul 09 07:48:46 2024 +0000 +++ b/make_EAR.py Fri Aug 30 09:27:31 2024 +0000 @@ -1,6 +1,5 @@ import argparse -import glob import logging import math import os @@ -22,7 +21,7 @@ # CAUTION: This is for the Galaxy version! # by Diego De Panis # ERGA Sequencing and Assembly Committee -EAR_version = "v24.05.20_glxy_beta" +EAR_version = "v24.08.26" def make_report(yaml_file): @@ -120,19 +119,9 @@ fifth_column_value = target_line.split('\t')[4].strip() return fifth_column_value except Exception as e: - logging.warning(f"Error reading {file_path}: {str(e)}") + logging.error(f"Error reading {file_path} for tool {tool} and haplotype {haplotype}: {str(e)}") return '' - # Getting kmer plots for curated asm - def get_png_files(dir_path): - png_files = glob.glob(f"{dir_path}/*.ln.png") - if len(png_files) < 4: - logging.warning(f"Warning: Less than 4 png files found in {dir_path}. If this is diploid, some images may be missing.") - # fill missing with None - while len(png_files) < 4: - png_files.append(None) - return png_files[:4] - # get unique part in file names def find_unique_parts(file1, file2): # Split filenames into parts @@ -141,7 +130,6 @@ # Find unique parts unique_parts1 = [part for part in parts1 if part not in parts2] unique_parts2 = [part for part in parts2 if part not in parts1] - return ' '.join(unique_parts1), ' '.join(unique_parts2) # extract BUSCO values @@ -274,33 +262,34 @@ # Parse pipeline and generate "tree" def generate_pipeline_tree(pipeline_data): tree_lines = [] - indent = " " * 2 # Adjust indent spacing as needed - - for tool_version_param in pipeline_data: - parts = tool_version_param.split('|') - tool_version = parts[0] - tool, version = tool_version.split('_v') if '_v' in tool_version else (tool_version, "NA") + indent = " " * 2 # Adjust indent spacing - # Handle parameters: join all but the first (which is tool_version) with ', ' - param_text = ', '.join(parts[1:]) if len(parts) > 1 else "NA" + if isinstance(pipeline_data, dict): + for tool, version_param in pipeline_data.items(): + # Tool line + tool_line = f"- {tool}" + tree_lines.append(tool_line) - # Tool line - tool_line = f"- {tool}" - tree_lines.append(tool_line) + # Convert version_param to string and split + version_param_str = str(version_param) + parts = version_param_str.split('/') + version = parts[0] + params = [p for p in parts[1:] if p] # This will remove empty strings - # Version line - version_line = f"{indent*2}|_ ver: {version}" - tree_lines.append(version_line) + # Version line + version_line = f"{indent * 2}|_ ver: {version}" + tree_lines.append(version_line) - # Param line(s) - if param_text != "NA": - for param in param_text.split(','): - param = param.strip() - param_line = f"{indent*2}|_ key param: {param if param else 'NA'}" + # Param line(s) + if params: + for param in params: + param_line = f"{indent * 2}|_ key param: {param}" + tree_lines.append(param_line) + else: + param_line = f"{indent * 2}|_ key param: NA" tree_lines.append(param_line) - else: - param_line = f"{indent*2}|_ key param: NA" - tree_lines.append(param_line) + else: + tree_lines.append("Invalid pipeline data format") # Join lines with HTML break for paragraph tree_diagram = "
".join(tree_lines) @@ -330,10 +319,10 @@ tags = yaml_data["Tags"] # Check if tag is valid - valid_tags = ["ERGA-BGE", "ERGA-Pilot", "ERGA-Satellite"] + valid_tags = ["ERGA-BGE", "ERGA-Pilot", "ERGA-Community", "ERGA-testing"] if tags not in valid_tags: tags += "[INVALID TAG]" - logging.warning("# SAMPLE INFORMATION section in the yaml file contains an invalid tag. Valid tags are ERGA-BGE, ERGA-Pilot and ERGA-Satellite") + logging.warning("# SAMPLE INFORMATION section in the yaml file contains an invalid tag. Valid tags are ERGA-BGE, ERGA-Pilot and ERGA-Community.") # Get data from GoaT based on species name # urllib.parse.quote to handle special characters and spaces in the species name @@ -401,16 +390,15 @@ # Create a list of lists for the table table_data = [headers, data_values] - # Extract pipeline data from 'Pre-curation' category - asm_pipeline_data = yaml_data.get('ASSEMBLIES', {}).get('Pre-curation', {}).get('pipeline', []) - asm_pipeline_tree = generate_pipeline_tree(asm_pipeline_data) + # Extract pipeline data + asm_pipeline_data = yaml_data.get('PIPELINES', {}).get('Assembly', {}) + curation_pipeline_data = yaml_data.get('PIPELINES', {}).get('Curation', {}) # Extract pipeline data from 'Curated' category - curation_pipeline_data = yaml_data.get('ASSEMBLIES', {}).get('Curated', {}).get('pipeline', []) + asm_pipeline_tree = generate_pipeline_tree(asm_pipeline_data) curation_pipeline_tree = generate_pipeline_tree(curation_pipeline_data) # Reading GENOME PROFILING DATA section from yaml ############################################# - profiling_data = yaml_data.get('PROFILING') # Check if profiling_data is available @@ -418,38 +406,46 @@ logging.error('Error: No profiling data found in the YAML file.') sys.exit(1) - # Handle GenomeScope specific processing + # Check for GenomeScope data (mandatory) genomescope_data = profiling_data.get('GenomeScope') - if genomescope_data: - summary_file = genomescope_data.get('genomescope_summary_txt') - if summary_file and os.path.exists(summary_file): - with open(summary_file, "r") as f: - summary_txt = f.read() - genome_haploid_length = re.search(r"Genome Haploid Length\s+([\d,]+) bp", summary_txt).group(1) - proposed_ploidy_match = re.search(r"p = (\d+)", summary_txt) - proposed_ploidy = proposed_ploidy_match.group(1) if proposed_ploidy_match else 'NA' - else: - logging.error(f"File {summary_file} not found for GenomeScope.") - sys.exit(1) - else: - logging.error("GenomeScope data is missing in the PROFILING section.") + if not genomescope_data: + logging.error("Error: GenomeScope data is missing in the YAML file. This is mandatory.") + sys.exit(1) + + genomescope_summary = genomescope_data.get('genomescope_summary_txt') + if not genomescope_summary: + logging.error("Error: GenomeScope summary file path is missing in the YAML file.") sys.exit(1) - # Handle Smudgeplot specific processing + # Read the content of the GenomeScope summary file + try: + with open(genomescope_summary, "r") as f: + summary_txt = f.read() + # Extract values from summary.txt + genome_haploid_length = re.search(r"Genome Haploid Length\s+([\d,]+) bp", summary_txt).group(1) + proposed_ploidy = re.search(r"p = (\d+)", summary_txt).group(1) + except Exception as e: + logging.error(f"Error reading GenomeScope summary file: {str(e)}") + sys.exit(1) + + # Check for Smudgeplot data (optional) smudgeplot_data = profiling_data.get('Smudgeplot') if smudgeplot_data: - verbose_summary_file = smudgeplot_data.get('smudgeplot_verbose_summary_txt') - if verbose_summary_file and os.path.exists(verbose_summary_file): - with open(verbose_summary_file, "r") as f: - smud_summary_txt = f.readlines() - for line in smud_summary_txt: - if line.startswith("* Proposed ploidy"): - proposed_ploidy = line.split(":")[1].strip() - break + smudgeplot_summary = smudgeplot_data.get('smudgeplot_verbose_summary_txt') + if smudgeplot_summary: + try: + with open(smudgeplot_summary, "r") as f: + smud_summary_txt = f.readlines() + for line in smud_summary_txt: + if line.startswith("* Proposed ploidy"): + proposed_ploidy = line.split(":")[1].strip() + break + except Exception as e: + logging.warning(f"Error reading Smudgeplot summary file: {str(e)}. Using GenomeScope ploidy.") else: - logging.warning(f"Verbose summary file {verbose_summary_file} not found for Smudgeplot; skipping detailed Smudgeplot analysis.") + logging.warning("Smudgeplot summary file path is missing. Using GenomeScope ploidy.") else: - logging.warning("Smudgeplot data is missing in the PROFILING section; skipping Smudgeplot analysis.") + logging.info("Smudgeplot data not provided. Using GenomeScope ploidy.") # Reading ASSEMBLY DATA section from yaml ##################################################### @@ -459,7 +455,7 @@ asm_stages = [] for asm_stage, stage_properties in asm_data.items(): for haplotypes in stage_properties.keys(): - if haplotypes != 'pipeline' and haplotypes not in asm_stages: + if haplotypes not in asm_stages: asm_stages.append(haplotypes) # get gfastats-based data @@ -483,7 +479,7 @@ except (ValueError, ZeroDivisionError): gaps_per_gbp_data[(asm_stage, haplotypes)] = '' - # Define the contigging table (column names) DON'T MOVE THIS AGAIN!!!!!!! + # Define the contigging table (column names) asm_table_data = [["Metrics"] + [f'{asm_stage} \n {haplotypes}' for asm_stage in asm_data for haplotypes in asm_stages if haplotypes in asm_data[asm_stage]]] # Fill the table with the gfastats data @@ -493,8 +489,6 @@ asm_table_data.append([metric] + [format_number(gfastats_data.get((asm_stage, haplotypes), [''])[i]) if (asm_stage, haplotypes) in gfastats_data else '' for asm_stage in asm_data for haplotypes in asm_stages if haplotypes in asm_data[asm_stage]]) # Add the gaps/gbp in between - gc_index = display_names.index("GC %") - gc_index asm_table_data.insert(gaps_index + 1, ['Gaps/Gbp'] + [format_number(gaps_per_gbp_data.get((asm_stage, haplotypes), '')) for asm_stage in asm_data for haplotypes in asm_stages if haplotypes in asm_data[asm_stage]]) # get QV, Kmer completeness and BUSCO data @@ -502,7 +496,7 @@ completeness_data = {} busco_data = {metric: {} for metric in ['BUSCO sing.', 'BUSCO dupl.', 'BUSCO frag.', 'BUSCO miss.']} for asm_stage, stage_properties in asm_data.items(): - asm_stage_elements = [element for element in stage_properties.keys() if element != 'pipeline'] + asm_stage_elements = list(stage_properties.keys()) for i, haplotypes in enumerate(asm_stage_elements): haplotype_properties = stage_properties[haplotypes] if isinstance(haplotype_properties, dict): @@ -580,7 +574,7 @@ styles.add(ParagraphStyle(name='subTitleStyle', fontName='Courier', fontSize=16)) styles.add(ParagraphStyle(name='normalStyle', fontName='Courier', fontSize=12)) styles.add(ParagraphStyle(name='midiStyle', fontName='Courier', fontSize=10)) - styles.add(ParagraphStyle(name='LinkStyle', fontName='Courier', fontSize=10, textColor='blue', underline=True)) + # styles.add(ParagraphStyle(name='LinkStyle', fontName='Courier', fontSize=10, textColor='blue', underline=True)) styles.add(ParagraphStyle(name='treeStyle', fontName='Courier', fontSize=10, leftIndent=12)) styles.add(ParagraphStyle(name='miniStyle', fontName='Courier', fontSize=8)) styles.add(ParagraphStyle(name='FileNameStyle', fontName='Courier', fontSize=6)) @@ -659,7 +653,7 @@ # Iterate over haplotypes in the Curated category to get data for EBP metrics curated_assemblies = yaml_data.get('ASSEMBLIES', {}).get('Curated', {}) - haplotype_names = [key for key in curated_assemblies.keys() if key != 'pipeline'] + haplotype_names = list(curated_assemblies.keys()) for haplotype in haplotype_names: properties = curated_assemblies[haplotype] @@ -756,7 +750,7 @@ # Store BUSCO version and lineage information from each file in list busco_info_list = [] for asm_stages, stage_properties in asm_data.items(): - for haplotype_keys, haplotype_properties in stage_properties.items(): + for i, haplotype_properties in stage_properties.items(): if isinstance(haplotype_properties, dict): if 'busco_short_summary_txt' in haplotype_properties: busco_version, lineage_info = extract_busco_info(haplotype_properties['busco_short_summary_txt']) @@ -787,9 +781,9 @@ tool_count = 0 # Add title and images for each step - for idx, (asm_stages, stage_properties) in enumerate(asm_data.items(), 1): + for asm_stages, stage_properties in asm_data.items(): if asm_stages == 'Curated': - tool_elements = [element for element in stage_properties.keys() if element != 'pipeline'] + tool_elements = list(stage_properties.keys()) images_with_names = [] @@ -825,7 +819,7 @@ # Add images and names to the elements in pairs for i in range(0, len(images_with_names), 4): # Process two images (and their names) at a time - elements_to_add = images_with_names[i:i + 4] + elements_to_add = images_with_names[i: i + 4] # Create table for the images and names table = Table(elements_to_add) @@ -856,7 +850,6 @@ # Iterate over haplotypes in the Curated category to get K-mer spectra images curated_assemblies = yaml_data.get('ASSEMBLIES', {}).get('Curated', {}) - haplotype_names = [key for key in curated_assemblies.keys() if key != 'pipeline'] # Get paths for spectra files spectra_files = { @@ -974,9 +967,9 @@ tool_count = 0 # Add title and images for each step - for idx, (asm_stages, stage_properties) in enumerate(asm_data.items(), 1): + for asm_stages, stage_properties in asm_data.items(): if asm_stages == 'Curated': # Check if the current stage is 'Curated' - tool_elements = [element for element in stage_properties.keys() if element != 'pipeline'] + tool_elements = list(stage_properties.keys()) for haplotype in tool_elements: haplotype_properties = stage_properties[haplotype] diff -r b61022e1b807 -r a34826ae0a73 make_EAR.xml --- a/make_EAR.xml Tue Jul 09 07:48:46 2024 +0000 +++ b/make_EAR.xml Fri Aug 30 09:27:31 2024 +0000 @@ -39,7 +39,6 @@ # ASSEMBLY DATA ASSEMBLIES: Pre-curation: - pipeline: [Hifiasm_v0.19.4|HiC|l0, Purge_Dups_v1.2.6|, Bionano_vGalaxy_3.7.0, YaHS_v1.1] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${pre_curation_assembly_data.gfstats_nstar_report_precuration}' busco_short_summary_txt: '${pre_curation_assembly_data.busco_short_summary_precuration}' @@ -52,7 +51,6 @@ merqury_completeness_stats: '${pre_curation_assembly_data.hap2_precuration_data.merqury_completeness_stats_hap2_precuration}' Curated: - pipeline: [GRIT_rapid_v2.0, HiGlass_v1.0] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${curated_assembly_data.gfstats_nstar_report_curated}' busco_short_summary_txt: '${curated_assembly_data.busco_short_summary_curated}' @@ -80,7 +78,6 @@ # ASSEMBLY DATA ASSEMBLIES: Pre-curation: - pipeline: [Hifiasm_v0.19.4|HiC|l0, Purge_Dups_v1.2.6|, Bionano_vGalaxy_3.7.0, YaHS_v1.1] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${pre_curation_assembly_data.gfstats_nstar_report_precuration}' busco_short_summary_txt: '${pre_curation_assembly_data.busco_short_summary_precuration}' @@ -88,7 +85,6 @@ merqury_completeness_stats: '${pre_curation_assembly_data.merqury_completeness_stats_precuration}' Curated: - pipeline: [GRIT_rapid_v2.0, HiGlass_v1.0] '${pre_curation_assembly_data.haplotype_selection}': gfastats--nstar-report_txt: '${curated_assembly_data.gfstats_nstar_report_curated}' busco_short_summary_txt: '${curated_assembly_data.busco_short_summary_curated}' @@ -102,6 +98,18 @@ blobplot_cont_png: '${curated_assembly_data.blobplot_cont_curated}' #end if +# METHODS DATA +PIPELINES: + Assembly: + #for $repeat in $method_data.assembly_method_info: + ${repeat.assembly_tools_info} + #end for + + Curation: + #for $repeat in $method_data.curation_method_info: + ${repeat.curation_tools_info} + #end for + # CURATION NOTES NOTES: Obs_Haploid_num: '${curation_notes.obs_haploid_num}' @@ -131,7 +139,7 @@ - + @@ -150,7 +158,6 @@
- @@ -177,7 +184,6 @@
- @@ -210,6 +216,16 @@
+ +
+ + + + + + +
+
@@ -282,6 +298,7 @@
+
@@ -356,6 +373,7 @@
+
diff -r b61022e1b807 -r a34826ae0a73 test-data/EAR.pdf --- a/test-data/EAR.pdf Tue Jul 09 07:48:46 2024 +0000 +++ b/test-data/EAR.pdf Fri Aug 30 09:27:31 2024 +0000 @@ -153,7 +153,7 @@ endobj 20 0 obj << -/Author (\(anonymous\)) /CreationDate (D:20240704122313+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20240704122313+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) +/Author (\(anonymous\)) /CreationDate (D:20240829084627+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20240829084627+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False >> endobj @@ -164,10 +164,10 @@ endobj 22 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1481 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1471 >> stream -Gat=+D,91O&H9tY(p.mXG?$ur]JSl.P84Y<^#NLOkd2FnEIluM^TmAs>*>+='f6gn1\6Pgo>Dq_j+j$%o*jP6Js-QDKL3$Gi`[s&=)]3$rB)8=d\SW(-<:\2!IE[UD&24'#^o8XmPo_F_cRVOD.6?_BlnfJq&)@4"j^fF(@Ls$8G*Rh*tWkKBl3#^G"(!mIqK"Clj9>H9!tD4#o]F=\M,"a78EUq-\Ns]`f['/F#n1n85d+_?t-5#Uo%h,HY5J&J$4ELL^9.3@m@&7/\h?Z_UZYZNFp.Anl#jRoO/0Q&++-lWX"1/G'W44(E&!!Z:+Am#XL2BNPkQJ[]/8+2qFZ*LU5"WbR,sEqli8!YCZYoeu;2L.$FTi4H*n*'\CD+b6>Y_Q:B>g6(J;c_8rsB\;L-]`)ru=dhBD")CGOX.)K$#]2%I4Qg9ClMOCkJcpFKOH*:EWD2maQa')V(N:tZkV_iis3g3$a88o)0eeE^E>7M8>b^*JroI7YihqiXcZ)bDF_;eIYR>4R>Ml^.0QSd+8mV7i3.QQ3`(*4sIT^i*S>:;9-8Pt^J7UKb5,+?@3/SEh1@8)-j_d0eu8UoAT2nn#.U:E3B$D3W+\kN4>7X!YQ=QV4E3'enYR2s!Ro(KncL(`*O5S'>L!m-k:=Mj)*]A*"%5;ImQnFe'!XlLnO#*I@LlfR4)Be7oH)dK`@Kca)ROn=!m;$Ti9?iBEF6s2$!R2dE[]Gu9)q0g4YnV[RUOah&5dS`*W-@A3rUT'OT@4MnStTY^M$WVL7lnpIE;O:"\\4#k!g/[O>].b&h6YFRJO+.2u!Hr.:23sj[4Z'3Zlmu&sQbD+LLoq$QQQ1!F4Mcat>3L`A23\s-8_3r@ZbEBLaE`;a@&>O"ACWQsEmn+-!4Q$R8X29I<&gcZjOiK4c.4iZCdM!,#jI'];CIVnc'Ij9e:8\O"U!;R$f-L\YA;826M(9p>*-8#,>8K8LP-6lEOQ8*&8T1\+2&0l<7R9`=7kP0T8%D6*44VNg_pSLooqXb~>endstream +Gat=+hbW8l&:WfG(p.mXG?$ur3,-7I8INRZHcNr(eS-+ME>uC3.Z(ct$hXGp)`p8T99QTZJk"d=B"kN_^:4;Nr"/aTScP?UiT(@I6=IG*_Aid//%CrfrB)8=P#7U"'.43`&3Y!K[0p_pKYF5gG9"IV_cRVOCL^3TBln?=q&)@4Kudlj(@K+E.fBsV5@F?0Uq+IjDQ8QNUtT/_]RqcjRRC.P8`Gjf`sj%-$tf3)De:YKBBCXM(8kOuiV;d1@PBt0/t>>$6bQ%sJa9a830EN^+bfl5Um2l`ikkohE,;,1JAimpBKd2>Qcog,Fs-2#Cb>"M(*q]cR"S37OZi<`o_IepXjYoOiaPDX0H,4qKX:tu>&WDen`85L^lpG.!1=,IZZ^e(ITp[(V#6AA7i3^'!2!HhqmDe=U?Epa#r#-Y`%5Vk`!M2&-K>2.dE"A=VnD5MC\_&993u9Xf.nBS)-H*^MM:=bKi*K6hSdN0nU/&Cg*'1GrIo_cW^FP54E>goC:Ye13=`)B8`AO[h8\aJf\@7B?LRHGBOf'&g)`d3m-E7W[2=Wu:)+#ZC'[BMmk3gE8F7Im($LXBr:n?Ia?&37,ko#<:*A)Cj-8luTCf5lN[]Z7Z>V>ciOPJ6g(aOh!%lV0;'CN[M<<`T^?eISEADU_p]!qs8TZrDI=+;LuRnt]\QM##-K,N[,g1>=u2XG6Zl#?,O-fRk>)l(/rVh5Dljh6J8RS]@h3WfCg+Nd*HC)C3K[RLd$)McO&k_K(,a?kL'$A$I)9\qD4JC=1V**G/!P)UFpF=&$iN($SCMj8@qa,Du%5OVW1,#0_9RIX0QWODE?mZG&b>D_$ZV9F7/ef3_S#a'Pi=QhA-]8/Jau(`7bWo.mH9,.Vlb+oso^I".q\1$KeJ`V-&!CCoA]Hmj29Gh5p>405I3`b2Kh%_PKOL.'U&_mdB$?`>)pgoN34J.PpcdL.mYfWJ1G<9$O?Z&[%+=1*ijslYUltM:H79)A?p?]);2"9W"cm*SMKtq2N&MZ=!6ml=XGdIY..XBbakVY?/P]4YngS3P/D[Dr3894<9iih"Yn+@Kdob:jXQtk<%n+Y8,i[M-(l[~>endstream endobj 23 0 obj << @@ -188,7 +188,7 @@ /Filter [ /ASCII85Decode /FlateDecode ] /Length 501 >> stream -GatUo]l"cD&;9q-MCmoi4EHi^EnM@f)lF9e7Cn#a!J2;USE.P*Dd&$0fhqks#(^RPAnSk%LnOX]rAW%&2\THR+s=_O!-r+B5/X:4pbh!u5FK7#MoTg0@%-sD!n-sKoJC9)VrkS+p1MX^-,QNF^0fm&(5U^^S^WXT/""&go\h6?h9YCl'99VA,iULm'DQsO4jM.VY@&?3KTXC)_+7$b6qmnHa?_kO1L/gTQMJ]NNS8-P`^U_"-YIIf%G-pEq6X[D[4QOo*P,[4HVkO^EImk:C0R)PariuC]?M=k4KE5;B)gr,Ft$/)al9J*].\JBUsTNcf`MmokHJ]beP2m)sWVkhu2VGd&f4L2F[j;1dRnKR8pr!eeYa+prA@!Dg!endstream +GatUo9lC\2%#46J.sqarZaDsRf*ZhT9;F^i6_5qVe5]li%fC[N$!kt++OOr"\TmN9I@hMn?pAbGo*fS"_9Eu$0h`%tN!?/?n9u:i&+o`l_r-.h6\qF[oE$'AbEnpsdQ1,,R-$SjJak^X+!thgW4=Obs&S`"&_Jt^58H9/!j^_83Sj^_"4L>=hEGUjqg#3Bj/W!X`$UMCUUdaRN,rUi8Jh[Pib7!8WKD<3^3b"_0M,=Kn[\ZE>RJr9&lQHsY`cW<`P=ZbrOV6*l+e[-sdgEq%B`ACTJMI1gRgl9#@^[j)?&S+:>SH!,oG4(`lendstream endobj 26 0 obj << @@ -199,10 +199,10 @@ endobj 27 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 820 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 825 >> stream -Gau`R9lo#B&A@Zcp.-ul,0@]f\Ift29W)TVHg$;`)FKedK,SEeRJuuaAK)'1Zsi6C(pIWHl0[6UC,#gJ+(fsdLB(Bs>[B3o*5"9c5m2H$@-"`0[9/G'*$"2S.:%clcp=sTJ/:tjhClJ4Rl4V8(,(5K?k\FI`@a*\k?0O=H;-\`q,`r9$2N7:$)J=]%F*eIJpCjt(CXfnJk*CEcpr,A&'!$^*qDG7TiXcjbGro"s7E>GMWkMW0E,H-2/j!2db.[mmrAUH;<2Y1V"$XVI1jeXUT92N'/SZ1J#pHa_Wg*1kVlX5%]=gl/].<]4IQEia>c6@ZE6_h[.RQ?qd#3]_O.CuNr:&8kW;q67**0e&'P75TF1'GNU%h#S2^o%M5lfY)@."33erDm!\n5@cck(5ZVA0W99M873"YoI<(t0AD5gaYm(lA)%XSr[H!r;3F4_>#+J#_Yn-hI64$s[,ctt_KX1Dh#LrPJXZ&o&;Em'^K`$>lj@#%2[;'Z>1mq9::KY!4-b0'N5>GO,,qF3$FRp(/obs$IS+oVZ_t[bLEllT8e=Yc*W\uf/QObVaI68CjWIAVW?1tfan>4S='g9E;LOKbP-l3ohF=82kk]*ZD//X>ChLR:mr;q2qU(7~>endstream +Gau`R9lo#B&A@Zcp.-ul,0<0;\KrMPAif<)\;-XmV(nrIIo$5]16Wg;[?/4PQo7Xo%Kk;K5.uBV=tO>On\a_^f*("oK[VFUN'fQ*-A[3A"'gS_+"cW:JqL7SU/[e)#r^*"_!!f*&t?2]O5J]kdcF^SkVlu]n>ed_NTTN#!\S#Upg\.FYO+7='_-ROb]d`L'noSXV1\Bi]lY'i?:J;j*UKI[gOFkB9(:MK8Y,eYii]G,2NjbMjnbH,5Na(e3(K=%9c0UME]jhQr\u.k<6=WKjLDDOo8h:mmBbP.%1$>6\JPha520K%<-2X/'T+@T*S5m10oe\f84`hY<^18=/WX.[rKm1.dIVcTI2@J0eI2a-=m_;l;3^Wg1H?*['Z(Lb^>FB_KGQ(Ko>;*?ZO,G%!4DDG?&-k5gKlj\GU_.QGgc<-mR."p'Q3/eeh:2MUr<~>endstream endobj xref 0 28 @@ -229,15 +229,15 @@ 0000330272 00000 n 0000330556 00000 n 0000330649 00000 n -0000332222 00000 n -0000333508 00000 n -0000333921 00000 n -0000334513 00000 n -0000335058 00000 n +0000332212 00000 n +0000333498 00000 n +0000333911 00000 n +0000334503 00000 n +0000335048 00000 n trailer << /ID -[] +[<8c056efa70858ae5ac5250f2ea61c6b1><8c056efa70858ae5ac5250f2ea61c6b1>] % ReportLab generated PDF document -- digest (http://www.reportlab.com) /Info 20 0 R @@ -245,5 +245,5 @@ /Size 28 >> startxref -335969 +335964 %%EOF diff -r b61022e1b807 -r a34826ae0a73 test-data/EAR_2.pdf --- a/test-data/EAR_2.pdf Tue Jul 09 07:48:46 2024 +0000 +++ b/test-data/EAR_2.pdf Fri Aug 30 09:27:31 2024 +0000 @@ -180,7 +180,7 @@ endobj 23 0 obj << -/Author (\(anonymous\)) /CreationDate (D:20240704122404+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20240704122404+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) +/Author (\(anonymous\)) /CreationDate (D:20240829084715+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20240829084715+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False >> endobj @@ -191,10 +191,10 @@ endobj 25 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1506 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1495 >> stream -Gau0CgMYb*&:Ml+$jnQD(60S:(JB3ZTZJP6G-ac((lLQl-tnOIS=KL>+XPR50FC9M&;1(1i.>e]jPg;j8KI4UJ4YX'-rA[:U,p`\o&9EiI*?Z':6Q,LnME:UsrJ*'DZk_$Q1`0O]le%!k!1c`]!Ke!'JW+JE:BB,#l7gC4crBR<$1@K.9\A_N2i0L@mll6j\^rW+8/NrbUo]gA508@(-OMn?(p8N-u'J`T74:4FsCq=I1PZmL[_fNTY\L5nn.g4@aJ3:_rOiatF5fp1#R-H&BUS6m%=QCFq.4d#[KIHC7^;<.Ycij5$8m3G1cjsbkK,t[)EAh_89XJ[Nd)Ma6(jW/=Mrt9M"E[A[s7"$5,"20m1QgL^oXAQIm.%X%sc'E?*V!bJ5q"LOGoS;`GbCeuhHW$BtT^7gE*f]!YXnIe"DP4$&2ZJliApg\]ITThEZ*LriD8jQ<8tF[nO+l+@ZVFVH[C4u-*\ktO>":.9/R)#ohg&Flqb&^6l\K6s3'BU@(Q4.^4V7a>.!j@L,L2BcZY).\/fC%a7?]aa*!-$H:&U2]&N$!F*C12Y1%)dWtSQQAa+_LrG@XS[@HbA@*eFL$/&;b;Sbst:E$Tm>S#LLUmQF%B%'2dX^Jo`6'ke9G$kU(/2[!]V5o9e,CaE_(8F`7^OGQ#]*37X&mSYQmRolTm?WE?^!5G*6=J`;KYk;R$&*M/&diD+$,p<;N5\^_Sks5$6CuD"O'K3%o2=S2_j?Nb\%WILc)]7f(J)*"nLTHCSYf(uYXp%+XX-oNa<2.2kPb"0QP5$X]U[C..iYU)2/d;T;oRaT8"f\V>dG\VS!#/CKnLaVOYL6cle_(VkBm4N"'RDbHRX37cM__mT,VcSktRJBn01HBF.=iI'7Apjr+rK'MMF-#$/Tjq3j;^o)(^Ol8%>NgnsN$ISjg/N.m._sU\^&W=BuWek85^J0UEse?4-V?[ntf7#isW-Yb6;N^omN#SFlKo)9_U@`b'SemM>_l(Y1sR!H5DF@d0t7t&P!%oP;#i>kegdA]atendstream +Gau0CgMYb*&:Ml+$jnQD(60S:(JB3ZTZJP6G-ac((lLT=KskZ!Kd=5DS](Eu4/=Kp*D'!8/<%NanBB)oEor0)@,m$0s0EEtHidq^":0_'*/JC@0Y)bXaiVjB;BfBRUL7U95o?7"0%ac%/l'c[eDX.)R%tRW!jt`RWZqs*+0IiGk<.ql1tfYH?O<\q_W1qq53Xoupt8A!o'03O9>5J/KjBfh$S"\0;qJ;WMu(,T4jobO$'3:m@>fLAH4s'"Y:k7OPP2jbkZH=P"lS-YK7jun?4]rcs$g1[p*1W;&.b]88TL2J_MJ%u!+ng+&8)6r/Bu](DJMPdNIj`0q%/uN+&$"Ur6ZppM'=j$Y5\jsjfcA3WCf3Bh];\3I.k/EV4>acVX08g%_;OuS=:bV:"$WJZGMi?D]U7pNsHKO(oh8[/n*?V''>mps@J4":MOeEo;SVikVtA_[]n47"Ksr?r3TiVLL`i2,k5'^%G(]-MqtA%-6S]jnAfW+s#j5GY\)+"OB6elI1"XKGo7nZfAtVmbSp@5Z<2'@pHkmItNhbk$J<&IX$1ee@1lcEBqZEV*]3bn'2V33!49fQM8s4;00!M.EmVBG#h/9P]F,=):gSUNW_+#nCU%]pn.h5'LF`<*TP&PH>H08!(dkEd/Kr=&0o"R/!@D.kbOK'/K2rG=[SF_8^XLIuAMUB1T!\UuA7&^)f%RLXb]PST_D%EV3@u%_1%bK3YM>N/n!dT'&_eY"Fc_<`47lTH3*>aPJ)Zb\Ju'/fJU2q_\kpgrcl(6]boRToUa1,oe!%CHJm?N<9"UHXGGXR)>*Fg=g[V&diE`,ZO)Hr^IM-o=cIaepU+3d!qA=e,._PNf)&XOKOMO_9NNPKI=LsEb$b*@ou3TTK_ATEDt'#AB9R>OE;R8?!6T/XEQ+RJ+LN"04EBMGOQ:V3)G(2#`2c0sha-*c0g4CKif/D5o>!M`ln]sfE1p\mA]ZsjU?;Xd/Hqr8Y[2r_9oRk.Fq7?WIn3*Xl[pqfo'fij4H%r]H#]]8!o@'RL&=:S8iE)VhY1QeOsMf9h@Ghu+`ocXjM\gX/r0ik/Z/k$,['_lZdTsVEDaK^_461r*F?1in2he1X5)L>&^0e3&n2E@5erBKeqOu:jm&t\9ZQ:rf'h5l$pBQ3`1hj*u?'Xn,S'4k;99P88%&\aILdGntYIo@XU>Grhn!gEWoc>W0pI.Wf?,;\`3lurBPU@d*1=Vk7(1&4-&UpMp>))\g1-`LT^Q:7tDL/\OZj<`,$m.8B*337<)+N6"uHD'oblB^s&Q?NFM#6'Z!!j=kquI"C]=)0\MW!@<01=GET11Nh\9'MDGEU#.(/uLAFj_Tf+FNi]);I1=pX_X+WB0D>,&GrWA2Ch/W~>endstream endobj 26 0 obj << @@ -212,10 +212,10 @@ endobj 28 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 556 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 557 >> stream -GatU.9lErb&A71-pm<9$e[o/B]@RC9JP2h`i.ffi8;J[UKEM5q2k_C36Uci4HC&o`ZSQ)FD@GmHAS'*1Cp%]m!1LG;TS^nj_7Z6Rh\YeZ:I:7):!CJ,0`\Zf2P1e<$cdCUc+Ft,d%K.^_Y['NpNG(FB)A]qh+#Z#S+:'a]2jo/[r%DFrk(#X(Gih:f=W'o=P(_npBcKuPOh,e:YOP`3T6*gBTQdL%!Wnhk6\r_!>[jaLN"$\\*M<(j#Tsg3"A,#YsUfT^ucPT&r\Fl*bu$S@#;N#ae5a,+_Xde1T]rlg=hT>^`\2MNoUG;N8ZX?I1_RD+cMlW*N.e_MV9hm.XbQ@L@b4I3$q`#=;U^sLYAV"Kf!6kk7n\AD6ia"LbR?N'd~>endstream +GatUnc#-HG%#+G$$82M0gl=Oh)1.:7)q\-$$[-34OUtA4#n,U]8O>euL5HUH\f6]Yc?FXJlmOuDb6m1P$obX$82,6QSf$T#n8;Q[?f:C>9FrLF+t@)32!gp@q7IQNGZn__k1/*Q5k_F?p7%B:c)PVgqlQ.>-Gd8\q6mm]IiFaDJae0>uoEKH\aLmF'i]@Z8a;u/_!f8&:SbFX;(Q/2t2.#d[*23T#-e'thrS,"m7SHLd<%YhNiMHD@[tL?mM/eKmK$Ra!)K;TR;*#X8A_afYAeLAF]!TU3lNpf5ocj1eTj[BuMAdO.~>endstream endobj 29 0 obj << @@ -233,10 +233,10 @@ endobj 31 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 817 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 825 >> stream -Gau`R968f@&AI`dp.-ul,0@]f\Ift29W)TVHg$;`)FKed5n13C9RhHAZ4."Sg+[Y2%!ZFZe$i+)AjGn(bjX^+i6$\qCBf`M2[:[c!:Lqe][jP-bE7C/,97u)/+E`\W%fQU(7o)A\rHnH6nSt>)D7U:[>/M!@%"rS*VaQgHNNX[V8'po^'i]L\gX$b]&o;'_-uFBLeG?XE]AGU<"Y]82V9XeTn>\k_u*sskVU@C@@jB$8qg1pK#Q/S9_>0[9.6]roJpZ\YB;^[0mcE6V;k*8E=L?Y6l*ku0[bSgS.2@@m6U[+:Xm7bM9[D0#7C<4j3aUb0a"[aO=VB_H]YU#2u%gD$[Y*UOY6P[MB;o_.6ECf9T>?UQ#oelGG)fXELL*9/W,tIF>u].@1-ZuD.'BiUn,?\3IN79L:cJSZt6^e(fQW.SU2+JX-Fbj">@"/90^eNanoHR[k7gBQ:!&Z$ca0L6Jn2$1Zp*:,#2fq^j*Q*[W3$Vmp)nnR&c,YBjC$6>Q+=W/!r`@KG3$#TP4KSbU(-)cf+feA%bj9_IM_"IRjAmD*WEoEMX;!=/p\oS7T8au/nk1bGO_C=~>endstream +Gau`RgN)"%&:N^lp5rqk7?W?Vo0s"PAif;^gkS'J8n1bq^/+oG16[df>0()(V@?J5_gjVV7q^/N2$UPti,ZV`El?d9m_=3-`jr;Yh#MTS".Qp<^UgCdZg;&B)Z/JQ'J*F/J*-=t5nPktFV+3sZlN\B*oQnZs1OFZ(Dc/r$(1j%8`B=Te2.>=oD&:K3V;52h+Z',Oi^a?l8"m"iqUg4["2\!.5t=9OE@fbjB.eKpI37XBt2l%)$Y@a*Yk?0O1C8Qp];,`r9$;k3(.EL[E6cHT\l'P?VRXAJ'nDLmC'=%%-Y7U1RJ,tJdPr>eFX5WAId["TM2)l*p-MrMdmH-VGn#U[P`2b(AgUK^RR5-gY>)_3Pm9@5GL#IC'l5Qfjlc'tT;l(uIb80\>nMd`KW.i!R4kK(-qrb(k[fNCdRFF[X^A7R<^]ROp3j0*dM[@JA'S>/\usm)X<,f@],Ef4i&H$7>,\c5o[n-t,:O`dA9ZpCQYe1)1ldO6dn_bHql;Z6!%^^<.M6l2>A2W2jjEjE97ol>m\_G0kYJa;r=?ZO96%+Lj,];dbJTOS3BoKSt,jW/hpc!BbJ1@WgM"I7c[;TE2ND^ZDR.U1[u=@[TVeqRL\'("k7`\1KfUr*~>endstream endobj xref 0 32 @@ -266,16 +266,16 @@ 0000371924 00000 n 0000372208 00000 n 0000372309 00000 n -0000373907 00000 n -0000375591 00000 n -0000376043 00000 n -0000376690 00000 n -0000377240 00000 n -0000377710 00000 n +0000373896 00000 n +0000375580 00000 n +0000376032 00000 n +0000376680 00000 n +0000377230 00000 n +0000377700 00000 n trailer << /ID -[<8576d3492d1e783b7da52a0f2ceb1828><8576d3492d1e783b7da52a0f2ceb1828>] +[] % ReportLab generated PDF document -- digest (http://www.reportlab.com) /Info 23 0 R @@ -283,5 +283,5 @@ /Size 32 >> startxref -378618 +378616 %%EOF