msds-tds/scripts/generate-sds-en.py
2026-07-08 13:12:20 +00:00

555 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Generate English SDS — proper @page margin-box header/footer, with working page numbers.
WHAT CHANGED vs the original script, and why:
1. Header/footer are no longer `position: fixed` <div>s living in normal document flow.
They are now `position: running(...)` elements, pulled into real CSS Paged Media
margin boxes (`@top-center`, `@bottom-center`) via `@page` rules. This is the only
mechanism WeasyPrint reliably supports for content that must repeat identically on
every page and stay anchored to the page box rather than the flow.
2. `html, body { width: 100%; height: 100%; }` is set explicitly. Without an explicit
containing-block size, a `position: fixed` box has nothing reliable to size itself
against, which is one reason the old header/footer collapsed to the top of page 1
at a shrunk scale instead of spanning full page width.
3. Page numbers now live in the SAME @bottom-center margin box as the footer contact
line, via one `content:` value list, e.g.:
content: element(footer) "Page " counter(page) " of " counter(pages);
Previously the footer contact info and the "Page X of Y" counter were assigned to
two different margin boxes (@bottom-left vs @bottom-center) which don't stack or
share a baseline — that's why the counter looked "missing" or floated separately.
counter(page)/counter(pages) ONLY resolve inside an @page margin box's `content`,
never inside ordinary flowed or fixed HTML — so this placement is mandatory, not
a style choice.
4. Page geometry (@page margin values) is defined ONCE and reused consistently, sized
to actually fit the running header/footer content (rule + 2 lines), instead of two
different margin numbers copied between two files with different assumptions.
All Section 1-16 content, tables, and copy are unchanged from the original script —
only the header/footer/page-number plumbing was touched.
"""
import os, base64, re
from weasyprint import HTML
PRIMARY = "#004C98"
SECONDARY = "#2B2E33"
MUTED = "#5B6670"
ACCENT = "#0071B8"
HIGHLIGHT = "#F2F6FA"
BORDER = "#D9E1E8"
WHITE = "#FFFFFF"
WARNING_AMBER = "#C77700"
def img_b64(path):
with open(path, 'rb') as f:
return f"data:image/png;base64,{base64.b64encode(f.read()).decode()}"
def svg_b64(path):
with open(path, 'rb') as f:
return f"data:image/svg+xml;base64,{base64.b64encode(f.read()).decode()}"
logo_b64 = img_b64('/home/bachko/msds-tds/assets/logo-synthesia.png')
iso_b64 = svg_b64('/home/bachko/msds-tds/assets/iso-tuv-rheinland.svg')
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@page {{
size: A4;
/* Margins sized to fit the running header (logo+title+rule) and
running footer (rule + contact line + cert line + page counter). */
margin: 2.6cm 1.9cm 2.1cm 1.9cm;
@top-center {{
content: element(header);
}}
@bottom-center {{
/* Footer content and the page counter live in ONE box, so they
stack as a single visual unit instead of two disconnected boxes. */
content: element(footer);
}}
}}
/* A4 (21cm) minus 1.8cm left + 1.8cm right @page margins = 17.4cm.
@top-center / @bottom-center are otherwise sized shrink-to-fit and then
centered, so a `width: 100%` on the element inside them resolves against
that shrunk box, not the page's content width -- which is why header and
footer were rendering ~4cm narrower on each side than the body text.
Setting an explicit absolute width (below, on .running-header /
.running-footer) removes that ambiguity instead of relying on 100%. */
html, body {{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}}
body {{
font-family: Calibri, Carlito, Arial, sans-serif;
font-size: 10pt;
color: {SECONDARY};
line-height: 1.2;
}}
/* ═══ RUNNING HEADER (rendered into @top-center on every page) ═══ */
.running-header {{
position: running(header);
/* Explicit absolute width = page content width (A4 21cm - 1.8cm - 1.8cm
@page margins), not 100%, so it doesn't resolve against a shrink-to-fit
@top-center box and end up centered with gaps on both sides. */
width: 17.2cm;
}}
.hdr-table {{ width: 100%; border-collapse: collapse; }}
.hdr-logo {{ width: 130px; text-align: left; vertical-align: middle; }}
.hdr-logo img {{ height: 1.0cm; }}
.hdr-iso {{ width: 55px; text-align: right; vertical-align: middle; }}
.hdr-iso img {{ height: 1.22cm; }}
.hdr-title {{ vertical-align: middle; text-align: right; }}
.hdr-title .t1 {{ font-size: 9pt; font-weight: bold; color: {PRIMARY}; }}
.hdr-title .t2 {{ font-size: 7.5pt; color: {SECONDARY}; }}
.hdr-title .t3 {{ font-size: 6.5pt; color: {MUTED}; }}
.hdr-rule {{ border: none; border-top: 1pt solid {PRIMARY}; margin: 2px 0 0 0; }}
/* ═══ RUNNING FOOTER (rendered into @bottom-center on every page) ═══ */
.running-footer {{
position: running(footer);
/* Same fix as .running-header: explicit absolute width instead of 100%,
so it isn't sized shrink-to-fit-then-centered inside @bottom-center. */
width: 17.2cm;
}}
.ftr-rule {{ border: none; border-top: 0.5pt solid {BORDER}; margin: 0 0 2px 0; }}
.ftr-line {{ font-size: 6.5pt; color: {MUTED}; text-align: center; margin: 1px 0; }}
.ftr-line-sm {{ font-size: 6pt; color: {MUTED}; text-align: center; font-style: italic; margin: 1px 0; }}
.ftr-pagenum {{
font-size: 6.5pt; color: {MUTED}; text-align: center; margin: 1px 0 0 0;
}}
/* The counter itself is generated CSS content, attached to this element
via ::after — this is still inside the SAME running(footer) box, so it
stacks directly under the contact/cert lines rather than living in a
separate, disconnected margin box. */
.ftr-pagenum::after {{
content: "Page " counter(page) " of " counter(pages);
}}
/* ═══ CONTENT ═══ */
h1.doc-title {{
font-size: 16pt; font-weight: bold; color: {PRIMARY};
border-bottom: 2pt solid {PRIMARY}; padding-bottom: 3px; margin: 0 0 8pt 0;
}}
h2.section {{
font-size: 12pt; font-weight: bold; color: {SECONDARY};
border-left: 4pt solid {ACCENT}; padding-left: 6px; margin: 8pt 0 4pt 0;
}}
h3.subsection {{ font-size: 10.5pt; font-weight: bold; color: {SECONDARY}; margin: 6pt 0 3pt 0; }}
p {{ margin: 3px 0; }}
table.data {{ width: 100%; border-collapse: collapse; margin: 4px 0; font-size: 9pt; }}
table.data th {{ background-color: {PRIMARY}; color: {WHITE}; font-weight: bold; padding: 3pt 5pt; text-align: left; border: 0.5pt solid {BORDER}; font-size: 9pt; }}
table.data td {{ padding: 3pt 5pt; border: 0.5pt solid {BORDER}; }}
table.data tr:nth-child(even) td {{ background-color: {HIGHLIGHT}; }}
table.data tr:nth-child(odd) td {{ background-color: {WHITE}; }}
table.data td:first-child {{ font-weight: bold; color: {SECONDARY}; }}
table.layout {{ width: 100%; border-collapse: collapse; margin: 2px 0; }}
table.layout td {{ border: none; padding: 1px 5px 1px 0; vertical-align: top; }}
table.layout td:first-child {{ width: 30%; font-weight: bold; }}
.alert-warning {{ color: {WARNING_AMBER}; font-weight: bold; }}
.meta-line {{ font-size: 7.5pt; color: {MUTED}; text-align: right; border-bottom: 1px solid {BORDER}; padding-bottom: 3px; margin-bottom: 6px; }}
/* Pagination now follows content flow instead of fixed manual breaks
points regardless of how much room was left on the page, which is
what caused half-empty pages). These rules only prevent a heading
from being stranded alone at the bottom of a page, and prevent a
single table row from being split across two pages. */
table.data thead {{ display: table-header-group; }}
</style>
</head>
<body>
<!-- ═══ RUNNING HEADER definition (not visible in flow — pulled into @top-center) ═══ -->
<div class="running-header">
<table class="hdr-table">
<tr>
<td class="hdr-logo"><img src="{logo_b64}" alt="Synthesia"></td>
<td class="hdr-title">
<div class="t1">SAFETY DATA SHEET</div>
<div class="t2">Ethyl acetate</div>
<div class="t3">SDS-ETHYL-ACETATE-REV1 · 06/07/2026</div>
</td>
<td class="hdr-iso"><img src="{iso_b64}" alt="TÜV Rheinland"></td>
</tr>
</table>
<hr class="hdr-rule">
</div>
<!-- ═══ RUNNING FOOTER definition (not visible in flow — pulled into @bottom-center) ═══ -->
<div class="running-footer">
<hr class="ftr-rule">
<div class="ftr-line">Synthesia Ltd. · VAT BG121306542 · Todor Radunchev 9, Krivina 1588, Bulgaria · +359 2 974 4968 · office@synthesia.bg</div>
<div class="ftr-line-sm">Certified ISO 9001:2015 · ISO 14001:2015 · ISO 45001:2018 — TÜV Rheinland, ID 9105074744</div>
<div class="ftr-pagenum"></div>
</div>
<!-- ═══ CONTENT (unchanged from original) ═══ -->
<h1 class="doc-title">SAFETY DATA SHEET<br><span style="font-size:11pt;color:{SECONDARY};">Ethyl acetate</span></h1>
<p class="meta-line">According to Regulation (EU) 2020/878 (REACH Annex II) · CLP Regulation (EC) 1272/2008 as amended by EU 2024/2865</p>
<h2 class="section">SECTION 1: Identification of the substance/mixture and of the company/undertaking</h2>
<h3 class="subsection">1.1 Product identifier</h3>
<table class="data">
<tr><th>Property</th><th>Value</th></tr>
<tr><td>Product form</td><td>Substance</td></tr>
<tr><td>Chemical name</td><td>Ethyl acetate</td></tr>
<tr><td>EC Index-No.</td><td>607-022-00-5</td></tr>
<tr><td>EC-No.</td><td>205-500-4</td></tr>
<tr><td>CAS-No.</td><td>141-78-6</td></tr>
<tr><td>REACH registration No.</td><td>01-2119475103-46-XXXX</td></tr>
<tr><td>Molecular formula</td><td>C₄H₈O₂</td></tr>
</table>
<h3 class="subsection">1.2 Relevant identified uses</h3>
<table class="layout"><tr><td>Use of the substance/mixture</td><td>Industrial use</td></tr>
<tr><td>Restrictions on use</td><td>No data available</td></tr></table>
<h3 class="subsection">1.3 Details of the supplier</h3>
<table class="data">
<tr><th>Field</th><th>Details</th></tr>
<tr><td>Company</td><td>Synthesia Ltd.</td></tr>
<tr><td>Address</td><td>Todor Radunchev 9, Krivina 1588, Bulgaria</td></tr>
<tr><td>VAT</td><td>BG121306542</td></tr>
<tr><td>Phone</td><td>+359 2 974 4968</td></tr>
<tr><td>Email</td><td>office@synthesia.bg</td></tr>
</table>
<h3 class="subsection">1.4 Emergency telephone number</h3>
<table class="layout"><tr><td>Emergency number</td><td>112</td></tr></table>
<h2 class="section">SECTION 2: Hazards identification</h2>
<h3 class="subsection">2.1 Classification</h3>
<p><b>Classification according to Regulation (EC) No. 1272/2008 [CLP] as amended by EU 2024/2865:</b></p>
<table class="data">
<tr><th>Hazard class</th><th>Category</th><th>Hazard statement</th></tr>
<tr><td>Flammable liquids</td><td>Category 2</td><td>H225 — Highly flammable liquid and vapour</td></tr>
<tr><td>Serious eye damage/eye irritation</td><td>Category 2</td><td>H319 — Causes serious eye irritation</td></tr>
<tr><td>Specific target organ toxicity — Single exposure</td><td>Category 3 (Narcosis)</td><td>H336 — May cause drowsiness or dizziness</td></tr>
</table>
<p>Full text of H-statements: see Section 16.</p>
<h3 class="subsection">2.2 Label elements</h3>
<table class="data">
<tr><th>Element</th><th>Details</th></tr>
<tr><td>Hazard pictograms</td><td>GHS02 (Flame) · GHS07 (Exclamation mark)</td></tr>
<tr><td>Signal word</td><td><span class="alert-warning">Danger</span></td></tr>
<tr><td>Hazard statements</td><td>H225 — Highly flammable liquid and vapour.<br>H319 — Causes serious eye irritation.<br>H336 — May cause drowsiness or dizziness.</td></tr>
<tr><td>Precautionary statements</td><td>P210 — Keep away from heat, hot surfaces, sparks, open flames and other ignition sources. No smoking.<br>P280 — Wear protective gloves/protective clothing/eye protection/face protection.<br>P304+P340 — IF INHALED: Remove person to fresh air and keep comfortable for breathing.<br>P305+P351+P338 — IF IN EYES: Rinse cautiously with water for several minutes. Remove contact lenses, if present and easy to do. Continue rinsing.<br>P337+P313 — If eye irritation persists: Get medical advice/attention.<br>P403+P233 — Store in a well-ventilated place. Keep container tightly closed.</td></tr>
<tr><td>EUH-statements</td><td>EUH066 — Repeated exposure may cause skin dryness or cracking.</td></tr>
</table>
<h3 class="subsection">2.3 Other hazards</h3>
<p>This substance does not contain components considered to be persistent, bioaccumulative and toxic (PBT), very persistent and very bioaccumulative (vPvB), persistent, mobile and toxic (PMT), or very persistent and very mobile (vPvM) at levels of 0.1% or higher, in accordance with the criteria of CLP Regulation as amended by EU 2024/2865 and REACH Annex XIII.</p>
<p>This substance is not classified as an endocrine disruptor (ED) according to the criteria of CLP Regulation as amended by EU 2024/2865, Commission Delegated Regulation (EU) 2017/2100, or Commission Regulation (EU) 2018/605.</p>
<p>Vapours may form explosive mixture with air.</p>
<h2 class="section">SECTION 3: Composition/information on ingredients</h2>
<h3 class="subsection">3.1 Substances</h3>
<table class="data">
<tr><th>Component</th><th>CAS-No.</th><th>EC-No.</th><th>Index-No.</th><th>Classification</th><th>Concentration</th></tr>
<tr><td>Ethyl acetate</td><td>141-78-6</td><td>205-500-4</td><td>607-022-00-5</td><td>Flam. Liq. 2; Eye Irrit. 2; STOT SE 3; H225, H319, H336</td><td>&gt; 99.8%</td></tr>
</table>
<p>Molecular weight: 88.11 g/mol &nbsp;|&nbsp; Formula: C₄H₈O₂</p>
<h3 class="subsection">3.2 Mixtures</h3>
<p>Not applicable.</p>
<h2 class="section">SECTION 4: First aid measures</h2>
<h3 class="subsection">4.1 Description of first aid measures</h3>
<table class="data">
<tr><th>Route</th><th>Measures</th></tr>
<tr><td>General</td><td>Call a poison centre or a doctor if you feel unwell.</td></tr>
<tr><td>After inhalation</td><td>Remove person to fresh air and keep comfortable for breathing.</td></tr>
<tr><td>After skin contact</td><td>Rinse skin with water/shower. Take off immediately all contaminated clothing.</td></tr>
<tr><td>After eye contact</td><td>Rinse cautiously with water for several minutes. Remove contact lenses, if present and easy to do. Continue rinsing.</td></tr>
<tr><td>After ingestion</td><td>Rinse mouth. Do not induce vomiting. Call a poison centre or a doctor if you feel unwell.</td></tr>
</table>
<h3 class="subsection">4.2 Most important symptoms and effects</h3>
<table class="data">
<tr><th>Symptoms/effects</th><th>Description</th></tr>
<tr><td>General</td><td>May cause drowsiness or dizziness.</td></tr>
<tr><td>Skin contact</td><td>Repeated exposure may cause skin dryness or cracking.</td></tr>
<tr><td>Eye contact</td><td>Eye irritation.</td></tr>
</table>
<h3 class="subsection">4.3 Indication of immediate medical attention</h3>
<p>Treat symptomatically.</p>
<h2 class="section">SECTION 5: Firefighting measures</h2>
<h3 class="subsection">5.1 Extinguishing media</h3>
<table class="layout"><tr><td>Suitable extinguishing media</td><td>Water spray. Dry powder. Foam. Carbon dioxide.</td></tr>
<tr><td>Unsuitable extinguishing media</td><td>High volume water jet.</td></tr></table>
<h3 class="subsection">5.2 Special hazards</h3>
<table class="layout"><tr><td>Fire hazard</td><td>Highly flammable liquid and vapour.</td></tr>
<tr><td>Hazardous decomposition products</td><td>Toxic fumes may be released. Carbon oxides.</td></tr></table>
<h3 class="subsection">5.3 Advice for firefighters</h3>
<p>Do not attempt to take action without suitable protective equipment.</p>
<h2 class="section">SECTION 6: Accidental release measures</h2>
<h3 class="subsection">6.1 Personal precautions</h3>
<table class="data">
<tr><th>Personnel</th><th>Measures</th></tr>
<tr><td>For non-emergency personnel</td><td>Wear recommended PPE. Ventilate area. No open flames, no sparks, no smoking.</td></tr>
<tr><td>For emergency responders</td><td>Eliminate all ignition sources. Evacuate unnecessary personnel.</td></tr>
</table>
<h3 class="subsection">6.2 Environmental precautions</h3>
<p>Avoid release to the environment.</p>
<h3 class="subsection">6.3 Methods for cleaning up</h3>
<table class="layout"><tr><td>Methods for cleaning up</td><td>Take up liquid spill into absorbent material.</td></tr>
<tr><td>Other information</td><td>Dispose of residues at an authorised site.</td></tr></table>
<h3 class="subsection">6.4 Reference to other sections</h3>
<p>Refer to protective measures listed in Sections 7 and 8.</p>
<h2 class="section">SECTION 7: Handling and storage</h2>
<h3 class="subsection">7.1 Precautions for safe handling</h3>
<p>Keep away from heat, hot surfaces, sparks, open flames and other ignition sources. No smoking. Ground/bond container and receiving equipment. Use only non-sparking tools. Take precautionary measures against static discharge. Use only outdoors or in a well-ventilated area.</p>
<p><b>Hygiene measures:</b> Do not eat, drink or smoke when using this product.</p>
<h3 class="subsection">7.2 Conditions for safe storage</h3>
<table class="layout">
<tr><td>Storage conditions</td><td>Store in a well-ventilated place. Keep cool. Keep container tightly closed.</td></tr>
<tr><td>Incompatibilities</td><td>Heat and ignition sources.</td></tr>
</table>
<h3 class="subsection">7.3 Specific end use(s)</h3>
<p>No additional information available.</p>
<h2 class="section">SECTION 8: Exposure controls/personal protection</h2>
<h3 class="subsection">8.1 Control parameters</h3>
<table class="data"><tr><th colspan="2">EU Occupational Exposure Limits (IOELV) — Ethyl acetate</th></tr>
<tr><td>IOELV TWA</td><td>734 mg/m³ (200 ppm)</td></tr>
<tr><td>IOELV STEL</td><td>1,468 mg/m³ (400 ppm)</td></tr>
<tr><td>Reference</td><td>Directive (EU) 2017/164</td></tr></table>
<table class="data"><tr><th colspan="2">DNEL/DMEL — Workers</th></tr>
<tr><td>Acute - inhalation</td><td>1,468 mg/m³</td></tr>
<tr><td>Long-term - dermal</td><td>63 mg/kg bw/day</td></tr>
<tr><td>Long-term - inhalation</td><td>734 mg/m³</td></tr></table>
<table class="data"><tr><th colspan="2">DNEL/DMEL — General population</th></tr>
<tr><td>Acute - inhalation</td><td>734 mg/m³</td></tr>
<tr><td>Long-term - oral</td><td>4.5 mg/kg bw/day</td></tr>
<tr><td>Long-term - inhalation</td><td>367 mg/m³</td></tr>
<tr><td>Long-term - dermal</td><td>37 mg/kg bw/day</td></tr></table>
<table class="data"><tr><th colspan="2">PNEC</th></tr>
<tr><td>Freshwater</td><td>0.24 mg/l</td></tr>
<tr><td>Marine water</td><td>0.024 mg/l</td></tr>
<tr><td>Soil</td><td>0.148 mg/kg dwt</td></tr>
<tr><td>STP</td><td>650 mg/l</td></tr></table>
<h3 class="subsection">8.2 Exposure controls</h3>
<table class="data">
<tr><th>Control type</th><th>Measures</th></tr>
<tr><td>Engineering controls</td><td>Ensure good ventilation.</td></tr>
<tr><td>Hand protection</td><td>Protective gloves.</td></tr>
<tr><td>Eye protection</td><td>Safety glasses.</td></tr>
<tr><td>Respiratory protection</td><td>If ventilation insufficient, wear suitable respiratory equipment.</td></tr>
<tr><td>Environmental exposure</td><td>Avoid release to the environment.</td></tr>
</table>
<h2 class="section">SECTION 9: Physical and chemical properties</h2>
<h3 class="subsection">9.1 Information on basic physical and chemical properties</h3>
<table class="data">
<tr><th>Property</th><th>Value</th></tr>
<tr><td>Physical state</td><td>Clear liquid</td></tr>
<tr><td>Colour / Odour</td><td>Colourless / Fruity</td></tr>
<tr><td>Melting point</td><td>-84 °C</td></tr>
<tr><td>Boiling point</td><td>76.5 – 77.5 °C</td></tr>
<tr><td>Flash point</td><td>-4 °C</td></tr>
<tr><td>Auto-ignition</td><td>426 °C</td></tr>
<tr><td>Flammability limits</td><td>2 – 11.5 vol %</td></tr>
<tr><td>Vapour pressure</td><td>73 mm Hg (20 °C)</td></tr>
<tr><td>Vapour density</td><td>3.04 (Air = 1)</td></tr>
<tr><td>Density</td><td>0.902 g/cm³ (20 °C)</td></tr>
<tr><td>Solubility (water)</td><td>8 g/100 ml (25 °C)</td></tr>
<tr><td>Log Pow</td><td>0.73</td></tr>
<tr><td>Viscosity</td><td>0.45 mPa·s (20 °C)</td></tr>
</table>
<h2 class="section">SECTION 10: Stability and reactivity</h2>
<h3 class="subsection">10.1 Reactivity</h3><p>Highly flammable liquid and vapour.</p>
<h3 class="subsection">10.2 Chemical stability</h3><p>Stable under normal conditions.</p>
<h3 class="subsection">10.3 Hazardous reactions</h3><p>No dangerous reactions known under normal conditions of use.</p>
<h3 class="subsection">10.4 Conditions to avoid</h3><p>Avoid contact with hot surfaces. No flames, no sparks. Eliminate all sources of ignition.</p>
<h3 class="subsection">10.5 Incompatible materials</h3><p>No additional information available.</p>
<h3 class="subsection">10.6 Hazardous decomposition products</h3><p>Under normal conditions of storage and use, hazardous decomposition products should not be produced.</p>
<h2 class="section">SECTION 11: Toxicological information</h2>
<h3 class="subsection">11.1 Information on hazard classes</h3>
<table class="data">
<tr><th>Endpoint</th><th>Classification</th><th>Data</th></tr>
<tr><td>Acute toxicity</td><td>Not classified</td><td>LD50 oral: 5,620 mg/kg bw (rabbit)</td></tr>
<tr><td>Skin corrosion/irritation</td><td>Not classified</td><td></td></tr>
<tr><td>Serious eye damage/irritation</td><td><b>Causes serious eye irritation</b></td><td></td></tr>
<tr><td>Respiratory/skin sensitisation</td><td>Not classified</td><td></td></tr>
<tr><td>Germ cell mutagenicity</td><td>Not classified</td><td></td></tr>
<tr><td>Carcinogenicity</td><td>Not classified</td><td></td></tr>
<tr><td>Reproductive toxicity</td><td>Not classified</td><td></td></tr>
<tr><td>STOT — single exposure</td><td>Category 3 (Narcosis)</td><td>May cause drowsiness/dizziness</td></tr>
<tr><td>STOT — repeated exposure</td><td>Not classified</td><td>NOAEL 900 mg/kg bw (90d rat)</td></tr>
<tr><td>Aspiration hazard</td><td>Not classified</td><td></td></tr>
</table>
<h3 class="subsection">11.2 Other hazards</h3>
<p>Not classified as having endocrine disrupting properties according to REACH Article 57(f), CLP as amended by EU 2024/2865.</p>
<h2 class="section">SECTION 12: Ecological information</h2>
<h3 class="subsection">12.1 Toxicity</h3>
<p>The product is not considered harmful to aquatic organisms.</p>
<table class="data">
<tr><th>Endpoint</th><th>Value</th><th>Species</th></tr>
<tr><td>LC50 (fish, 96h)</td><td>230 mg/l</td><td>Pimephales promelas</td></tr>
<tr><td>EC50 (daphnia, 48h)</td><td>720 mg/l</td><td>Daphnia magna</td></tr>
<tr><td>EC50 (algae, 72h)</td><td>3,300 mg/l</td><td></td></tr>
</table>
<h3 class="subsection">12.2 Persistence and degradability</h3><p>Readily biodegradable — 100% after 28 days.</p>
<h3 class="subsection">12.3 Bioaccumulative potential</h3><table class="layout"><tr><td>Log Pow</td><td>0.73 (low)</td></tr></table>
<h3 class="subsection">12.4 Mobility in soil</h3><p>Low mobility.</p>
<h3 class="subsection">12.5 PBT and vPvB assessment</h3><p>Not classified as PBT or vPvB.</p>
<h3 class="subsection">12.6 Endocrine disrupting properties</h3><p>Not classified as having ED properties.</p>
<h3 class="subsection">12.7 Other adverse effects</h3><p>No additional information available.</p>
<h2 class="section">SECTION 13: Disposal considerations</h2>
<h3 class="subsection">13.1 Waste treatment methods</h3>
<table class="layout">
<tr><td>Waste treatment methods</td><td>Dispose of contents/container in accordance with licensed collector's sorting instructions.</td></tr>
<tr><td>Packaging disposal</td><td>Dispose of contents/container to hazardous or special waste collection point.</td></tr>
<tr><td>Additional information</td><td>Do not re-use empty containers. Flammable vapours may accumulate.</td></tr>
</table>
<h2 class="section">SECTION 14: Transport information</h2>
<p>In accordance with ADR / RID / IMDG / IATA / ADN</p>
<table class="data">
<tr><th>Transport mode</th><th>UN No.</th><th>Proper shipping name</th><th>Class</th><th>PG</th><th>Env.</th></tr>
<tr><td>ADR (road)</td><td>1173</td><td>ETHYL ACETATE, 3, II, (D/E)</td><td>3</td><td>II</td><td>No</td></tr>
<tr><td>IMDG (sea)</td><td>1173</td><td>ETHYL ACETATE, 3, II</td><td>3</td><td>II</td><td>No</td></tr>
<tr><td>IATA (air)</td><td>1173</td><td>Ethyl acetate, 3, II</td><td>3</td><td>II</td><td>No</td></tr>
</table>
<table class="data">
<tr><th colspan="2">ADR Details</th></tr>
<tr><td>Classification code</td><td>F1</td></tr>
<tr><td>Limited quantities</td><td>1 L</td></tr>
<tr><td>Excepted quantities</td><td>E2</td></tr>
<tr><td>Tunnel restriction code</td><td>D/E</td></tr>
<tr><td>Kemler No.</td><td>33</td></tr>
<tr><td>Transport category</td><td>2</td></tr>
</table>
<h3 class="subsection">14.7 Transport in bulk</h3>
<p>Not applicable.</p>
<h2 class="section">SECTION 15: Regulatory information</h2>
<h3 class="subsection">15.1 Safety, health and environmental regulations</h3>
<h3 class="subsection">15.1.1 EU Regulations</h3>
<ul>
<li>No REACH Annex XVII restrictions.</li>
<li>Ethyl acetate is not on the REACH Candidate List (SVHC).</li>
<li>Ethyl acetate is not on the REACH Annex XIV (Authorisation List).</li>
<li>Not subject to Regulation (EU) No 649/2012 (PIC).</li>
<li>Not subject to Regulation (EU) 2019/1021 (POP).</li>
</ul>
<h3 class="subsection">15.1.2 National regulations</h3>
<table class="data">
<tr><th>Country</th><th>Regulation</th><th>Status</th></tr>
<tr><td>Germany</td><td>Water hazard class (WGK)</td><td>WGK 1 — Slightly hazardous to water (AwSV; ID No. 95)</td></tr>
</table>
<h3 class="subsection">15.2 Chemical safety assessment</h3>
<p>No chemical safety assessment has been carried out.</p>
<h2 class="section">SECTION 16: Other information</h2>
<h3 class="subsection">Abbreviations and acronyms</h3>
<table class="data">
<tr><th>Abbreviation</th><th>Full form</th></tr>
<tr><td>ADR</td><td>European Agreement concerning the International Carriage of Dangerous Goods by Road</td></tr>
<tr><td>CAS</td><td>Chemical Abstract Service number</td></tr>
<tr><td>CLP</td><td>Classification, Labelling and Packaging Regulation (EC) No 1272/2008</td></tr>
<tr><td>DNEL</td><td>Derived No-Effect Level</td></tr>
<tr><td>IATA</td><td>International Air Transport Association</td></tr>
<tr><td>IMDG</td><td>International Maritime Dangerous Goods</td></tr>
<tr><td>PBT</td><td>Persistent, Bioaccumulative, Toxic</td></tr>
<tr><td>PNEC</td><td>Predicted No-Effect Concentration</td></tr>
<tr><td>REACH</td><td>Regulation (EC) No 1907/2006</td></tr>
<tr><td>STOT</td><td>Specific Target Organ Toxicity</td></tr>
<tr><td>vPvB</td><td>Very Persistent and Very Bioaccumulative</td></tr>
<tr><td>WGK</td><td>Water Hazard Class (Germany)</td></tr>
</table>
<h3 class="subsection">Full text of H- and EUH-statements</h3>
<table class="data">
<tr><th>Code</th><th>Text</th></tr>
<tr><td>H225</td><td>Highly flammable liquid and vapour.</td></tr>
<tr><td>H319</td><td>Causes serious eye irritation.</td></tr>
<tr><td>H336</td><td>May cause drowsiness or dizziness.</td></tr>
<tr><td>EUH066</td><td>Repeated exposure may cause skin dryness or cracking.</td></tr>
</table>
<br>
<p style="font-size: 8pt; color: {MUTED}; border-top: 1pt solid {BORDER}; padding-top: 5pt;">
<b>Disclaimer of Liability:</b> The information in this SDS was obtained from sources which we believe are reliable. However, the information is provided without any warranty, express or implied, regarding its correctness. The conditions or methods of handling, storage, use or disposal of the product are beyond our control. We do not assume responsibility and expressly disclaim liability for loss, damage or expense arising out of or in any way connected with the handling, storage, use or disposal of the product.
</p>
</body>
</html>
"""
def add_thead_tbody(html_str):
"""
Wrap each <table class="data">...</table>'s leading header row(s) in
<thead> and the remaining rows in <tbody>.
Why this is needed: WeasyPrint only repeats a header row on every page
a table spans if that row is inside a real <thead> element. The tables
above are flat <table><tr><th>...</tr><tr><td>...</tr>...</table> markup
with no <thead>/<tbody> at all, so when a table splits across a page
break, the header simply doesn't come back on the continuation page.
This is done as a single automated pass instead of hand-editing every
table, so every table.data element gets the fix consistently.
"""
table_pattern = re.compile(r'(<table class="data">)(.*?)(</table>)', re.DOTALL)
def process_table(match):
open_tag, body, close_tag = match.group(1), match.group(2), match.group(3)
rows = re.findall(r'<tr>.*?</tr>', body, re.DOTALL)
if not rows:
return match.group(0)
header_rows = []
data_rows = []
for row in rows:
# Leading row(s) containing <th> are treated as the header;
# once a row without <th> appears, everything after is body.
if '<th' in row and not data_rows:
header_rows.append(row)
else:
data_rows.append(row)
if not header_rows:
return match.group(0)
new_body = (
'<thead>' + ''.join(header_rows) + '</thead>'
'<tbody>' + ''.join(data_rows) + '</tbody>'
)
return open_tag + new_body + close_tag
return table_pattern.sub(process_table, html_str)
html = add_thead_tbody(html)
# ─── Generate ───
OUT_DIR = '/home/bachko/msds-tds/output'
os.makedirs(OUT_DIR, exist_ok=True)
html_path = os.path.join(OUT_DIR, 'sds-ethyl-acetate-en-fixed.html')
with open(html_path, 'w', encoding='utf-8') as f:
f.write(html)
print(f"HTML: {html_path}")
pdf_path = os.path.join(OUT_DIR, 'SDS_Ethyl_Acetate_REV1_EN_fixed.pdf')
HTML(string=html).write_pdf(pdf_path)
print(f"PDF: {pdf_path}")