No edit summary
(Replaced content with "Kotoba")
Tag: Replaced
 
(3 intermediate revisions by the same user not shown)
Line 1: Line 1:
#https://github.com/camelot-dev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools
Kotoba
#https://datascience.blog.wzb.eu/category/pdfs/
import os, sys, json, re, pathlib
import base64, io
import subprocess
import numpy as np
import pandas as pd
import requests
 
subprocess.run(["echo","$VIRTUAL_ENV"],shell=True)
baseDir = os.environ['HOME'] + '/lav/dauvi/portfolio/audit/'
fName = "foo"
fName = "am35"
fName = "iplex_nx"
fName = "AM5386"
#fName = "Policies"
fPath = baseDir + fName + '.pdf'
fUrl = "https://www.olympus-ims.com/en/rvi-products/iplex-nx/#!cms[focus]=cmsContent13653"
 
#-------------------------------------------------unstructured-----------------------------------
from langchain_community.document_loaders import UnstructuredPDFLoader
loader = UnstructuredPDFLoader(fPath, mode="elements")
data = loader.load()
 
from typing import Any
from pydantic import BaseModel
from unstructured.partition.pdf import partition_pdf
from langchain.chat_models import ChatOpenAI
from langchain.schema.messages import HumanMessage
from PIL import Image
 
elements = partition_pdf(filename=fPath,extract_images_in_pdf=True,infer_table_structure=True,chunking_strategy="by_title",max_characters=4000,new_after_n_chars=3800,combine_text_under_n_chars=2000,image_output_dir_path=baseDir+"pdfImages/")
 
llm = ChatOpenAI(model="gpt-4-vision-preview")
def image_to_base64(image_path):
    with Image.open(image_path) as image:
        buffered = io.BytesIO()
        image.save(buffered, format=image.format)
        img_str = base64.b64encode(buffered.getvalue())
        return img_str.decode('utf-8')
 
image_str = image_to_base64("static/pdfImages/figure-15-6.jpg")
chat = ChatOpenAI(model="gpt-4-vision-preview",max_tokens=1024)
msg = chat.invoke([HumanMessage(content=[{"type": "text", "text" : "Please give a summary of the image provided. Be descriptive"},{"type": "image_url","image_url": {"url": f"data:image/jpeg;base64,{image_str}"},},])])
msg.content
 
#-------------------------------------pypdfium2-------------------------------------------------
from langchain_community.document_loaders import PyPDFium2Loader
loader = PyPDFium2Loader(fPath)
data = loader.load()
 
#----------------------------------------pdfminer------------------------------------------------
 
from langchain_community.document_loaders import PDFMinerLoader
from langchain_community.document_loaders import PDFMinerPDFasHTMLLoader
 
loader = PDFMinerPDFasHTMLLoader(fPath)
data = loader.load()
 
#-----------------------------------------texatract----------------------------------------------
 
from langchain_community.document_loaders import AmazonTextractPDFLoader
from textractor.data.constants import TextractFeatures
from textractor import TExtractor
from textractor import Textractor
 
 
loader = AmazonTextractPDFLoader(baseDir + "szx7.png")
documents = loader.load()
extractor = TExtractor(profile_name="default")
document = extractor.analyze_document(
file_source=baseDir + "szx7.png",
features=[TextractFeatures.TABLES]
)
document.tables[0].to_excel(baseDir+"output.xlsx")
 
extractor = Textractor(profile_name="default")
from textractor.data.constants import TextractFeatures
document = extractor.analyze_document(
    file_source="tests/fixtures/form.png",
    features=[TextractFeatures.TABLES]
)
document.tables[0].to_excel("output.xlsx")
 
 
#-----------------------------------------azure------------------------------------------------
 
%pip install --upgrade --quiet  langchain langchain-community azure-ai-documentintelligence
from langchain_community.document_loaders import AzureAIDocumentIntelligenceLoader
loader = AzureAIDocumentIntelligenceLoader(api_endpoint="", api_key="", file_path=fPath, api_model="prebuilt-layout")
documents = loader.load()
 
#-------------------------------------------upstage---------------------------------------------
 
from langchain_upstage import UpstageLayoutAnalysisLoader
os.environ["UPSTAGE_DOCUMENT_AI_API_KEY"] = "YOUR_API_KEY"
loader = UpstageLayoutAnalysisLoader(fPath)
data = loader.load()
 
#----------------------------------------------agent-chunking-------------------------------------
 
from langchain.output_parsers.openai_tools import JsonOutputToolsParser
from langchain_community.chat_models import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain.chains import create_extraction_chain
from typing import Optional, List
from langchain.chains import create_extraction_chain_pydantic
from langchain_core.pydantic_v1 import BaseModel
from langchain import hub
 
obj = hub.pull("wfh/proposal-indexing")
llm = ChatOpenAI(model='gpt-4-1106-preview', openai_api_key = os.getenv("OPENAI_API_KEY", 'YouKey'))
runnable = obj | llm
 
class Sentences(BaseModel):
    sentences: List[str]
   
extraction_chain = create_extraction_chain_pydantic(pydantic_schema=Sentences, llm=llm)
def get_propositions(text):
    runnable_output = runnable.invoke({"input": text}).content
    propositions = extraction_chain.run(runnable_output)[0].sentences
    return propositions
 
with open(baseDir + "AM5386" + '.txt') as f:
    essay = f.read()
 
paragraphs = essay.split("\n\n")
len(paragraphs)
essay_propositions = []
for i, para in enumerate(paragraphs[:5]):
    propositions = get_propositions(para)
    essay_propositions.extend(propositions)
    print (f"Done with {i}")
 
print (f"You have {len(essay_propositions)} propositions")
essay_propositions[:10]
 
#------------------------------------mathpix----------------------------------------------------
 
from langchain_community.document_loaders import MathpixPDFLoader
loader = MathpixPDFLoader(fPath)
 
#------------------------------------diffbot--------------------------------------------------------
 
from langchain_experimental.graph_transformers.diffbot import DiffbotGraphTransformer
diffbot_nlp = DiffbotGraphTransformer(diffbot_api_key=os.getenv("DIFFBOT_API_KEY", 'YourKey'))
text = """
Greg is friends with Bobby. San Francisco is a great city, but New York is amazing.
Greg lives in New York.
"""
docs = [Document(page_content=text)]
graph_documents = diffbot_nlp.convert_to_graph_documents(docs)
graph_documents
   
#-------------------------------------------------tika-------------------------------------------
 
import tika
tika.initVM()
from tika import parser, detector
parsed = parser.from_file(fPath,xmlContent=True)
print(parsed["content"])
print(detector.from_file(fPath))
 
#---------------------------------------------------pymupdf---------------------------------------
 
import pymupdf
import pymupdf4llm
import markdown
with pymupdf.open(fPath) as doc: 
    text = chr(12).join([page.get_text() for page in doc])
 
pathlib.Path(baseDir + fName + ".txt").write_bytes(text.encode())
md_text = pymupdf4llm.to_markdown(fPath)
pathlib.Path(baseDir + fName + ".md").write_bytes(md_text.encode())
html_text = markdown(md_text,extensions=['markdown.extensions.tables'])
pathlib.Path(baseDir + fName + ".html").write_bytes(html_text.encode())
 
#---------------------------------------beatifulsoup---------------------------------------------
 
from bs4 import BeautifulSoup
with open(baseDir + fName + '.html') as fByte:
    fString = fByte.read()
response = requests.get(fUrl)
 
with open(baseDir + 'iplex.html','w') as fByte:
    fByte.write(response.text)
 
soup = BeautifulSoup(response.text, 'html.parser')
tableL = soup.find_all('table')
tableS = "".join([str(t) for t in tableL])
tabDf = pd.read_html(tableS)
for tab in tableL:
    t = str(tab)
    if re.search("flexibility gradually",t):
        tabD  = pd.read_html(t, header=[0,1])[0]
        break
tabD.to_csv(baseDir + "implex.csv",index=False)
 
#------------------------------------------pdftabextract------------------------------------------
 
from pdftabextract import imgproc
from pdftabextract.common import read_xml, parse_pages
from math import radians, degrees
from pdftabextract.common import ROTATION, SKEW_X, SKEW_Y
from pdftabextract.geom import pt
from pdftabextract.textboxes import rotate_textboxes, deskew_textboxes
from pdftabextract.clustering import find_clusters_1d_break_dist
from pdftabextract.clustering import calc_cluster_centers_1d
from pdftabextract.clustering import zip_clusters_and_values
from pdftabextract.textboxes import border_positions_from_texts, split_texts_by_positions, join_texts
from pdftabextract.common import all_a_in_b, DIRECTION_VERTICAL
from pdftabextract.extract import make_grid_from_positions
from pdftabextract.common import save_page_grids
from pdftabextract.extract import fit_texts_into_grid, datatable_to_dataframe
 
xPath = baseDir + "output.xml"
xmltree, xmlroot = read_xml(xPath)
p_num = 3
p = pages[p_num]
pages = parse_pages(xmlroot)
imgfilebasename = p['image'][:p['image'].rindex('.')]
imgfile = os.path.join(baseDir, p['image'])
print("page %d: detecting lines in image file '%s'..." % (p_num, imgfile))
iproc_obj = imgproc.ImageProc(imgfile)
page_scaling_x = iproc_obj.img_w / p['width']  # scaling in X-direction
page_scaling_y = iproc_obj.img_h / p['height']  # scaling in Y-direction
lines_hough = iproc_obj.detect_lines(canny_kernel_size=3, canny_low_thresh=50, canny_high_thresh=150,
                                    hough_rho_res=1,
                                    hough_theta_res=np.pi/500,
                                    hough_votes_thresh=round(0.2 * iproc_obj.img_w))
print("> found %d lines" % len(lines_hough))
import cv2
def save_image_w_lines(iproc_obj, imgfilebasename):
    img_lines = iproc_obj.draw_lines(orig_img_as_background=True)
    img_lines_file = os.path.join(baseDir, '%s-lines-orig.png' % imgfilebasename)
   
    print("> saving image with detected lines to '%s'" % img_lines_file)
    cv2.imwrite(img_lines_file, img_lines)
 
save_image_w_lines(iproc_obj, imgfilebasename)
rot_or_skew_type, rot_or_skew_radians = iproc_obj.find_rotation_or_skew(radians(0.5),
                                                                        radians(1),
                                                                        omit_on_rot_thresh=radians(0.5))
 
needs_fix = True
if rot_or_skew_type == ROTATION:
    print("> rotating back by %f°" % -degrees(rot_or_skew_radians))
    rotate_textboxes(p, -rot_or_skew_radians, pt(0, 0))
elif rot_or_skew_type in (SKEW_X, SKEW_Y):
    print("> deskewing in direction '%s' by %f°" % (rot_or_skew_type, -degrees(rot_or_skew_radians)))
    deskew_textboxes(p, -rot_or_skew_radians, rot_or_skew_type, pt(0, 0))
else:
    needs_fix = False
    print("> no page rotation / skew found")
if needs_fix:
    lines_hough = iproc_obj.apply_found_rotation_or_skew(rot_or_skew_type, -rot_or_skew_radians)
    save_image_w_lines(iproc_obj, imgfilebasename + '-repaired')
 
output_files_basename = xPath[:xPath.rindex('.')]
repaired_xmlfile = os.path.join(xPath, output_files_basename + '.repaired.xml')
print("saving repaired XML file to '%s'..." % repaired_xmlfile)
xmltree.write(repaired_xmlfile)
 
MIN_COL_WIDTH = 60
vertical_clusters = iproc_obj.find_clusters(imgproc.DIRECTION_VERTICAL, find_clusters_1d_break_dist,
                                            remove_empty_cluster_sections_use_texts=p['texts'],
                                            remove_empty_cluster_sections_n_texts_ratio=0.1,
                                            remove_empty_cluster_sections_scaling=page_scaling_x,
                                            dist_thresh=MIN_COL_WIDTH/2)
print("> found %d clusters" % len(vertical_clusters))
img_w_clusters = iproc_obj.draw_line_clusters(imgproc.DIRECTION_VERTICAL, vertical_clusters)
save_img_file = os.path.join(baseDir, '%s-vertical-clusters.png' % imgfilebasename)
print("> saving image with detected vertical clusters to '%s'" % save_img_file)
cv2.imwrite(save_img_file, img_w_clusters)
page_colpos = np.array(calc_cluster_centers_1d(vertical_clusters)) / page_scaling_x
print('found %d column borders:' % len(page_colpos))
print(page_colpos)
col2_rightborder = page_colpos[2]
median_text_height = np.median([t['height'] for t in p['texts']])
text_height_deviation_thresh = median_text_height / 2
texts_cols_1_2 = [t for t in p['texts']
                  if t['right'] <= col2_rightborder
                    and abs(t['height'] - median_text_height) <= text_height_deviation_thresh]
borders_y = border_positions_from_texts(texts_cols_1_2, DIRECTION_VERTICAL)
clusters_y = find_clusters_1d_break_dist(borders_y, dist_thresh=median_text_height/2)
clusters_w_vals = zip_clusters_and_values(clusters_y, borders_y)
pos_y = calc_cluster_centers_1d(clusters_w_vals)
pos_y.append(p['height'])
print('number of line positions:', len(pos_y))
pttrn_table_row_beginning = re.compile(r'^[\d Oo][\d Oo]{2,} +[A-ZÄÖÜ]')
texts_cols_1_2_per_line = split_texts_by_positions(texts_cols_1_2, pos_y, DIRECTION_VERTICAL,
                                                  alignment='middle',
                                                  enrich_with_positions=True)
for line_texts, (line_top, line_bottom) in texts_cols_1_2_per_line:
    line_str = join_texts(line_texts)
    if pttrn_table_row_beginning.match(line_str): 
        top_y = line_top
        break
else:
    top_y = 0
 
words_in_footer = ('anzeige', 'annahme', 'ala')
min_footer_text_height = median_text_height * 1.5
min_footer_y_pos = p['height'] * 0.7
bottom_texts = [t for t in p['texts']
                if t['top'] >= min_footer_y_pos and t['height'] >= min_footer_text_height]
bottom_texts_per_line = split_texts_by_positions(bottom_texts,
                                                pos_y + [p['height']],
                                                DIRECTION_VERTICAL,
                                                alignment='middle',
                                                enrich_with_positions=True)
page_span = page_colpos[-1] - page_colpos[0]
min_footer_text_width = page_span * 0.8
for line_texts, (line_top, line_bottom) in bottom_texts_per_line:
    line_str = join_texts(line_texts)
    has_wide_footer_text = any(t['width'] >= min_footer_text_width for t in line_texts)
    if has_wide_footer_text or all_a_in_b(words_in_footer, line_str):
        bottom_y = line_top
        break
else:
    bottom_y = p['height']
 
page_rowpos = [y for y in pos_y if top_y <= y <= bottom_y]
print("> page %d: %d lines between [%f, %f]" % (p_num, len(page_rowpos), top_y, bottom_y))
grid = make_grid_from_positions(page_colpos, page_rowpos)
n_rows = len(grid)
n_cols = len(grid[0])
print("> page %d: grid with %d rows, %d columns" % (p_num, n_rows, n_cols))
page_grids_file = os.path.join(baseDir, output_files_basename + '.pagegrids_p3_only.json')
print("saving page grids JSON file to '%s'" % page_grids_file)
save_page_grids({p_num: grid}, page_grids_file)
datatable = fit_texts_into_grid(p['texts'], grid)
df = datatable_to_dataframe(datatable)
df.head(n=10)
csv_output_file = os.path.join(baseDir, output_files_basename + '-p3_only.csv')
print("saving extracted data to '%s'" % csv_output_file)
df.to_csv(csv_output_file, index=False)
excel_output_file = os.path.join(baseDir, output_files_basename + '-p3_only.xlsx')
print("saving extracted data to '%s'" % excel_output_file)
df.to_excel(excel_output_file, index=False)
 
 
#------------------------------------------table-extract-------------------------------------------
import pdftableextract as pdf
root, ext = os.path.splitext(os.path.basename(fPath))
pages = ['1']
cells = [pdf.process_page(sys.argv[1], p) for p in pages]
cells = [cell for row in cells for cell in row]
 
tables = pdf.table_to_list(cells, pages)
for i, table in enumerate(tables[1:]):
    df = pd.DataFrame(table)
    out = '{}-page-1-table-{}.csv'.format(root, i + 1)
    df.to_csv(out, index=False, quoting=1, encoding='utf-8')
 
#-------------------------------pdftables------------------------------------------------
resq = requests.post("https://pdftables.com/api?key="+os.environ['PDFTABLES_KEY']+"&format=xlsx-single")
 
 
#-------------------------------tika--------------------------------------------
 
import tika
tika.initVM()
from tika import parser
parsed = parser.from_file(fPath)
print(parsed["metadata"])
print(parsed["content"])
                   
#----------------------------pypdf------------------------------------------------
from pypdf import PdfReader
reader = PdfReader(fPath)
number_of_pages = len(reader.pages)
page = reader.pages[0]
text = page.extract_text()
 
#----------------------------llmsherpa-------------------------------------------
 
from llmsherpa.readers import LayoutPDFReader
pdf_reader = LayoutPDFReader("https://readers.llmsherpa.com/api/document/developer/parseDocument?renderFormat=all")
doc = pdf_reader.read_pdf(fPath)
docL = []
for s in doc.sections():
    sectS = ''
    for p in s.children:
        sectS += p.to_text()
        if sectS == '':
            sectS = '-'
        docL.append(Document(text=sectS,metadata={"sect":s.to_context_text(),"lev":s.level}))
for t in doc.tables():
    docL.append(Document(text=t.to_text(),metadata={"table":s.block_idx,"lev":t.level}))
 
#---------------------------------------------pymupdf---------------------------
 
import pymupdf4llm
import pymupdf
md_text = pymupdf4llm.to_markdown(pdf_doc,pages=[0,1])
md_text = pymupdf4llm.to_markdown(pdf_doc)
# parser = LlamaParse(api_key="...",result_type="markdown")
# documents = parser.load_data("./my_file.pdf")
#single_sentences_list = re.split(r'(?<=[.?!])\s+', essay)
headers_split = [("#", "Chapter"),("##", "Section"),('###','Subsection')]
splitter = MarkdownHeaderTextSplitter(headers_split)#,strip_headers=True,return_each_line=False,)
docL = splitter.split_text(md_text)
#splitter = RecursiveCharacterTextSplitter(chunk_size = 1000, chunk_overlap=200)
#splitter = SentenceSplitter(chunk_size=200,chunk_overlap=15)
#elements = partition_pdf(filename=pdf_doc,strategy="hi_res",infer_table_structure=True,model_name="yolox")
 
os.environ["LLAMA_CLOUD_API_KEY"] = "llx-"
llm = get_llm()
parsing_instructions = '''The document describes IT security policies for audit. It contains many tables. Answer questions using the information in this article and be precise.'''
documents = LlamaParse(result_type="markdown", parsing_instructions=parsing_instructions).load_data(pdf_doc)
print(documents[0].text[:1000])
node_parser = MarkdownElementNodeParser(llm=llm, num_workers=8).from_defaults()
nodes = node_parser.get_nodes_from_documents(documents)
base_nodes, objects = node_parser.get_nodes_and_objects(nodes)
 
#-------------------------------------------pypdf2------------------------------
 
from PyPDF2 import PdfReader
text = ""
docL = []
for pdf in pdf_docs:
    pdf_reader = PdfReader(pdf)
    for i, page in enumerate(pdf_reader.pages):
        text = page.extract_text()
        docL.append(Document(text=text,metadata={"page":i}))
       
 
 
#-----------------------------------camelot-----------------------------
 
import camelot
tables = camelot.read_pdf(fPath)
tDf = tables[0].df
tDf.to_csv(baseDir + fName + ".csv")
 
#----------------------------------pdf-plumber-------------------------------
 
import fitz
import pdfplumber
from collections import Counter
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import SimpleDocTemplate, Preformatted
 
font_size_counter = Counter()
with pdfplumber.open(fPath) as pdf:
    for i in range(len(pdf.pages)):
        words = pdf.pages[i].extract_words(extra_attrs=['fontname', 'size'])
        lines = {}
        for word in words:
            line_num = word['top']
            if line_num not in lines:
                lines[line_num] = []
            lines[line_num].append(word)
        for line_words in lines.values():
            font_size_counter[line_words[0]['size']] += 1
 
repeated_sizes = [size for size, count in font_size_counter.items() if count > 1]
extracted_font_size = max(repeated_sizes)
 
chunks = extract_chunks_from_pdf(fPath, markers)
 
 
lines_with_target_font_size = []
with pdfplumber.open(fPath) as pdf:
    for i in range(len(pdf.pages)):
        words = pdf.pages[i].extract_words(extra_attrs=['fontname', 'size'])
        lines = {}
        for word in words:
            line_num = word['top']
            if line_num not in lines:
                lines[line_num] = []
            lines[line_num].append(word)
        for line_num, line_words in lines.items():
            line_font_sizes = [word['size'] for word in line_words]
            if target_font_size in line_font_sizes:
                line_text = ' '.join([word['text'] for word in line_words])
                lines_with_target_font_size.append(line_text)
 
extracted_font_size = lines_with_target_font_size
               
doc = SimpleDocTemplate(output_fPath, pagesize=letter)
styles = getSampleStyleSheet()
story = []
for chunk in chunks:
    preformatted = Preformatted(chunk, styles["Normal"])
    story.append(preformatted)
doc.build(story)
 
if not os.path.exists(output_folder):
    os.makedirs(output_folder)
for i, chunk in enumerate(chunks, start=1):
    output_fPath = os.path.join(output_folder, f"output_pdf_part{i}.pdf")
    write_chunks_to_pdf([chunk], output_fPath)
 
chunks = []
current_chunk = []
current_marker_index = 0
pdf_document = fitz.open(fPath)
for page_num in range(pdf_document.page_count):
    page = pdf_document[page_num]
    text = page.get_text("text")
    lines = text.split('\n')
    for line in lines:
        if current_marker_index < len(markers) and markers[current_marker_index] in line:
            if current_chunk:
                chunks.append('\n'.join(current_chunk))
            current_chunk = []
            current_marker_index += 1
        current_chunk.append(line)
if current_chunk:
    chunks.append('\n'.join(current_chunk))
pdf_document.close()
output_folder = "output"
 
#--------------------------------------------------------adobe---------------------------------------------
 
from adobe.pdfservices.operation.auth.service_principal_credentials import ServicePrincipalCredentials
from adobe.pdfservices.operation.exception.exceptions import ServiceApiException, ServiceUsageException, SdkException
from adobe.pdfservices.operation.io.cloud_asset import CloudAsset
from adobe.pdfservices.operation.io.stream_asset import StreamAsset
from adobe.pdfservices.operation.pdf_services import PDFServices
from adobe.pdfservices.operation.pdf_services_media_type import PDFServicesMediaType
from adobe.pdfservices.operation.pdfjobs.jobs.export_pdf_job import ExportPDFJob
from adobe.pdfservices.operation.pdfjobs.params.export_pdf.export_pdf_params import ExportPDFParams
from adobe.pdfservices.operation.pdfjobs.params.export_pdf.export_pdf_target_format import ExportPDFTargetFormat
from adobe.pdfservices.operation.pdfjobs.result.export_pdf_result import ExportPDFResult
 
credentials = ServicePrincipalCredentials(
    client_id=os.getenv('PDF_SERVICES_CLIENT_ID'),
    client_secret=os.getenv('PDF_SERVICES_CLIENT_SECRET'))
pdf_services = PDFServices(credentials=credentials)
file = open('src/resources/Bodea Brochure.pdf', 'rb')
input_stream = file.read()
file.close()
input_asset = pdf_services.upload(input_stream=input_stream, mime_type=PDFServicesMediaType.PDF)
export_pdf_params = ExportPDFParams(target_format=ExportPDFTargetFormat.DOCX)
export_pdf_job = ExportPDFJob(input_asset=input_asset, export_pdf_params=export_pdf_params)
location = pdf_services.submit(export_pdf_job)
pdf_services_response = pdf_services.get_job_result(location, ExportPDFResult)
result_asset: CloudAsset = pdf_services_response.get_result().get_asset()
stream_asset: StreamAsset = pdf_services.get_content(result_asset)
output_file_path = "./Bodea Brochure.docx"
with open(output_file_path, "wb") as file:
    file.write(stream_asset.get_input_stream())
 
#-----------------------------------nougat-ocr----------------------------------
#-----------------------------------marker-pdf----------------------------------
 
   
print("te se qe te ve be te ne?")
 
#https://www.jnjmedtech.com/system/files/pdf/090912-220322%20DSUS_EMEA%20Large%20Bone%20Saw%20Blades%20Product%20Brochure.pdf

Latest revision as of 12:11, 6 November 2024

Kotoba