feat(dwg): switch to ODA File Converter (libredwg too unstable)
ODA File Converter handles all DWG versions reliably. Uses xvfb for headless Qt operation in Docker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+14
-29
@@ -1,42 +1,27 @@
|
|||||||
FROM ubuntu:24.04 AS builder
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
build-essential autoconf automake libtool pkg-config \
|
|
||||||
wget ca-certificates && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Build libredwg from source (not available in apt repos)
|
|
||||||
ARG LIBREDWG_VERSION=0.13.3
|
|
||||||
RUN wget -q "https://github.com/LibreDWG/libredwg/releases/download/${LIBREDWG_VERSION}/libredwg-${LIBREDWG_VERSION}.tar.xz" && \
|
|
||||||
tar xf "libredwg-${LIBREDWG_VERSION}.tar.xz" && \
|
|
||||||
cd "libredwg-${LIBREDWG_VERSION}" && \
|
|
||||||
./configure --prefix=/usr/local --disable-static --disable-docs && \
|
|
||||||
make -j"$(nproc)" && \
|
|
||||||
make install
|
|
||||||
|
|
||||||
# ---
|
|
||||||
|
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
# Copy only the built binaries and libraries
|
# ODA File Converter needs Qt libs + virtual framebuffer for headless mode
|
||||||
COPY --from=builder /usr/local/bin/dwg2dxf /usr/local/bin/
|
RUN apt-get update && \
|
||||||
COPY --from=builder /usr/local/lib/libredwg* /usr/local/lib/
|
apt-get install -y --no-install-recommends \
|
||||||
|
python3 python3-pip wget ca-certificates \
|
||||||
RUN ldconfig && \
|
xvfb libxcb-xinerama0 libxcb-icccm4 libxcb-image0 \
|
||||||
apt-get update && \
|
libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \
|
||||||
apt-get install -y --no-install-recommends python3 python3-pip && \
|
libxcb-shape0 libxcb-xkb1 libxkbcommon0 libxkbcommon-x11-0 \
|
||||||
|
libglib2.0-0 libgl1 libfontconfig1 libfreetype6 && \
|
||||||
pip3 install --no-cache-dir --break-system-packages flask && \
|
pip3 install --no-cache-dir --break-system-packages flask && \
|
||||||
apt-get purge -y python3-pip && \
|
apt-get purge -y python3-pip && \
|
||||||
apt-get autoremove -y && \
|
apt-get autoremove -y && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Download and install ODA File Converter
|
||||||
|
RUN wget -q "https://download.opendesign.com/guestfiles/Demo/ODAFileConverter_QT6_lnxX64_8.3dll_25.12.deb" \
|
||||||
|
-O /tmp/oda.deb && \
|
||||||
|
dpkg -i /tmp/oda.deb || apt-get install -f -y && \
|
||||||
|
rm /tmp/oda.deb
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
|
|
||||||
RUN useradd --system --no-create-home converter
|
|
||||||
USER converter
|
|
||||||
|
|
||||||
EXPOSE 5001
|
EXPOSE 5001
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
|||||||
+32
-26
@@ -1,6 +1,8 @@
|
|||||||
"""Minimal DWG-to-DXF conversion microservice via libredwg."""
|
"""DWG-to-DXF conversion microservice via ODA File Converter."""
|
||||||
|
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
@@ -11,15 +13,16 @@ app = Flask(__name__)
|
|||||||
|
|
||||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
|
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
|
||||||
|
|
||||||
|
# ODA File Converter installs here on Ubuntu
|
||||||
|
ODA_BIN = "/usr/bin/ODAFileConverter"
|
||||||
|
|
||||||
|
|
||||||
@app.route("/health", methods=["GET"])
|
@app.route("/health", methods=["GET"])
|
||||||
def health():
|
def health():
|
||||||
"""Health check — verifies dwg2dxf binary is callable."""
|
"""Health check — verifies ODA File Converter is installed."""
|
||||||
try:
|
if os.path.isfile(ODA_BIN) and os.access(ODA_BIN, os.X_OK):
|
||||||
subprocess.run(["dwg2dxf", "--version"], capture_output=True, timeout=5)
|
return jsonify({"status": "ok", "converter": "ODAFileConverter"}), 200
|
||||||
return jsonify({"status": "ok", "dwg2dxf": "available"}), 200
|
return jsonify({"status": "error", "detail": "ODAFileConverter not found"}), 503
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"status": "error", "detail": str(e)}), 503
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/convert", methods=["POST"])
|
@app.route("/convert", methods=["POST"])
|
||||||
@@ -38,11 +41,13 @@ def convert():
|
|||||||
c if c.isalnum() or c in "._-" else "_" for c in original_name
|
c if c.isalnum() or c in "._-" else "_" for c in original_name
|
||||||
)
|
)
|
||||||
|
|
||||||
tmp_dir = os.path.join(tempfile.gettempdir(), f"dwg-{uuid.uuid4().hex}")
|
work_dir = os.path.join(tempfile.gettempdir(), f"dwg-{uuid.uuid4().hex}")
|
||||||
os.makedirs(tmp_dir, exist_ok=True)
|
input_dir = os.path.join(work_dir, "in")
|
||||||
|
output_dir = os.path.join(work_dir, "out")
|
||||||
|
os.makedirs(input_dir, exist_ok=True)
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
input_path = os.path.join(tmp_dir, safe_name)
|
input_path = os.path.join(input_dir, safe_name)
|
||||||
output_path = input_path.rsplit(".", 1)[0] + ".dxf"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
uploaded.save(input_path)
|
uploaded.save(input_path)
|
||||||
@@ -51,21 +56,30 @@ def convert():
|
|||||||
if file_size > MAX_FILE_SIZE:
|
if file_size > MAX_FILE_SIZE:
|
||||||
return jsonify({"error": f"File too large ({file_size} bytes)."}), 413
|
return jsonify({"error": f"File too large ({file_size} bytes)."}), 413
|
||||||
|
|
||||||
|
# ODA File Converter: input_dir output_dir version type recurse audit
|
||||||
|
# ACAD2018 = output DXF version, DXF = output format, 0 = no recurse, 1 = audit
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["dwg2dxf", input_path],
|
[
|
||||||
|
"xvfb-run", "--auto-servernum", "--server-args=-screen 0 1x1x24",
|
||||||
|
ODA_BIN, input_dir, output_dir, "ACAD2018", "DXF", "0", "1",
|
||||||
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.exists(output_path):
|
# Find the output DXF file
|
||||||
|
dxf_files = glob.glob(os.path.join(output_dir, "*.dxf"))
|
||||||
|
if not dxf_files:
|
||||||
stderr = result.stderr.decode("utf-8", errors="replace")
|
stderr = result.stderr.decode("utf-8", errors="replace")
|
||||||
return jsonify({
|
stdout = result.stdout.decode("utf-8", errors="replace")
|
||||||
"error": f"Conversion failed: {stderr or 'no output file'}"
|
detail = stderr or stdout or "No output file produced"
|
||||||
}), 500
|
return jsonify({"error": f"Conversion failed: {detail}"}), 500
|
||||||
|
|
||||||
|
dxf_path = dxf_files[0]
|
||||||
dxf_name = safe_name.rsplit(".", 1)[0] + ".dxf"
|
dxf_name = safe_name.rsplit(".", 1)[0] + ".dxf"
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
output_path,
|
dxf_path,
|
||||||
mimetype="application/dxf",
|
mimetype="application/dxf",
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
download_name=dxf_name,
|
download_name=dxf_name,
|
||||||
@@ -78,15 +92,7 @@ def convert():
|
|||||||
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
for f in os.listdir(tmp_dir):
|
shutil.rmtree(work_dir, ignore_errors=True)
|
||||||
try:
|
|
||||||
os.unlink(os.path.join(tmp_dir, f))
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
os.rmdir(tmp_dir)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user