Compare commits
10 Commits
v2.2.13-rc
...
feature/an
| Author | SHA1 | Date | |
|---|---|---|---|
| 58be922fb3 | |||
| 111672a614 | |||
| d1387d2685 | |||
| fda2944e5a | |||
| 8ae5ab73e3 | |||
| d8d35da574 | |||
| 792d91f9d5 | |||
| d10d038617 | |||
| 490ce4abb6 | |||
| bed9be1f81 |
@@ -13,6 +13,10 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
||||||
@@ -89,12 +93,12 @@ jobs:
|
|||||||
DOMAIN="e-tib.com"
|
DOMAIN="e-tib.com"
|
||||||
PRJ="etib"
|
PRJ="etib"
|
||||||
|
|
||||||
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
TARGET="testing"
|
TARGET="testing"
|
||||||
IMAGE_TAG="main-${SHORT_SHA}"
|
IMAGE_TAG="main-${SHORT_SHA}"
|
||||||
ENV_FILE=".env.testing"
|
ENV_FILE=".env.testing"
|
||||||
TRAEFIK_HOST="test.${DOMAIN}"
|
TRAEFIK_HOST="test.${DOMAIN}"
|
||||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
TARGET="production"
|
TARGET="production"
|
||||||
IMAGE_TAG="$REF"
|
IMAGE_TAG="$REF"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function generateMetadata(props: {
|
|||||||
template: '%s | E-TIB',
|
template: '%s | E-TIB',
|
||||||
default: 'E-TIB | Die Experten für Kabelnetzbau',
|
default: 'E-TIB | Die Experten für Kabelnetzbau',
|
||||||
},
|
},
|
||||||
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
||||||
metadataBase: new URL(baseUrl),
|
metadataBase: new URL(baseUrl),
|
||||||
manifest: '/manifest.webmanifest',
|
manifest: '/manifest.webmanifest',
|
||||||
alternates: {
|
alternates: {
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap
|
|||||||
import { HeroSection } from '@/components/blocks/HeroSection';
|
import { HeroSection } from '@/components/blocks/HeroSection';
|
||||||
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
||||||
|
|
||||||
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||||
|
|
||||||
|
const mdxComponents = {
|
||||||
|
ul: (props: any) => <ul className="list-disc pl-5 mb-6 text-neutral-600 text-sm space-y-2" {...props} />,
|
||||||
|
li: (props: any) => <li className="pl-1" {...props} />,
|
||||||
|
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
|
||||||
|
};
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
@@ -126,6 +134,13 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
|||||||
{ref.frontmatter.title}
|
{ref.frontmatter.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{/* Project Details / Content */}
|
||||||
|
{ref.content && (
|
||||||
|
<div className="flex-grow mb-6">
|
||||||
|
<MDXRemote source={ref.content} components={mdxComponents} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Technical Meta Data */}
|
{/* Technical Meta Data */}
|
||||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { getServerAppServices } from '@/lib/services/create-services.server';
|
|
||||||
|
|
||||||
export async function requestBrochureAction(formData: FormData) {
|
|
||||||
const services = getServerAppServices();
|
|
||||||
const logger = services.logger.child({ action: 'requestBrochureAction' });
|
|
||||||
|
|
||||||
const { headers } = await import('next/headers');
|
|
||||||
const requestHeaders = await headers();
|
|
||||||
|
|
||||||
if ('setServerContext' in services.analytics) {
|
|
||||||
(services.analytics as any).setServerContext({
|
|
||||||
userAgent: requestHeaders.get('user-agent') || undefined,
|
|
||||||
language: requestHeaders.get('accept-language')?.split(',')[0] || undefined,
|
|
||||||
referrer: requestHeaders.get('referer') || undefined,
|
|
||||||
ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
services.analytics.track('brochure-request-attempt');
|
|
||||||
|
|
||||||
const email = formData.get('email') as string;
|
|
||||||
const locale = (formData.get('locale') as string) || 'en';
|
|
||||||
|
|
||||||
// Anti-spam Honeypot Check
|
|
||||||
const honeypot = formData.get('company_website') as string;
|
|
||||||
if (honeypot) {
|
|
||||||
logger.warn('Spam detected via honeypot in brochure request', { email });
|
|
||||||
// Silently succeed to fool the bot without doing actual work
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
logger.warn('Missing email in brochure request');
|
|
||||||
return { success: false, error: 'Missing email address' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic email validation
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
return { success: false, error: 'Invalid email address' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Save to CMS - Removed along with Payload CMS
|
|
||||||
|
|
||||||
// 2. Notify via Gotify
|
|
||||||
try {
|
|
||||||
await services.notifications.notify({
|
|
||||||
title: '📑 Brochure Download Request',
|
|
||||||
message: `New brochure download request from ${email} (${locale})`,
|
|
||||||
priority: 3,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to send notification', { error });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Send Brochure via Email
|
|
||||||
const brochureUrl = `https://e-tib.com/brochure/etib-product-catalog-${locale}.pdf`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { sendEmail } = await import('@/lib/mail/mailer');
|
|
||||||
const { render } = await import('@mintel/mail');
|
|
||||||
const React = await import('react');
|
|
||||||
const { BrochureDeliveryEmail } = await import('@/components/emails/BrochureDeliveryEmail');
|
|
||||||
|
|
||||||
const html = await render(
|
|
||||||
React.createElement(BrochureDeliveryEmail, {
|
|
||||||
_email: email,
|
|
||||||
brochureUrl,
|
|
||||||
locale: locale as 'en' | 'de',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailResult = await sendEmail({
|
|
||||||
to: email,
|
|
||||||
subject: locale === 'de' ? 'Ihr E-TIB Kabelkatalog' : 'Your E-TIB Cable Catalog',
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emailResult.success) {
|
|
||||||
logger.info('Brochure email sent successfully', { email });
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to send brochure email', { error: emailResult.error, email });
|
|
||||||
services.errors.captureException(new Error(`Brochure email failed: ${emailResult.error}`), {
|
|
||||||
action: 'requestBrochureAction_email',
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Failed to send email. Please try again later.' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Exception while sending brochure email', { error });
|
|
||||||
return { success: false, error: 'Failed to send email. Please try again later.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Track success
|
|
||||||
services.analytics.track('brochure-request-success', {
|
|
||||||
locale,
|
|
||||||
delivery_method: 'email',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
@@ -11,24 +11,13 @@ export default function AnnotatorClientWrapper() {
|
|||||||
const [assets, setAssets] = useState<string[]>([]);
|
const [assets, setAssets] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
getAnnotatorAssets()
|
||||||
// Toggle key from Annotator.tsx is Shift+A
|
.then(fetchedAssets => {
|
||||||
if (e.shiftKey && e.key === "A") {
|
if (Array.isArray(fetchedAssets)) {
|
||||||
setAssets((prev) => {
|
setAssets(fetchedAssets);
|
||||||
if (prev.length === 0) {
|
}
|
||||||
getAnnotatorAssets().then(fetchedAssets => {
|
})
|
||||||
if (Array.isArray(fetchedAssets)) {
|
.catch(err => console.error('[AnnotatorClientWrapper] Fetch failed:', err));
|
||||||
setAssets(fetchedAssets);
|
|
||||||
}
|
|
||||||
}).catch(err => console.error('[AnnotatorClientWrapper] Fetch failed:', err));
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async (annotations: any[]) => {
|
const handleSubmit = async (annotations: any[]) => {
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
|
|||||||
return window.self !== window.top;
|
return window.self !== window.top;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isExcluded) return null;
|
|
||||||
|
|
||||||
const getSelector = (el: HTMLElement): string => {
|
const getSelector = (el: HTMLElement): string => {
|
||||||
try {
|
try {
|
||||||
return finder(el, {
|
return finder(el, {
|
||||||
@@ -236,6 +234,8 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
|
|||||||
const hoveredRect = useMemo(() => hoveredElement?.getBoundingClientRect(), [hoveredElement, updateTick]);
|
const hoveredRect = useMemo(() => hoveredElement?.getBoundingClientRect(), [hoveredElement, updateTick]);
|
||||||
const selectedRect = useMemo(() => selectedElement?.getBoundingClientRect(), [selectedElement, updateTick]);
|
const selectedRect = useMemo(() => selectedElement?.getBoundingClientRect(), [selectedElement, updateTick]);
|
||||||
|
|
||||||
|
if (isExcluded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="annotator-ui-ignore">
|
<div className="annotator-ui-ignore">
|
||||||
{/* 1. Global Toolbar */}
|
{/* 1. Global Toolbar */}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="show"
|
whileInView="show"
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, amount: 0.1 }}
|
||||||
>
|
>
|
||||||
{certificates.map((cert, index) => {
|
{certificates.map((cert, index) => {
|
||||||
const isIso = cert.type === 'iso';
|
const isIso = cert.type === 'iso';
|
||||||
|
|||||||
@@ -81,12 +81,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
||||||
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
|
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-12 px-4 md:px-0">
|
<div className="flex flex-row justify-between items-end mb-12 px-4 md:px-0">
|
||||||
<div className="max-w-2xl text-left">
|
<div className="max-w-2xl text-left">
|
||||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
|
<Button href={ctaHref} variant="ghost" className="bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0 whitespace-nowrap">
|
||||||
{ctaLabel}
|
{ctaLabel}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { MapPin, Factory, Zap, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||||
import { Location, defaultLocations, minorLocations, projectLocations } from '@/lib/map-data';
|
import { Location, projectLocations } from '@/lib/map-data';
|
||||||
import { standorteLocations } from '@/lib/standorte-data';
|
import { standorteLocations } from '@/lib/standorte-data';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
@@ -28,6 +28,59 @@ interface InteractiveGermanyMapProps {
|
|||||||
hideStandorte?: boolean;
|
hideStandorte?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
|
||||||
|
<div
|
||||||
|
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
||||||
|
style={{
|
||||||
|
left: `${loc.x}%`,
|
||||||
|
top: `${loc.y}%`,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => onEnter(loc)}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
onTouchStart={() => onEnter(loc)}
|
||||||
|
onClick={() => onEnter(loc)}
|
||||||
|
>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
||||||
|
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
MinorNode.displayName = 'MinorNode';
|
||||||
|
|
||||||
|
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
|
||||||
|
const isHQ = loc.type === 'hq';
|
||||||
|
const isBranch = loc.type === 'branch';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
||||||
|
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
||||||
|
onMouseEnter={() => onEnter(loc)}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
onTouchStart={() => onEnter(loc)}
|
||||||
|
onClick={() => onEnter(loc)}
|
||||||
|
>
|
||||||
|
{(isHQ || isBranch) && (
|
||||||
|
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
||||||
|
)}
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', delay: idx * 0.1 }}
|
||||||
|
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
|
||||||
|
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
|
||||||
|
} ${
|
||||||
|
isHQ || isBranch
|
||||||
|
? 'w-7 h-7 bg-primary text-[#050B14]'
|
||||||
|
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MajorNode.displayName = 'MajorNode';
|
||||||
|
|
||||||
export function InteractiveGermanyMap({
|
export function InteractiveGermanyMap({
|
||||||
badge,
|
badge,
|
||||||
title,
|
title,
|
||||||
@@ -40,22 +93,21 @@ export function InteractiveGermanyMap({
|
|||||||
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
||||||
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleMouseEnter = (loc: Location) => {
|
const handleMouseEnter = useCallback((loc: Location) => {
|
||||||
if (hoverTimeoutRef.current) {
|
if (hoverTimeoutRef.current) {
|
||||||
clearTimeout(hoverTimeoutRef.current);
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
hoverTimeoutRef.current = null;
|
hoverTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
setActiveLocation(loc);
|
setActiveLocation(loc);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setActiveLocation(null);
|
setActiveLocation(null);
|
||||||
}, 200);
|
}, 200);
|
||||||
hoverTimeoutRef.current = timeout;
|
hoverTimeoutRef.current = timeout;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations('InteractiveGermanyMap');
|
const t = useTranslations('InteractiveGermanyMap');
|
||||||
@@ -67,14 +119,11 @@ export function InteractiveGermanyMap({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const finalBadge = badge || tStandard('badge');
|
const finalBadge = badge || tStandard('badge');
|
||||||
// the map is mostly used with specific props, so we leave title & description to fallbacks if not provided
|
|
||||||
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
||||||
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||||
|
|
||||||
const finalLocations = React.useMemo(() => {
|
const finalLocations = useMemo(() => {
|
||||||
if (hideStandorte) return locations;
|
if (hideStandorte) return locations;
|
||||||
|
|
||||||
// Ensure the 3 standorte are always included, without duplicating if already present by ID
|
|
||||||
const base = [...locations];
|
const base = [...locations];
|
||||||
standorteLocations.forEach(sl => {
|
standorteLocations.forEach(sl => {
|
||||||
if (!base.some(l => l.id === sl.id)) {
|
if (!base.some(l => l.id === sl.id)) {
|
||||||
@@ -84,19 +133,28 @@ export function InteractiveGermanyMap({
|
|||||||
return base;
|
return base;
|
||||||
}, [locations, hideStandorte]);
|
}, [locations, hideStandorte]);
|
||||||
|
|
||||||
|
const mapBackground = useMemo(() => (
|
||||||
|
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
|
||||||
|
<Image
|
||||||
|
src="/germany-map.svg"
|
||||||
|
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
), [locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
||||||
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
|
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
|
||||||
{/* Animated Border Glow */}
|
|
||||||
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
|
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
|
||||||
|
|
||||||
{/* Background Effects */}
|
|
||||||
<div className={`absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14] ${!isHero && 'rounded-[2.5rem] md:rounded-[3.5rem]'} overflow-hidden`} />
|
<div className={`absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14] ${!isHero && 'rounded-[2.5rem] md:rounded-[3.5rem]'} overflow-hidden`} />
|
||||||
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[120px] pointer-events-none translate-x-1/3 -translate-y-1/3 overflow-hidden" />
|
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[120px] pointer-events-none translate-x-1/3 -translate-y-1/3 overflow-hidden" />
|
||||||
|
|
||||||
<div className={`relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] ${isHero ? 'max-w-7xl mx-auto px-4 sm:px-6' : 'p-8 md:p-12 lg:p-16'}`}>
|
<div className={`relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] ${isHero ? 'max-w-7xl mx-auto px-4 sm:px-6' : 'p-8 md:p-12 lg:p-16'}`}>
|
||||||
|
|
||||||
{/* Content Left */}
|
|
||||||
<div className="xl:col-span-5 text-white z-20">
|
<div className="xl:col-span-5 text-white z-20">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-8">
|
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-8">
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
@@ -117,7 +175,6 @@ export function InteractiveGermanyMap({
|
|||||||
{finalDescription}
|
{finalDescription}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Industrial Stats Grid */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{finalStats.map((stat, i) => {
|
{finalStats.map((stat, i) => {
|
||||||
const StatCard = () => (
|
const StatCard = () => (
|
||||||
@@ -148,85 +205,32 @@ export function InteractiveGermanyMap({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map Right */}
|
|
||||||
<div className="xl:col-span-7 relative flex items-center justify-center min-h-[500px] lg:min-h-[700px] z-10 pointer-events-none">
|
<div className="xl:col-span-7 relative flex items-center justify-center min-h-[500px] lg:min-h-[700px] z-10 pointer-events-none">
|
||||||
{/* Map Container - Enforce strict aspect ratio matching the SVG (1024x1024) */}
|
|
||||||
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
|
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
|
||||||
|
|
||||||
{/* Map SVG */}
|
{mapBackground}
|
||||||
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none">
|
|
||||||
<Image
|
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||||
src="/germany-map.svg"
|
<MinorNode
|
||||||
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
key={`minor-${loc.id || idx}`}
|
||||||
fill
|
loc={loc}
|
||||||
className="object-cover"
|
isActive={activeLocation === loc}
|
||||||
priority
|
onEnter={handleMouseEnter}
|
||||||
|
onLeave={handleMouseLeave}
|
||||||
/>
|
/>
|
||||||
</div>
|
))}
|
||||||
|
|
||||||
{/* Minor Location Markers (The 100+ generic projects) */}
|
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
|
||||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => {
|
<MajorNode
|
||||||
const isActive = activeLocation === loc;
|
key={loc.id}
|
||||||
return (
|
loc={loc}
|
||||||
<div
|
idx={idx}
|
||||||
key={`minor-${idx}`}
|
isActive={activeLocation?.id === loc.id}
|
||||||
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
onEnter={handleMouseEnter}
|
||||||
style={{
|
onLeave={handleMouseLeave}
|
||||||
left: `${loc.x}%`,
|
/>
|
||||||
top: `${loc.y}%`,
|
))}
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => handleMouseEnter(loc)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onTouchStart={() => handleMouseEnter(loc)}
|
|
||||||
onClick={() => handleMouseEnter(loc)}
|
|
||||||
>
|
|
||||||
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
|
||||||
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
|
||||||
</div>
|
|
||||||
)})}
|
|
||||||
|
|
||||||
{/* Major Location Markers (HQ, Branch, Project) */}
|
|
||||||
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => {
|
|
||||||
const isHQ = loc.type === 'hq';
|
|
||||||
const isBranch = loc.type === 'branch';
|
|
||||||
const isActive = activeLocation?.id === loc.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={loc.id}
|
|
||||||
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
|
||||||
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
|
||||||
onMouseEnter={() => handleMouseEnter(loc)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onTouchStart={() => handleMouseEnter(loc)}
|
|
||||||
onClick={() => handleMouseEnter(loc)}
|
|
||||||
>
|
|
||||||
{/* Ping Animation for HQ / Branch */}
|
|
||||||
{(isHQ || isBranch) && (
|
|
||||||
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Elegant Marker Dot */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ type: 'spring', delay: idx * 0.1 }}
|
|
||||||
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
|
|
||||||
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
|
|
||||||
} ${
|
|
||||||
isHQ || isBranch
|
|
||||||
? 'w-7 h-7 bg-primary text-[#050B14]'
|
|
||||||
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Global Tooltip */}
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{activeLocation && (
|
{activeLocation && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -235,17 +239,16 @@ export function InteractiveGermanyMap({
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="absolute z-[9999] pointer-events-auto"
|
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw-2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
|
||||||
style={{
|
style={{
|
||||||
left: `${activeLocation.x}%`,
|
'--md-left': `${activeLocation.x}%`,
|
||||||
top: `${activeLocation.y}%`,
|
'--md-top': `${activeLocation.y}%`,
|
||||||
transform: activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
||||||
}}
|
} as React.CSSProperties}
|
||||||
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl min-w-[200px] w-max max-w-[300px] text-white overflow-hidden">
|
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
|
||||||
|
|
||||||
{activeLocation.featuredImage && (
|
{activeLocation.featuredImage && (
|
||||||
<div className="relative w-full h-32 bg-neutral-900">
|
<div className="relative w-full h-32 bg-neutral-900">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
<section id="referenzen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
|
<section id="referenzen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
||||||
|
|
||||||
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-end gap-6">
|
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
||||||
<div>
|
<div className="text-left">
|
||||||
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
|
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
|
<div className="text-left max-w-3xl mx-auto md:mx-0 mb-8 md:mb-16">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export function ServiceDetailGrid({
|
|||||||
{/* On a white card, a very subtle primary/grey shine works best */}
|
{/* On a white card, a very subtle primary/grey shine works best */}
|
||||||
<HoverShineOverlay shineColor="via-primary/5" />
|
<HoverShineOverlay shineColor="via-primary/5" />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6">
|
<div className="relative z-10 flex flex-row justify-between items-start mb-12 gap-6">
|
||||||
<div className="pt-2 flex-1 min-w-0 pr-4">
|
<div className="pt-2 flex-1 min-w-0 pr-4">
|
||||||
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
|
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
|
||||||
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
|
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, amount: 0.1 }}
|
||||||
>
|
>
|
||||||
{companiesData.map((company: any, index: number) => {
|
{companiesData.map((company: any, index: number) => {
|
||||||
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Container,
|
|
||||||
Head,
|
|
||||||
Heading,
|
|
||||||
Hr,
|
|
||||||
Html,
|
|
||||||
Preview,
|
|
||||||
Section,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
} from '@react-email/components';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
interface BrochureDeliveryEmailProps {
|
|
||||||
_email: string;
|
|
||||||
brochureUrl: string;
|
|
||||||
locale: 'en' | 'de';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BrochureDeliveryEmail = ({
|
|
||||||
_email,
|
|
||||||
brochureUrl,
|
|
||||||
locale = 'en',
|
|
||||||
}: BrochureDeliveryEmailProps) => {
|
|
||||||
const t =
|
|
||||||
locale === 'de'
|
|
||||||
? {
|
|
||||||
subject: 'Ihr E-TIB Kabelkatalog',
|
|
||||||
greeting: 'Vielen Dank für Ihr Interesse an E-TIB.',
|
|
||||||
body: 'Anbei erhalten Sie den Link zu unserem aktuellen Produktkatalog. Dieser enthält alle wichtigen technischen Spezifikationen und detaillierten Produktdaten.',
|
|
||||||
button: 'Katalog herunterladen',
|
|
||||||
footer: 'Diese E-Mail wurde von e-tib.com gesendet.',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
subject: 'Your E-TIB Cable Catalog',
|
|
||||||
greeting: 'Thank you for your interest in E-TIB.',
|
|
||||||
body: 'Below you will find the link to our current product catalog. It contains all key technical specifications and detailed product data.',
|
|
||||||
button: 'Download Catalog',
|
|
||||||
footer: 'This email was sent from e-tib.com.',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<Preview>{t.subject}</Preview>
|
|
||||||
<Body style={main}>
|
|
||||||
<Container style={container}>
|
|
||||||
<Section style={headerSection}>
|
|
||||||
<Heading style={h1}>{t.subject}</Heading>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section style={section}>
|
|
||||||
<Text style={text}>
|
|
||||||
<strong>{t.greeting}</strong>
|
|
||||||
</Text>
|
|
||||||
<Text style={text}>{t.body}</Text>
|
|
||||||
|
|
||||||
<Section style={buttonContainer}>
|
|
||||||
<Button style={button} href={brochureUrl}>
|
|
||||||
{t.button}
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Hr style={hr} />
|
|
||||||
</Section>
|
|
||||||
<Text style={footer}>{t.footer}</Text>
|
|
||||||
</Container>
|
|
||||||
</Body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BrochureDeliveryEmail;
|
|
||||||
|
|
||||||
const main = {
|
|
||||||
backgroundColor: '#f6f9fc',
|
|
||||||
fontFamily:
|
|
||||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
|
||||||
};
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
margin: '0 auto',
|
|
||||||
padding: '0 0 48px',
|
|
||||||
marginBottom: '64px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
border: '1px solid #e6ebf1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerSection = {
|
|
||||||
backgroundColor: '#111111',
|
|
||||||
padding: '32px 48px',
|
|
||||||
borderBottom: '4px solid #117c61',
|
|
||||||
};
|
|
||||||
|
|
||||||
const h1 = {
|
|
||||||
color: '#ffffff',
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
margin: '0',
|
|
||||||
};
|
|
||||||
|
|
||||||
const section = {
|
|
||||||
padding: '32px 48px 0',
|
|
||||||
};
|
|
||||||
|
|
||||||
const text = {
|
|
||||||
color: '#333',
|
|
||||||
fontSize: '16px',
|
|
||||||
lineHeight: '24px',
|
|
||||||
textAlign: 'left' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonContainer = {
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
marginTop: '32px',
|
|
||||||
marginBottom: '32px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const button = {
|
|
||||||
backgroundColor: '#117c61',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
textDecoration: 'none',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
display: 'inline-block',
|
|
||||||
padding: '16px 32px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const hr = {
|
|
||||||
borderColor: '#e6ebf1',
|
|
||||||
margin: '20px 0',
|
|
||||||
};
|
|
||||||
|
|
||||||
const footer = {
|
|
||||||
color: '#8898aa',
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: '16px',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
marginTop: '20px',
|
|
||||||
};
|
|
||||||
@@ -19,7 +19,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-[#050B14] text-neutral-light pt-40 pb-10 md:pt-40 md:pb-16 relative overflow-hidden border-t border-white/5">
|
<footer className="bg-[#050B14] text-neutral-light pt-24 pb-28 md:pt-32 md:pb-16 relative overflow-hidden border-t border-white/5">
|
||||||
{/* Subtle background tech grid */}
|
{/* Subtle background tech grid */}
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
|
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
|
||||||
{locale === 'de'
|
{locale === 'de'
|
||||||
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
||||||
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
: 'Experts in cable network construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
|||||||
}, [currentLocale, navLinks]);
|
}, [currentLocale, navLinks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-6 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
|
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-2 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
|
||||||
<nav className="flex justify-around items-center max-w-md mx-auto">
|
<nav className="flex justify-around items-center max-w-md mx-auto">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
// Determine if active
|
// Determine if active
|
||||||
|
|||||||
BIN
contact_sheet.jpg
Normal file
BIN
contact_sheet.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Kabelleitungstiefbau im Bereich Telekommunikation"
|
title: "Kabelleitungsnetzbau im Bereich Telekommunikation"
|
||||||
date: "2024-03-20"
|
date: "2024-03-20"
|
||||||
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungstiefbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
|
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungsnetzbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
|
||||||
layout: "fullBleed"
|
layout: "fullBleed"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -117,12 +117,12 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
|||||||
ctaHref="/de/kompetenzen"
|
ctaHref="/de/kompetenzen"
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: "Kabelleitungstiefbau",
|
title: "Kabelleitungsnetzbau",
|
||||||
description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
||||||
tag: "Energie",
|
tag: "Energie",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/de/kabeltiefbau",
|
href: "/de/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungstiefbau" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bohrtechnik",
|
title: "Bohrtechnik",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: "Kabelleitungstiefbau"
|
title: "Kabelleitungsnetzbau"
|
||||||
date: "2024-03-20"
|
date: "2024-03-20"
|
||||||
excerpt: "Professioneller Kabelleitungstiefbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
|
excerpt: "Professioneller Kabelleitungsnetzbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
|
||||||
layout: "fullBleed"
|
layout: "fullBleed"
|
||||||
---
|
---
|
||||||
|
|
||||||
<HeroSection
|
<HeroSection
|
||||||
badge="Kernkompetenz"
|
badge="Kernkompetenz"
|
||||||
title="Kabelleitungstiefbau"
|
title="Kabelleitungsnetzbau"
|
||||||
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
||||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||||
alignment="center"
|
alignment="center"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ layout: "fullBleed"
|
|||||||
<HomeHero
|
<HomeHero
|
||||||
badge="Unsere Leistungen"
|
badge="Unsere Leistungen"
|
||||||
title="Alles aus einer Hand"
|
title="Alles aus einer Hand"
|
||||||
description="Als Full-Service-Partner im Bereich Kabelleitungstiefbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
|
description="Als Full-Service-Partner im Bereich Kabelleitungsnetzbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
|
||||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||||
linkText="Kontakt aufnehmen"
|
linkText="Kontakt aufnehmen"
|
||||||
linkHref="/de/kontakt"
|
linkHref="/de/kontakt"
|
||||||
@@ -23,12 +23,12 @@ layout: "fullBleed"
|
|||||||
ctaHref="/de/kontakt"
|
ctaHref="/de/kontakt"
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: "Kabelleitungstiefbau",
|
title: "Kabelleitungsnetzbau",
|
||||||
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||||
tag: "Energie",
|
tag: "Energie",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/de/kabeltiefbau",
|
href: "/de/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungstiefbau" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bohrtechnik",
|
title: "Bohrtechnik",
|
||||||
@@ -70,7 +70,7 @@ layout: "fullBleed"
|
|||||||
panels={[
|
panels={[
|
||||||
{
|
{
|
||||||
id: "energy",
|
id: "energy",
|
||||||
title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)",
|
title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)",
|
||||||
icon: "energy",
|
icon: "energy",
|
||||||
lists: [
|
lists: [
|
||||||
[
|
[
|
||||||
@@ -86,7 +86,7 @@ layout: "fullBleed"
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fiber",
|
id: "fiber",
|
||||||
title: "Kabelleitungstiefbau (Telekommunikation)",
|
title: "Kabelleitungsnetzbau (Telekommunikation)",
|
||||||
icon: "fiber",
|
icon: "fiber",
|
||||||
lists: [
|
lists: [
|
||||||
[
|
[
|
||||||
@@ -143,7 +143,7 @@ layout: "fullBleed"
|
|||||||
<JsonLd data={[
|
<JsonLd data={[
|
||||||
{
|
{
|
||||||
"@type": "Service",
|
"@type": "Service",
|
||||||
"name": "Kabelleitungstiefbau",
|
"name": "Kabelleitungsnetzbau",
|
||||||
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ layout: "fullBleed"
|
|||||||
|
|
||||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||||
<blockquote>
|
<blockquote>
|
||||||
Wir bieten unseren Kunden alles aus einer Hand – von der Grobplanung Ihrer Projektidee über den Kabelleitungstiefbau bis zum Bau Ihres erneuerbaren Energieprojektes.
|
Wir bieten unseren Kunden alles aus einer Hand – von der Grobplanung Ihrer Projektidee über den Kabelleitungsnetzbau bis zum Bau Ihres erneuerbaren Energieprojektes.
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
## Fundament für den Projekterfolg
|
## Fundament für den Projekterfolg
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Energiekontor AG"
|
|||||||
date: "2026-06-01"
|
date: "2026-06-01"
|
||||||
dateString: "01/2025 bis 06/2026"
|
dateString: "01/2025 bis 06/2026"
|
||||||
location: "18334 Dettmannsdorf"
|
location: "18334 Dettmannsdorf"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg"
|
featuredImage: "/assets/photos/DJI_0301.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 12.300 m Kabelgraben ausheben und verfüllen
|
- 12.300 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "wpd GmbH"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "seit 03/2026"
|
dateString: "seit 03/2026"
|
||||||
location: "24808 Jevenstedt"
|
location: "24808 Jevenstedt"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-15.jpg"
|
featuredImage: "/assets/photos/DJI_0037.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 500 m Kabelgraben ausheben und verfüllen
|
- 500 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "E.DIS Netz GmbH"
|
|||||||
date: "2025-06-01"
|
date: "2025-06-01"
|
||||||
dateString: "01/2025 bis 06/2025"
|
dateString: "01/2025 bis 06/2025"
|
||||||
location: "14669 Ketzin"
|
location: "14669 Ketzin"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-14.jpg"
|
featuredImage: "/assets/photos/DSC02643.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 1.600 m Kabelgraben ausheben und verfüllen
|
- 1.600 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "02/2025 bis 03/2026"
|
dateString: "02/2025 bis 03/2026"
|
||||||
location: "48346 Ostbevern"
|
location: "48346 Ostbevern"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
|
featuredImage: "/assets/photos/DJI_0048.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 4.100 m Kabelgraben ausheben und verfüllen
|
- 4.100 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "NaturStromAnlagen GmbH"
|
|||||||
date: "2025-02-01"
|
date: "2025-02-01"
|
||||||
dateString: "04/2024 bis 02/2025"
|
dateString: "04/2024 bis 02/2025"
|
||||||
location: "15326 Petershagen"
|
location: "15326 Petershagen"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-12.jpg"
|
featuredImage: "/assets/photos/Screenshot_2025-09-30_085736.png"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 12.000 m Kabelgraben ausheben und verfüllen
|
- 12.000 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Trianel GmbH"
|
|||||||
date: "2025-08-01"
|
date: "2025-08-01"
|
||||||
dateString: "02/2025 bis 08/2025"
|
dateString: "02/2025 bis 08/2025"
|
||||||
location: "15345 Prötzel"
|
location: "15345 Prötzel"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
featuredImage: "/assets/photos/DJI_0243.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 2.600 m Kabelgraben ausheben und verfüllen
|
- 2.600 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: "Home - Experts for Cable Construction & Civil Engineering"
|
title: "Home - Experts for cable network construction & Civil Engineering"
|
||||||
description: "E-TIB GmbH is your reliable partner for complex cable routes, horizontal directional drilling, surveying, and telecommunications infrastructure in Germany."
|
description: "E-TIB GmbH is your reliable partner for complex cable routes, horizontal directional drilling, surveying, and telecommunications infrastructure in Germany."
|
||||||
---
|
---
|
||||||
|
|
||||||
<HomeHero
|
<HomeHero
|
||||||
|
|
||||||
title={"The Experts for\nCable Construction"}
|
title={"The Experts for\ncable network construction"}
|
||||||
description="We realize complex supply lines for a modern society. Reliable, innovative and with the highest precision in cable construction and drilling technology."
|
description="We realize complex supply lines for a modern society. Reliable, innovative and with the highest precision in cable network construction and drilling technology."
|
||||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||||
linkText="Our Services"
|
linkText="Our Services"
|
||||||
linkHref="/en/competencies"
|
linkHref="/en/competencies"
|
||||||
@@ -122,7 +122,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
|||||||
tag: "Energy",
|
tag: "Energy",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/en/kabeltiefbau",
|
href: "/en/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Cable Civil Engineering" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Network Construction" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Drilling Technology",
|
title: "Drilling Technology",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Competencies - Cable Civil Engineering & Horizontal Directional Drilling"
|
title: "Competencies - Cable Civil Engineering & Horizontal Directional Drilling"
|
||||||
date: "2024-03-20"
|
date: "2024-03-20"
|
||||||
excerpt: "From cable construction to electrical installation: We provide holistic solutions for modern network infrastructures."
|
excerpt: "From cable network construction to electrical installation: We provide holistic solutions for modern network infrastructures."
|
||||||
layout: "fullBleed"
|
layout: "fullBleed"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ layout: "fullBleed"
|
|||||||
tag: "Energy",
|
tag: "Energy",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/en/kabeltiefbau",
|
href: "/en/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Cable Civil Engineering" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Network Construction" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Drilling Technology",
|
title: "Drilling Technology",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Energiekontor AG"
|
|||||||
date: "2026-06-01"
|
date: "2026-06-01"
|
||||||
dateString: "01/2025 to 06/2026"
|
dateString: "01/2025 to 06/2026"
|
||||||
location: "18334 Dettmannsdorf"
|
location: "18334 Dettmannsdorf"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg"
|
featuredImage: "/assets/photos/DJI_0301.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 12,300 m cable trench
|
- Excavation and backfilling of 12,300 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "wpd GmbH"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "since 03/2026"
|
dateString: "since 03/2026"
|
||||||
location: "24808 Jevenstedt"
|
location: "24808 Jevenstedt"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-15.jpg"
|
featuredImage: "/assets/photos/DJI_0037.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 500 m cable trench
|
- Excavation and backfilling of 500 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "E.DIS Netz GmbH"
|
|||||||
date: "2025-06-01"
|
date: "2025-06-01"
|
||||||
dateString: "01/2025 to 06/2025"
|
dateString: "01/2025 to 06/2025"
|
||||||
location: "14669 Ketzin"
|
location: "14669 Ketzin"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-14.jpg"
|
featuredImage: "/assets/photos/DSC02643.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 1,600 m cable trench
|
- Excavation and backfilling of 1,600 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "02/2025 to 03/2026"
|
dateString: "02/2025 to 03/2026"
|
||||||
location: "48346 Ostbevern"
|
location: "48346 Ostbevern"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
|
featuredImage: "/assets/photos/DJI_0048.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 4,100 m cable trench
|
- Excavation and backfilling of 4,100 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "NaturStromAnlagen GmbH"
|
|||||||
date: "2025-02-01"
|
date: "2025-02-01"
|
||||||
dateString: "04/2024 to 02/2025"
|
dateString: "04/2024 to 02/2025"
|
||||||
location: "15326 Petershagen"
|
location: "15326 Petershagen"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-12.jpg"
|
featuredImage: "/assets/photos/Screenshot_2025-09-30_085736.png"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 12,000 m cable trench
|
- Excavation and backfilling of 12,000 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Trianel GmbH"
|
|||||||
date: "2025-08-01"
|
date: "2025-08-01"
|
||||||
dateString: "02/2025 to 08/2025"
|
dateString: "02/2025 to 08/2025"
|
||||||
location: "15345 Prötzel"
|
location: "15345 Prötzel"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
featuredImage: "/assets/photos/DJI_0243.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 2,600 m cable trench
|
- Excavation and backfilling of 2,600 m cable trench
|
||||||
|
|||||||
@@ -1,388 +0,0 @@
|
|||||||
/**
|
|
||||||
* Excel Products Module
|
|
||||||
*
|
|
||||||
* Provides typed access to technical product data from Excel source files.
|
|
||||||
* Reuses the parsing logic from the PDF datasheet generator.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const EXCEL_SOURCE_FILES = [
|
|
||||||
path.join(process.cwd(), 'data/excel/high-voltage.xlsx'),
|
|
||||||
path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'),
|
|
||||||
path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'),
|
|
||||||
path.join(process.cwd(), 'data/excel/solar-cables.xlsx'),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export type ExcelRow = Record<string, string | number | boolean | Date>;
|
|
||||||
|
|
||||||
export interface ExcelMatch {
|
|
||||||
rows: ExcelRow[];
|
|
||||||
units: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TechnicalData {
|
|
||||||
configurations: string[];
|
|
||||||
attributes: Array<{
|
|
||||||
name: string;
|
|
||||||
options: string[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductLookupParams {
|
|
||||||
name?: string;
|
|
||||||
slug?: string;
|
|
||||||
sku?: string;
|
|
||||||
translationKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache singleton
|
|
||||||
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize Excel key to match product identifiers
|
|
||||||
* Examples:
|
|
||||||
* - "NA2XS(FL)2Y" -> "NA2XSFL2Y"
|
|
||||||
* - "na2xsfl2y-3" -> "NA2XSFL2Y"
|
|
||||||
*/
|
|
||||||
function normalizeExcelKey(value: string): string {
|
|
||||||
return String(value || '')
|
|
||||||
.toUpperCase()
|
|
||||||
.replace(/-\d+$/g, '')
|
|
||||||
.replace(/[^A-Z0-9]+/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize value (strip HTML, trim whitespace)
|
|
||||||
*/
|
|
||||||
function normalizeValue(value: string): string {
|
|
||||||
if (!value) return '';
|
|
||||||
return String(value)
|
|
||||||
.replace(/<[^>]*>/g, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if value looks numeric
|
|
||||||
*/
|
|
||||||
function looksNumeric(value: string): boolean {
|
|
||||||
const v = normalizeValue(value).replace(/,/g, '.');
|
|
||||||
return /^-?\d+(?:\.\d+)?$/.test(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Excel rows from a file using xlsx library
|
|
||||||
*/
|
|
||||||
function loadExcelRows(filePath: string): ExcelRow[] {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
console.warn(`[excel-products] File not found: ${filePath}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const workbook = XLSX.readFile(filePath, {
|
|
||||||
cellDates: false,
|
|
||||||
cellNF: false,
|
|
||||||
cellText: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the first sheet
|
|
||||||
const sheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
|
|
||||||
// Convert to JSON
|
|
||||||
const rows = XLSX.utils.sheet_to_json(worksheet, {
|
|
||||||
defval: '',
|
|
||||||
raw: false
|
|
||||||
}) as ExcelRow[];
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[excel-products] Error reading ${filePath}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Excel index from all source files
|
|
||||||
*/
|
|
||||||
export function getExcelIndex(): Map<string, ExcelMatch> {
|
|
||||||
if (EXCEL_INDEX) return EXCEL_INDEX;
|
|
||||||
|
|
||||||
const idx = new Map<string, ExcelMatch>();
|
|
||||||
|
|
||||||
for (const file of EXCEL_SOURCE_FILES) {
|
|
||||||
const rows = loadExcelRows(file);
|
|
||||||
if (rows.length === 0) continue;
|
|
||||||
|
|
||||||
// Find units row (if present)
|
|
||||||
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
|
|
||||||
const units: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (unitsRow) {
|
|
||||||
for (const [k, v] of Object.entries(unitsRow)) {
|
|
||||||
if (k === 'Part Number') continue;
|
|
||||||
const unit = normalizeValue(String(v ?? ''));
|
|
||||||
if (unit) units[k] = unit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index rows by Part Number
|
|
||||||
for (const r of rows) {
|
|
||||||
const pn = r?.['Part Number'];
|
|
||||||
if (!pn || pn === 'Units') continue;
|
|
||||||
|
|
||||||
const key = normalizeExcelKey(String(pn));
|
|
||||||
if (!key) continue;
|
|
||||||
|
|
||||||
const cur = idx.get(key);
|
|
||||||
if (!cur) {
|
|
||||||
idx.set(key, { rows: [r], units });
|
|
||||||
} else {
|
|
||||||
cur.rows.push(r);
|
|
||||||
// Keep the most comprehensive units
|
|
||||||
if (Object.keys(cur.units).length < Object.keys(units).length) {
|
|
||||||
cur.units = units;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EXCEL_INDEX = idx;
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find Excel match for a product using various identifiers
|
|
||||||
*/
|
|
||||||
function findExcelForProduct(params: ProductLookupParams): ExcelMatch | null {
|
|
||||||
const idx = getExcelIndex();
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
params.name,
|
|
||||||
params.slug ? params.slug.replace(/-\d+$/g, '') : '',
|
|
||||||
params.sku,
|
|
||||||
params.translationKey,
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
|
|
||||||
for (const c of candidates) {
|
|
||||||
const key = normalizeExcelKey(c);
|
|
||||||
const match = idx.get(key);
|
|
||||||
if (match && match.rows.length) return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guess column key based on patterns
|
|
||||||
*/
|
|
||||||
function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
|
|
||||||
const keys = Object.keys(row || {});
|
|
||||||
|
|
||||||
for (const re of patterns) {
|
|
||||||
const k = keys.find(x => {
|
|
||||||
const key = String(x);
|
|
||||||
|
|
||||||
// Specific exclusions to prevent wrong matches
|
|
||||||
if (re.test('conductor') && /ross section conductor/i.test(key)) return false;
|
|
||||||
if (re.test('insulation thickness') && /Diameter over insulation/i.test(key)) return false;
|
|
||||||
if (re.test('conductor') && !/^conductor$/i.test(key)) return false;
|
|
||||||
if (re.test('insulation') && !/^insulation$/i.test(key)) return false;
|
|
||||||
if (re.test('sheath') && !/^sheath$/i.test(key)) return false;
|
|
||||||
if (re.test('norm') && !/^norm$/i.test(key)) return false;
|
|
||||||
|
|
||||||
return re.test(key);
|
|
||||||
});
|
|
||||||
if (k) return k;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get unique non-empty values from an array
|
|
||||||
*/
|
|
||||||
function getUniqueNonEmpty(options: string[]): string[] {
|
|
||||||
const uniq: string[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const v of options.map(normalizeValue).filter(Boolean)) {
|
|
||||||
const k = v.toLowerCase();
|
|
||||||
if (seen.has(k)) continue;
|
|
||||||
seen.add(k);
|
|
||||||
uniq.push(v);
|
|
||||||
}
|
|
||||||
return uniq;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get technical data for a product from Excel files
|
|
||||||
*/
|
|
||||||
export function getExcelTechnicalDataForProduct(params: ProductLookupParams): TechnicalData | null {
|
|
||||||
const match = findExcelForProduct(params);
|
|
||||||
if (!match || match.rows.length === 0) return null;
|
|
||||||
|
|
||||||
const rows = match.rows;
|
|
||||||
const sample = rows[0];
|
|
||||||
|
|
||||||
// Find cross-section column
|
|
||||||
const csKey = guessColumnKey(sample, [
|
|
||||||
/number of cores and cross-section/i,
|
|
||||||
/cross.?section/i,
|
|
||||||
/ross section conductor/i,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!csKey) return null;
|
|
||||||
|
|
||||||
// Extract configurations
|
|
||||||
const voltageKey = guessColumnKey(sample, [/rated voltage/i, /voltage rating/i, /spannungs/i, /nennspannung/i]);
|
|
||||||
|
|
||||||
const configurations = rows
|
|
||||||
.map(r => {
|
|
||||||
const cs = normalizeValue(String(r?.[csKey] ?? ''));
|
|
||||||
const v = voltageKey ? normalizeValue(String(r?.[voltageKey] ?? '')) : '';
|
|
||||||
if (!cs) return '';
|
|
||||||
if (!v) return cs;
|
|
||||||
const vHasUnit = /\bkv\b/i.test(v);
|
|
||||||
const vText = vHasUnit ? v : `${v} kV`;
|
|
||||||
return `${cs} - ${vText}`;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (configurations.length === 0) return null;
|
|
||||||
|
|
||||||
// Extract technical attributes
|
|
||||||
const attributes: Array<{ name: string; options: string[] }> = [];
|
|
||||||
|
|
||||||
// Key technical columns
|
|
||||||
const outerKey = guessColumnKey(sample, [/outer diameter\b/i, /outer diameter.*approx/i, /outer diameter of cable/i, /außen/i]);
|
|
||||||
const weightKey = guessColumnKey(sample, [/weight\b/i, /gewicht/i, /cable weight/i]);
|
|
||||||
const dcResKey = guessColumnKey(sample, [/dc resistance/i, /resistance conductor/i, /leiterwiderstand/i]);
|
|
||||||
const ratedVoltKey = voltageKey;
|
|
||||||
const testVoltKey = guessColumnKey(sample, [/test voltage/i, /prüfspannung/i]);
|
|
||||||
const tempRangeKey = guessColumnKey(sample, [/operating temperature range/i, /temperature range/i, /temperaturbereich/i]);
|
|
||||||
const conductorKey = guessColumnKey(sample, [/^conductor$/i]);
|
|
||||||
const insulationKey = guessColumnKey(sample, [/^insulation$/i]);
|
|
||||||
const sheathKey = guessColumnKey(sample, [/^sheath$/i]);
|
|
||||||
const normKey = guessColumnKey(sample, [/^norm$/i, /^standard$/i]);
|
|
||||||
const cprKey = guessColumnKey(sample, [/cpr class/i]);
|
|
||||||
const packagingKey = guessColumnKey(sample, [/^packaging$/i]);
|
|
||||||
const shapeKey = guessColumnKey(sample, [/shape of conductor/i]);
|
|
||||||
const flameKey = guessColumnKey(sample, [/flame retardant/i]);
|
|
||||||
const diamCondKey = guessColumnKey(sample, [/diameter conductor/i]);
|
|
||||||
const diamInsKey = guessColumnKey(sample, [/diameter over insulation/i]);
|
|
||||||
const diamScreenKey = guessColumnKey(sample, [/diameter over screen/i]);
|
|
||||||
const metalScreenKey = guessColumnKey(sample, [/metallic screen/i]);
|
|
||||||
const capacitanceKey = guessColumnKey(sample, [/capacitance/i]);
|
|
||||||
const reactanceKey = guessColumnKey(sample, [/reactance/i]);
|
|
||||||
const electricalStressKey = guessColumnKey(sample, [/electrical stress/i]);
|
|
||||||
const pullingForceKey = guessColumnKey(sample, [/max\. pulling force/i, /pulling force/i]);
|
|
||||||
const heatingTrefoilKey = guessColumnKey(sample, [/heating time constant.*trefoil/i]);
|
|
||||||
const heatingFlatKey = guessColumnKey(sample, [/heating time constant.*flat/i]);
|
|
||||||
const currentAirTrefoilKey = guessColumnKey(sample, [/current ratings in air.*trefoil/i]);
|
|
||||||
const currentAirFlatKey = guessColumnKey(sample, [/current ratings in air.*flat/i]);
|
|
||||||
const currentGroundTrefoilKey = guessColumnKey(sample, [/current ratings in ground.*trefoil/i]);
|
|
||||||
const currentGroundFlatKey = guessColumnKey(sample, [/current ratings in ground.*flat/i]);
|
|
||||||
const scCurrentCondKey = guessColumnKey(sample, [/conductor shortcircuit current/i]);
|
|
||||||
const scCurrentScreenKey = guessColumnKey(sample, [/screen shortcircuit current/i]);
|
|
||||||
const minLayKey = guessColumnKey(sample, [/minimal temperature for laying/i]);
|
|
||||||
const minStoreKey = guessColumnKey(sample, [/minimal storage temperature/i]);
|
|
||||||
const maxOpKey = guessColumnKey(sample, [/maximal operating conductor temperature/i, /max\. operating/i]);
|
|
||||||
const maxScKey = guessColumnKey(sample, [/maximal short-circuit temperature/i, /short\s*circuit\s*temperature/i]);
|
|
||||||
const insThkKey = guessColumnKey(sample, [/nominal insulation thickness/i, /insulation thickness/i]);
|
|
||||||
const sheathThkKey = guessColumnKey(sample, [/nominal sheath thickness/i, /minimum sheath thickness/i]);
|
|
||||||
const maxResKey = guessColumnKey(sample, [/maximum resistance of conductor/i]);
|
|
||||||
const bendKey = guessColumnKey(sample, [/bending radius/i, /min\. bending radius/i]);
|
|
||||||
|
|
||||||
// Helper to add attribute
|
|
||||||
const addAttr = (name: string, key: string | null, unit?: string) => {
|
|
||||||
if (!key) return;
|
|
||||||
const options = rows
|
|
||||||
.map(r => normalizeValue(String(r?.[key] ?? '')))
|
|
||||||
.map(v => (unit && v && looksNumeric(v) ? `${v} ${unit}` : v))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (options.length === 0) return;
|
|
||||||
|
|
||||||
const uniqueOptions = getUniqueNonEmpty(options);
|
|
||||||
attributes.push({ name, options: uniqueOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add attributes
|
|
||||||
addAttr('Outer diameter', outerKey, 'mm');
|
|
||||||
addAttr('Weight', weightKey, 'kg/km');
|
|
||||||
addAttr('DC resistance at 20 °C', dcResKey, 'Ω/km');
|
|
||||||
addAttr('Rated voltage', ratedVoltKey, '');
|
|
||||||
addAttr('Test voltage', testVoltKey, '');
|
|
||||||
addAttr('Operating temperature range', tempRangeKey, '');
|
|
||||||
addAttr('Minimal temperature for laying', minLayKey, '');
|
|
||||||
addAttr('Minimal storage temperature', minStoreKey, '');
|
|
||||||
addAttr('Maximal operating conductor temperature', maxOpKey, '');
|
|
||||||
addAttr('Maximal short-circuit temperature', maxScKey, '');
|
|
||||||
addAttr('Nominal insulation thickness', insThkKey, 'mm');
|
|
||||||
addAttr('Nominal sheath thickness', sheathThkKey, 'mm');
|
|
||||||
addAttr('Maximum resistance of conductor', maxResKey, 'Ω/km');
|
|
||||||
addAttr('Conductor', conductorKey, '');
|
|
||||||
addAttr('Insulation', insulationKey, '');
|
|
||||||
addAttr('Sheath', sheathKey, '');
|
|
||||||
addAttr('Standard', normKey, '');
|
|
||||||
addAttr('Conductor diameter', diamCondKey, 'mm');
|
|
||||||
addAttr('Insulation diameter', diamInsKey, 'mm');
|
|
||||||
addAttr('Screen diameter', diamScreenKey, 'mm');
|
|
||||||
addAttr('Metallic screen', metalScreenKey, '');
|
|
||||||
addAttr('Max. pulling force', pullingForceKey, '');
|
|
||||||
addAttr('Electrical stress conductor', electricalStressKey, '');
|
|
||||||
addAttr('Electrical stress insulation', electricalStressKey, '');
|
|
||||||
addAttr('Reactance', reactanceKey, '');
|
|
||||||
addAttr('Heating time constant trefoil', heatingTrefoilKey, 's');
|
|
||||||
addAttr('Heating time constant flat', heatingFlatKey, 's');
|
|
||||||
addAttr('Flame retardant', flameKey, '');
|
|
||||||
addAttr('CPR class', cprKey, '');
|
|
||||||
addAttr('Packaging', packagingKey, '');
|
|
||||||
addAttr('Bending radius', bendKey, 'mm');
|
|
||||||
addAttr('Shape of conductor', shapeKey, '');
|
|
||||||
addAttr('Capacitance', capacitanceKey, 'μF/km');
|
|
||||||
addAttr('Current ratings in air, trefoil', currentAirTrefoilKey, 'A');
|
|
||||||
addAttr('Current ratings in air, flat', currentAirFlatKey, 'A');
|
|
||||||
addAttr('Current ratings in ground, trefoil', currentGroundTrefoilKey, 'A');
|
|
||||||
addAttr('Current ratings in ground, flat', currentGroundFlatKey, 'A');
|
|
||||||
addAttr('Conductor shortcircuit current', scCurrentCondKey, 'kA');
|
|
||||||
addAttr('Screen shortcircuit current', scCurrentScreenKey, 'kA');
|
|
||||||
|
|
||||||
return {
|
|
||||||
configurations,
|
|
||||||
attributes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get raw Excel rows for a product (for detailed inspection)
|
|
||||||
*/
|
|
||||||
export function getExcelRowsForProduct(params: ProductLookupParams): ExcelRow[] {
|
|
||||||
const match = findExcelForProduct(params);
|
|
||||||
return match?.rows || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the Excel index cache (useful for development)
|
|
||||||
*/
|
|
||||||
export function clearExcelCache(): void {
|
|
||||||
EXCEL_INDEX = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preload Excel data on module initialization
|
|
||||||
* This ensures the cache is built during build time
|
|
||||||
*/
|
|
||||||
export function preloadExcelData(): void {
|
|
||||||
getExcelIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload when imported
|
|
||||||
if (require.main === module) {
|
|
||||||
preloadExcelData();
|
|
||||||
}
|
|
||||||
1436
lib/pdf-brochure.tsx
1436
lib/pdf-brochure.tsx
File diff suppressed because it is too large
Load Diff
329
lib/pdf-page.tsx
329
lib/pdf-page.tsx
@@ -1,329 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
|
|
||||||
|
|
||||||
// Register fonts (using system fonts for now, can be customized)
|
|
||||||
Font.register({
|
|
||||||
family: 'Helvetica',
|
|
||||||
fonts: [
|
|
||||||
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
|
|
||||||
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
|
|
||||||
const C = {
|
|
||||||
navy: '#001a4d',
|
|
||||||
navyDeep: '#000d26',
|
|
||||||
green: '#4da612',
|
|
||||||
greenLight: '#e8f5d8',
|
|
||||||
white: '#FFFFFF',
|
|
||||||
offWhite: '#f8f9fa',
|
|
||||||
gray100: '#f3f4f6',
|
|
||||||
gray200: '#e5e7eb',
|
|
||||||
gray300: '#d1d5db',
|
|
||||||
gray400: '#9ca3af',
|
|
||||||
gray600: '#4b5563',
|
|
||||||
gray900: '#111827',
|
|
||||||
};
|
|
||||||
|
|
||||||
const MARGIN = 56;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
page: {
|
|
||||||
color: C.gray900,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
backgroundColor: C.white,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 80,
|
|
||||||
fontFamily: 'Helvetica',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Hero-style header
|
|
||||||
hero: {
|
|
||||||
backgroundColor: C.white,
|
|
||||||
paddingTop: 24,
|
|
||||||
paddingBottom: 0,
|
|
||||||
paddingHorizontal: MARGIN,
|
|
||||||
marginBottom: 20,
|
|
||||||
position: 'relative',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
|
|
||||||
logoText: {
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
letterSpacing: 2,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
|
|
||||||
docTitle: {
|
|
||||||
fontSize: 8,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.green,
|
|
||||||
letterSpacing: 2,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Content Area
|
|
||||||
content: {
|
|
||||||
paddingHorizontal: MARGIN,
|
|
||||||
},
|
|
||||||
|
|
||||||
pageTitle: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginBottom: 8,
|
|
||||||
marginTop: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: -0.5,
|
|
||||||
},
|
|
||||||
|
|
||||||
accentBar: {
|
|
||||||
width: 30,
|
|
||||||
height: 2,
|
|
||||||
backgroundColor: C.green,
|
|
||||||
marginBottom: 20,
|
|
||||||
borderRadius: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lexical Elements
|
|
||||||
paragraph: {
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.gray600,
|
|
||||||
lineHeight: 1.7,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
heading1: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginTop: 20,
|
|
||||||
marginBottom: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
heading2: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
heading3: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
marginBottom: 12,
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
listItemBullet: {
|
|
||||||
width: 12,
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.green,
|
|
||||||
fontWeight: 700,
|
|
||||||
},
|
|
||||||
listItemContent: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.gray600,
|
|
||||||
lineHeight: 1.7,
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
color: C.green,
|
|
||||||
textDecoration: 'none',
|
|
||||||
},
|
|
||||||
textBold: {
|
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'Helvetica-Bold',
|
|
||||||
color: C.navyDeep,
|
|
||||||
},
|
|
||||||
textItalic: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Footer — matches brochure style
|
|
||||||
footer: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 28,
|
|
||||||
left: MARGIN,
|
|
||||||
right: MARGIN,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: 12,
|
|
||||||
borderTopWidth: 2,
|
|
||||||
borderTopColor: C.green,
|
|
||||||
},
|
|
||||||
|
|
||||||
footerText: {
|
|
||||||
fontSize: 7,
|
|
||||||
color: C.gray400,
|
|
||||||
fontWeight: 400,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.8,
|
|
||||||
},
|
|
||||||
|
|
||||||
footerBrand: {
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Lexical to React-PDF Renderer ────────────────────────────────
|
|
||||||
|
|
||||||
const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
|
||||||
if (!node) return null;
|
|
||||||
|
|
||||||
switch (node.type) {
|
|
||||||
case 'text': {
|
|
||||||
const format = node.format || 0;
|
|
||||||
const isBold = (format & 1) !== 0;
|
|
||||||
const isItalic = (format & 2) !== 0;
|
|
||||||
|
|
||||||
let elementStyle: any = {};
|
|
||||||
if (isBold) elementStyle = { ...elementStyle, ...styles.textBold };
|
|
||||||
if (isItalic) elementStyle = { ...elementStyle, ...styles.textItalic };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text key={idx} style={elementStyle}>
|
|
||||||
{node.text}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'paragraph': {
|
|
||||||
return (
|
|
||||||
<Text key={idx} style={styles.paragraph}>
|
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'heading': {
|
|
||||||
let hStyle = styles.heading3;
|
|
||||||
if (node.tag === 'h1') hStyle = styles.heading1;
|
|
||||||
if (node.tag === 'h2') hStyle = styles.heading2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text key={idx} style={hStyle}>
|
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'list': {
|
|
||||||
return (
|
|
||||||
<View key={idx} style={styles.list}>
|
|
||||||
{node.children?.map((child: any, i: number) => {
|
|
||||||
if (child.type === 'listitem') {
|
|
||||||
return (
|
|
||||||
<View key={i} style={styles.listItem}>
|
|
||||||
<Text style={styles.listItemBullet}>
|
|
||||||
{node.listType === 'number' ? `${i + 1}.` : '•'}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.listItemContent}>
|
|
||||||
{child.children?.map((c: any, ci: number) => renderLexicalNode(c, ci))}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return renderLexicalNode(child, i);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'link': {
|
|
||||||
const href = node.fields?.url || node.url || '#';
|
|
||||||
return (
|
|
||||||
<Link key={idx} src={href} style={styles.link}>
|
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'linebreak': {
|
|
||||||
return <Text key={idx}>{'\n'}</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore payload blocks recursively to avoid crashing
|
|
||||||
case 'block':
|
|
||||||
return null;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (node.children) {
|
|
||||||
return (
|
|
||||||
<Text key={idx}>
|
|
||||||
{node.children.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PDFPageProps {
|
|
||||||
page: any;
|
|
||||||
locale?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
|
||||||
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Document>
|
|
||||||
<Page size="A4" style={styles.page}>
|
|
||||||
{/* Hero Header */}
|
|
||||||
<View style={styles.hero} fixed>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.logoText}>KLZ</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.pageTitle}>{page.title}</Text>
|
|
||||||
<View style={styles.accentBar} />
|
|
||||||
|
|
||||||
<View>
|
|
||||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
|
||||||
renderLexicalNode(node, i),
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Minimal footer */}
|
|
||||||
<View style={styles.footer} fixed>
|
|
||||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
|
||||||
<Text style={styles.footerText}>{dateStr}</Text>
|
|
||||||
</View>
|
|
||||||
</Page>
|
|
||||||
</Document>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -42,7 +42,7 @@ export const getLocalBusinessSchema = () => ({
|
|||||||
url: SITE_URL,
|
url: SITE_URL,
|
||||||
logo: LOGO_URL,
|
logo: LOGO_URL,
|
||||||
image: LOGO_URL,
|
image: LOGO_URL,
|
||||||
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
||||||
telephone: '+4935616857733',
|
telephone: '+4935616857733',
|
||||||
email: 'info@e-tib.com',
|
email: 'info@e-tib.com',
|
||||||
address: {
|
address: {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const standorteData: StandortData[] = [
|
|||||||
shortName: 'Guben (Hauptsitz)',
|
shortName: 'Guben (Hauptsitz)',
|
||||||
type: 'hq',
|
type: 'hq',
|
||||||
description: {
|
description: {
|
||||||
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungstiefbau und koordinieren unseren hochmodernen Maschinenpark.',
|
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungsnetzbau und koordinieren unseren hochmodernen Maschinenpark.',
|
||||||
en: 'Our headquarters in Guben forms the strategic and operational center of the E-TIB Group. From here, we manage large-scale civil engineering projects nationwide and coordinate our state-of-the-art machinery fleet.'
|
en: 'Our headquarters in Guben forms the strategic and operational center of the E-TIB Group. From here, we manage large-scale civil engineering projects nationwide and coordinate our state-of-the-art machinery fleet.'
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
@@ -56,13 +56,13 @@ export const standorteData: StandortData[] = [
|
|||||||
keyFeatures: {
|
keyFeatures: {
|
||||||
de: [
|
de: [
|
||||||
'Zentrale strategische Steuerung',
|
'Zentrale strategische Steuerung',
|
||||||
'Kabelleitungstiefbau & Infrastruktur',
|
'Kabelleitungsnetzbau & Infrastruktur',
|
||||||
'Koordination Großmaschinenpark',
|
'Koordination Großmaschinenpark',
|
||||||
'Zentrales Projektmanagement'
|
'Zentrales Projektmanagement'
|
||||||
],
|
],
|
||||||
en: [
|
en: [
|
||||||
'Central strategic management',
|
'Central strategic management',
|
||||||
'Underground cable engineering & infrastructure',
|
'Cable network engineering & infrastructure',
|
||||||
'Coordination of heavy machinery',
|
'Coordination of heavy machinery',
|
||||||
'Central project management'
|
'Central project management'
|
||||||
]
|
]
|
||||||
|
|||||||
31
make_contact_sheet.py
Normal file
31
make_contact_sheet.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
import math
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
folder = 'public/assets/photos'
|
||||||
|
files = [f for f in os.listdir(folder) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
thumb_w, thumb_h = 300, 300
|
||||||
|
cols = 8
|
||||||
|
rows = math.ceil(len(files) / cols)
|
||||||
|
|
||||||
|
contact_sheet = Image.new('RGB', (cols * thumb_w, rows * thumb_h), (255, 255, 255))
|
||||||
|
draw = ImageDraw.Draw(contact_sheet)
|
||||||
|
|
||||||
|
for i, f in enumerate(files):
|
||||||
|
path = os.path.join(folder, f)
|
||||||
|
try:
|
||||||
|
img = Image.open(path)
|
||||||
|
img.thumbnail((thumb_w, thumb_h - 40))
|
||||||
|
x = (i % cols) * thumb_w
|
||||||
|
y = (i // cols) * thumb_h
|
||||||
|
img_x = x + (thumb_w - img.width) // 2
|
||||||
|
img_y = y + (thumb_h - 40 - img.height) // 2
|
||||||
|
contact_sheet.paste(img, (img_x, img_y))
|
||||||
|
draw.text((x + 10, y + thumb_h - 30), f, fill=(0,0,0))
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
contact_sheet.save('contact_sheet.jpg')
|
||||||
|
print("Saved contact_sheet.jpg")
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
"categories": {}
|
"categories": {}
|
||||||
},
|
},
|
||||||
"Index": {
|
"Index": {
|
||||||
"title": "E-TIB GmbH – Experts in Underground Cable Engineering & Infrastructure",
|
"title": "E-TIB GmbH – Experts in Cable network engineering & Infrastructure",
|
||||||
"description": "Your partner for underground cable engineering, horizontal drilling, and fiber optic installation in Guben and nationwide.",
|
"description": "Your partner for Cable network engineering, horizontal drilling, and fiber optic installation in Guben and nationwide.",
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "E-TIB GmbH | Specialist for Underground Cable Engineering & Grid Expansion",
|
"title": "E-TIB GmbH | Specialist for Cable network engineering & Grid Expansion",
|
||||||
"description": "E-TIB GmbH from Guben is your expert for underground cable engineering, directional drilling, plowing works, and electrical installation up to 110 kV."
|
"description": "E-TIB GmbH from Guben is your expert for Cable network engineering, directional drilling, plowing works, and electrical installation up to 110 kV."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Navigation": {
|
"Navigation": {
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
"career": "Career",
|
"career": "Career",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"copyright": "Copyright © {year} E-TIB GmbH. All rights reserved.",
|
"copyright": "Copyright © {year} E-TIB GmbH. All rights reserved.",
|
||||||
"tagline": "Experts for underground cable engineering and the execution of electrical infrastructure projects. Quality and reliability from Guben."
|
"tagline": "Experts for Cable network engineering and the execution of electrical infrastructure projects. Quality and reliability from Guben."
|
||||||
},
|
},
|
||||||
"Team": {
|
"Team": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "About Us | The E-TIB Group",
|
"title": "About Us | The E-TIB Group",
|
||||||
"description": "Get to know the E-TIB Group: Specialists for underground cable engineering, drilling technology, and engineering services."
|
"description": "Get to know the E-TIB Group: Specialists for Cable network engineering, drilling technology, and engineering services."
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"title": "Infrastructure for the Future",
|
"title": "Infrastructure for the Future",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"subtitle": "Specialized units for complex projects",
|
"subtitle": "Specialized units for complex projects",
|
||||||
"etib": {
|
"etib": {
|
||||||
"title": "E‑TIB GmbH",
|
"title": "E‑TIB GmbH",
|
||||||
"desc": "Execution of electrical infrastructure projects and underground cable engineering."
|
"desc": "Execution of electrical infrastructure projects and Cable network engineering."
|
||||||
},
|
},
|
||||||
"bohrtechnik": {
|
"bohrtechnik": {
|
||||||
"title": "E‑TIB Bohrtechnik GmbH",
|
"title": "E‑TIB Bohrtechnik GmbH",
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
"Contact": {
|
"Contact": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Contact | E-TIB GmbH Guben",
|
"title": "Contact | E-TIB GmbH Guben",
|
||||||
"description": "Get in touch with E-TIB GmbH in Guben. Your partner for underground cable engineering and grid infrastructure."
|
"description": "Get in touch with E-TIB GmbH in Guben. Your partner for Cable network engineering and grid infrastructure."
|
||||||
},
|
},
|
||||||
"title": "Contact Us",
|
"title": "Contact Us",
|
||||||
"subtitle": "Do you have questions about our services or a specific project? We are here for you.",
|
"subtitle": "Do you have questions about our services or a specific project? We are here for you.",
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
"subtitle": "Comprehensive solutions for the expansion of modern energy and data networks.",
|
"subtitle": "Comprehensive solutions for the expansion of modern energy and data networks.",
|
||||||
"badge": "Service Spectrum",
|
"badge": "Service Spectrum",
|
||||||
"items": {
|
"items": {
|
||||||
"kabelbau": "Underground Cable Engineering",
|
"kabelbau": "Cable network engineering",
|
||||||
"pflugarbeiten": "Cable Plowing",
|
"pflugarbeiten": "Cable Plowing",
|
||||||
"bohrungen": "Horizontal Directional Drilling",
|
"bohrungen": "Horizontal Directional Drilling",
|
||||||
"elektromontage": "Electrical Installations up to 110 kV",
|
"elektromontage": "Electrical Installations up to 110 kV",
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
},
|
},
|
||||||
"Home": {
|
"Home": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"title": "THE EXPERTS FOR <green>UNDERGROUND CABLE ENGINEERING</green>",
|
"title": "THE EXPERTS FOR <green>Cable network engineering</green>",
|
||||||
"subtitle": "We help expanding the energy cable networks for a green future.",
|
"subtitle": "We help expanding the energy cable networks for a green future.",
|
||||||
"cta": "Request now",
|
"cta": "Request now",
|
||||||
"searchPlaceholder": "Describe project or search for cables...",
|
"searchPlaceholder": "Describe project or search for cables...",
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
"Blog": {
|
"Blog": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Blog & News",
|
"title": "Blog & News",
|
||||||
"description": "Latest news and insights from the world of underground cable engineering and infrastructure."
|
"description": "Latest news and insights from the world of Cable network engineering and infrastructure."
|
||||||
},
|
},
|
||||||
"featuredPost": "Featured Post",
|
"featuredPost": "Featured Post",
|
||||||
"readFullArticle": "Read Full Article",
|
"readFullArticle": "Read Full Article",
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
"ReferencesSlider": {
|
"ReferencesSlider": {
|
||||||
"badge": "Selected Projects",
|
"badge": "Selected Projects",
|
||||||
"title": "References & Successes",
|
"title": "References & Successes",
|
||||||
"description": "A selection of our successfully completed projects in underground cable engineering, drilling technology, and grid infrastructure.",
|
"description": "A selection of our successfully completed projects in Cable network engineering, drilling technology, and grid infrastructure.",
|
||||||
"ctaLabel": "View All References"
|
"ctaLabel": "View All References"
|
||||||
},
|
},
|
||||||
"CompanyTimeline": {
|
"CompanyTimeline": {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"@mintel/next-utils": "1.9.5",
|
"@mintel/next-utils": "1.9.5",
|
||||||
"@next/mdx": "^16.1.6",
|
"@next/mdx": "^16.1.6",
|
||||||
"@react-email/components": "1.0.8",
|
"@react-email/components": "1.0.8",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.5.0",
|
||||||
"@sentry/nextjs": "^10.39.0",
|
"@sentry/nextjs": "^10.39.0",
|
||||||
@@ -31,7 +30,6 @@
|
|||||||
"next-intl": "^4.8.2",
|
"next-intl": "^4.8.2",
|
||||||
"next-mdx-remote": "^5.0.0",
|
"next-mdx-remote": "^5.0.0",
|
||||||
"nodemailer": "^8.0.7",
|
"nodemailer": "^8.0.7",
|
||||||
"pdf-lib": "^1.17.1",
|
|
||||||
"pino": "^10.3.0",
|
"pino": "^10.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -124,10 +122,6 @@
|
|||||||
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
||||||
"check:forms": "tsx ./scripts/check-forms.ts",
|
"check:forms": "tsx ./scripts/check-forms.ts",
|
||||||
"check:apis": "tsx ./scripts/check-apis.ts",
|
"check:apis": "tsx ./scripts/check-apis.ts",
|
||||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.tsx",
|
|
||||||
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
|
||||||
"excel:datasheets": "tsx ./scripts/generate-excel-datasheets.ts",
|
|
||||||
"brochure:generate": "tsx ./scripts/generate-brochure.ts",
|
|
||||||
"assets:push:staging": "bash ./scripts/assets-sync.sh local staging",
|
"assets:push:staging": "bash ./scripts/assets-sync.sh local staging",
|
||||||
"assets:push:prod": "bash ./scripts/assets-sync.sh local prod",
|
"assets:push:prod": "bash ./scripts/assets-sync.sh local prod",
|
||||||
"assets:pull:testing": "bash ./scripts/assets-sync.sh testing local",
|
"assets:pull:testing": "bash ./scripts/assets-sync.sh testing local",
|
||||||
|
|||||||
306
pnpm-lock.yaml
generated
306
pnpm-lock.yaml
generated
@@ -33,9 +33,6 @@ importers:
|
|||||||
'@react-email/components':
|
'@react-email/components':
|
||||||
specifier: 1.0.8
|
specifier: 1.0.8
|
||||||
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@react-pdf/renderer':
|
|
||||||
specifier: ^4.3.2
|
|
||||||
version: 4.3.2(react@19.2.4)
|
|
||||||
'@react-three/drei':
|
'@react-three/drei':
|
||||||
specifier: ^10.7.7
|
specifier: ^10.7.7
|
||||||
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
|
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
|
||||||
@@ -93,9 +90,6 @@ importers:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^8.0.7
|
specifier: ^8.0.7
|
||||||
version: 8.0.7
|
version: 8.0.7
|
||||||
pdf-lib:
|
|
||||||
specifier: ^1.17.1
|
|
||||||
version: 1.17.1
|
|
||||||
pino:
|
pino:
|
||||||
specifier: ^10.3.0
|
specifier: ^10.3.0
|
||||||
version: 10.3.1
|
version: 10.3.1
|
||||||
@@ -1897,12 +1891,6 @@ packages:
|
|||||||
'@paulirish/trace_engine@0.0.53':
|
'@paulirish/trace_engine@0.0.53':
|
||||||
resolution: {integrity: sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==}
|
resolution: {integrity: sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==}
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
|
||||||
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
|
|
||||||
|
|
||||||
'@pdf-lib/upng@1.0.1':
|
|
||||||
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
|
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0':
|
'@pinojs/redact@0.4.0':
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||||
|
|
||||||
@@ -2221,49 +2209,6 @@ packages:
|
|||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
|
|
||||||
'@react-pdf/fns@3.1.2':
|
|
||||||
resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==}
|
|
||||||
|
|
||||||
'@react-pdf/font@4.0.4':
|
|
||||||
resolution: {integrity: sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==}
|
|
||||||
|
|
||||||
'@react-pdf/image@3.0.4':
|
|
||||||
resolution: {integrity: sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==}
|
|
||||||
|
|
||||||
'@react-pdf/layout@4.4.2':
|
|
||||||
resolution: {integrity: sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==}
|
|
||||||
|
|
||||||
'@react-pdf/pdfkit@4.1.0':
|
|
||||||
resolution: {integrity: sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==}
|
|
||||||
|
|
||||||
'@react-pdf/png-js@3.0.0':
|
|
||||||
resolution: {integrity: sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==}
|
|
||||||
|
|
||||||
'@react-pdf/primitives@4.1.1':
|
|
||||||
resolution: {integrity: sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==}
|
|
||||||
|
|
||||||
'@react-pdf/reconciler@2.0.0':
|
|
||||||
resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
'@react-pdf/render@4.3.2':
|
|
||||||
resolution: {integrity: sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==}
|
|
||||||
|
|
||||||
'@react-pdf/renderer@4.3.2':
|
|
||||||
resolution: {integrity: sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
'@react-pdf/stylesheet@6.1.2':
|
|
||||||
resolution: {integrity: sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==}
|
|
||||||
|
|
||||||
'@react-pdf/textkit@6.1.0':
|
|
||||||
resolution: {integrity: sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==}
|
|
||||||
|
|
||||||
'@react-pdf/types@2.9.2':
|
|
||||||
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
|
|
||||||
|
|
||||||
'@react-three/drei@10.7.7':
|
'@react-three/drei@10.7.7':
|
||||||
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3438,9 +3383,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
abs-svg-path@0.1.1:
|
|
||||||
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3776,9 +3718,6 @@ packages:
|
|||||||
brotli@1.3.3:
|
brotli@1.3.3:
|
||||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
||||||
|
|
||||||
browserify-zlib@0.2.0:
|
|
||||||
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
|
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -3973,9 +3912,6 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
color-string@1.9.1:
|
|
||||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
|
||||||
|
|
||||||
colorette@2.0.20:
|
colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
|
|
||||||
@@ -4112,9 +4048,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
crypto-js@4.2.0:
|
|
||||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
|
||||||
|
|
||||||
crypto-random-string@2.0.0:
|
crypto-random-string@2.0.0:
|
||||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4430,9 +4363,6 @@ packages:
|
|||||||
electron-to-chromium@1.5.330:
|
electron-to-chromium@1.5.330:
|
||||||
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
||||||
|
|
||||||
emoji-regex-xs@1.0.0:
|
|
||||||
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
|
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
|
|
||||||
@@ -5170,12 +5100,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
hsl-to-hex@1.0.0:
|
|
||||||
resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==}
|
|
||||||
|
|
||||||
hsl-to-rgb-for-reals@1.1.1:
|
|
||||||
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
|
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0:
|
html-encoding-sniffer@6.0.0:
|
||||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
@@ -5257,9 +5181,6 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
hyphen@1.14.1:
|
|
||||||
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
|
|
||||||
|
|
||||||
i18next-fs-backend@2.6.3:
|
i18next-fs-backend@2.6.3:
|
||||||
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
||||||
|
|
||||||
@@ -5393,9 +5314,6 @@ packages:
|
|||||||
is-arrayish@0.2.1:
|
is-arrayish@0.2.1:
|
||||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
|
|
||||||
is-arrayish@0.3.4:
|
|
||||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
|
||||||
|
|
||||||
is-async-function@2.1.1:
|
is-async-function@2.1.1:
|
||||||
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
|
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5554,9 +5472,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
is-url@1.2.4:
|
|
||||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
|
||||||
|
|
||||||
is-weakmap@2.0.2:
|
is-weakmap@2.0.2:
|
||||||
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5598,9 +5513,6 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jay-peg@1.1.1:
|
|
||||||
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
|
|
||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
@@ -6034,9 +5946,6 @@ packages:
|
|||||||
mdn-data@2.27.1:
|
mdn-data@2.27.1:
|
||||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
media-engine@1.0.3:
|
|
||||||
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
|
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -6383,9 +6292,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
normalize-svg-path@1.1.0:
|
|
||||||
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
|
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6532,9 +6438,6 @@ packages:
|
|||||||
pako@0.2.9:
|
pako@0.2.9:
|
||||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||||
|
|
||||||
pako@1.0.11:
|
|
||||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -6553,9 +6456,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
parse-svg-path@0.1.2:
|
|
||||||
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
|
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||||
|
|
||||||
@@ -6607,9 +6507,6 @@ packages:
|
|||||||
pause-stream@0.0.11:
|
pause-stream@0.0.11:
|
||||||
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
||||||
|
|
||||||
pdf-lib@1.17.1:
|
|
||||||
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
|
|
||||||
|
|
||||||
pdf-parse@2.4.5:
|
pdf-parse@2.4.5:
|
||||||
resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
|
resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
|
||||||
engines: {node: '>=20.16.0 <21 || >=22.3.0'}
|
engines: {node: '>=20.16.0 <21 || >=22.3.0'}
|
||||||
@@ -6826,9 +6723,6 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
queue@6.0.2:
|
|
||||||
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
quick-format-unescaped@4.0.4:
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
@@ -7122,9 +7016,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
engines: {node: '>=v12.22.7'}
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
scheduler@0.25.0-rc-603e6108-20241029:
|
|
||||||
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
|
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -7222,9 +7113,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
|
||||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
|
||||||
|
|
||||||
sirv@2.0.4:
|
sirv@2.0.4:
|
||||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -7401,9 +7289,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
@@ -7489,9 +7374,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17.0'
|
react: '>=17.0'
|
||||||
|
|
||||||
svg-arc-to-cubic-bezier@3.2.0:
|
|
||||||
resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==}
|
|
||||||
|
|
||||||
svg-to-pdfkit@0.1.8:
|
svg-to-pdfkit@0.1.8:
|
||||||
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
||||||
|
|
||||||
@@ -7875,10 +7757,6 @@ packages:
|
|||||||
victory-vendor@37.3.6:
|
victory-vendor@37.3.6:
|
||||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||||
|
|
||||||
vite-compatible-readable-stream@3.6.1:
|
|
||||||
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
vite@8.0.3:
|
vite@8.0.3:
|
||||||
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -8217,9 +8095,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
yoga-layout@3.2.1:
|
|
||||||
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
|
||||||
|
|
||||||
zod-validation-error@4.0.2:
|
zod-validation-error@4.0.2:
|
||||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -9863,14 +9738,6 @@ snapshots:
|
|||||||
legacy-javascript: 0.0.1
|
legacy-javascript: 0.0.1
|
||||||
third-party-web: 0.29.2
|
third-party-web: 0.29.2
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
|
||||||
dependencies:
|
|
||||||
pako: 1.0.11
|
|
||||||
|
|
||||||
'@pdf-lib/upng@1.0.1':
|
|
||||||
dependencies:
|
|
||||||
pako: 1.0.11
|
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
@@ -10150,107 +10017,6 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
'@react-pdf/fns@3.1.2': {}
|
|
||||||
|
|
||||||
'@react-pdf/font@4.0.4':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/pdfkit': 4.1.0
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
fontkit: 2.0.4
|
|
||||||
is-url: 1.2.4
|
|
||||||
|
|
||||||
'@react-pdf/image@3.0.4':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/png-js': 3.0.0
|
|
||||||
jay-peg: 1.1.1
|
|
||||||
|
|
||||||
'@react-pdf/layout@4.4.2':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/image': 3.0.4
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/stylesheet': 6.1.2
|
|
||||||
'@react-pdf/textkit': 6.1.0
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
emoji-regex-xs: 1.0.0
|
|
||||||
queue: 6.0.2
|
|
||||||
yoga-layout: 3.2.1
|
|
||||||
|
|
||||||
'@react-pdf/pdfkit@4.1.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
'@react-pdf/png-js': 3.0.0
|
|
||||||
browserify-zlib: 0.2.0
|
|
||||||
crypto-js: 4.2.0
|
|
||||||
fontkit: 2.0.4
|
|
||||||
jay-peg: 1.1.1
|
|
||||||
linebreak: 1.1.0
|
|
||||||
vite-compatible-readable-stream: 3.6.1
|
|
||||||
|
|
||||||
'@react-pdf/png-js@3.0.0':
|
|
||||||
dependencies:
|
|
||||||
browserify-zlib: 0.2.0
|
|
||||||
|
|
||||||
'@react-pdf/primitives@4.1.1': {}
|
|
||||||
|
|
||||||
'@react-pdf/reconciler@2.0.0(react@19.2.4)':
|
|
||||||
dependencies:
|
|
||||||
object-assign: 4.1.1
|
|
||||||
react: 19.2.4
|
|
||||||
scheduler: 0.25.0-rc-603e6108-20241029
|
|
||||||
|
|
||||||
'@react-pdf/render@4.3.2':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/textkit': 6.1.0
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
abs-svg-path: 0.1.1
|
|
||||||
color-string: 1.9.1
|
|
||||||
normalize-svg-path: 1.1.0
|
|
||||||
parse-svg-path: 0.1.2
|
|
||||||
svg-arc-to-cubic-bezier: 3.2.0
|
|
||||||
|
|
||||||
'@react-pdf/renderer@4.3.2(react@19.2.4)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/font': 4.0.4
|
|
||||||
'@react-pdf/layout': 4.4.2
|
|
||||||
'@react-pdf/pdfkit': 4.1.0
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/reconciler': 2.0.0(react@19.2.4)
|
|
||||||
'@react-pdf/render': 4.3.2
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
events: 3.3.0
|
|
||||||
object-assign: 4.1.1
|
|
||||||
prop-types: 15.8.1
|
|
||||||
queue: 6.0.2
|
|
||||||
react: 19.2.4
|
|
||||||
|
|
||||||
'@react-pdf/stylesheet@6.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
color-string: 1.9.1
|
|
||||||
hsl-to-hex: 1.0.0
|
|
||||||
media-engine: 1.0.3
|
|
||||||
postcss-value-parser: 4.2.0
|
|
||||||
|
|
||||||
'@react-pdf/textkit@6.1.0':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
bidi-js: 1.0.3
|
|
||||||
hyphen: 1.14.1
|
|
||||||
unicode-properties: 1.4.1
|
|
||||||
|
|
||||||
'@react-pdf/types@2.9.2':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/font': 4.0.4
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/stylesheet': 6.1.2
|
|
||||||
|
|
||||||
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
|
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.2
|
'@babel/runtime': 7.29.2
|
||||||
@@ -11429,8 +11195,6 @@ snapshots:
|
|||||||
|
|
||||||
abbrev@2.0.0: {}
|
abbrev@2.0.0: {}
|
||||||
|
|
||||||
abs-svg-path@0.1.1: {}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -11751,10 +11515,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
|
|
||||||
browserify-zlib@0.2.0:
|
|
||||||
dependencies:
|
|
||||||
pako: 1.0.11
|
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.13
|
baseline-browser-mapping: 2.10.13
|
||||||
@@ -11969,11 +11729,6 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
color-string@1.9.1:
|
|
||||||
dependencies:
|
|
||||||
color-name: 1.1.4
|
|
||||||
simple-swizzle: 0.2.4
|
|
||||||
|
|
||||||
colorette@2.0.20: {}
|
colorette@2.0.20: {}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
@@ -12120,8 +11875,6 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
crypto-js@4.2.0: {}
|
|
||||||
|
|
||||||
crypto-random-string@2.0.0: {}
|
crypto-random-string@2.0.0: {}
|
||||||
|
|
||||||
csp_evaluator@1.1.5: {}
|
csp_evaluator@1.1.5: {}
|
||||||
@@ -12453,8 +12206,6 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.330: {}
|
electron-to-chromium@1.5.330: {}
|
||||||
|
|
||||||
emoji-regex-xs@1.0.0: {}
|
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@@ -13516,12 +13267,6 @@ snapshots:
|
|||||||
|
|
||||||
hoopy@0.1.4: {}
|
hoopy@0.1.4: {}
|
||||||
|
|
||||||
hsl-to-hex@1.0.0:
|
|
||||||
dependencies:
|
|
||||||
hsl-to-rgb-for-reals: 1.1.1
|
|
||||||
|
|
||||||
hsl-to-rgb-for-reals@1.1.1: {}
|
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||||
@@ -13620,8 +13365,6 @@ snapshots:
|
|||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
hyphen@1.14.1: {}
|
|
||||||
|
|
||||||
i18next-fs-backend@2.6.3: {}
|
i18next-fs-backend@2.6.3: {}
|
||||||
|
|
||||||
i18next@25.10.10(typescript@5.9.3):
|
i18next@25.10.10(typescript@5.9.3):
|
||||||
@@ -13773,8 +13516,6 @@ snapshots:
|
|||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
|
|
||||||
is-arrayish@0.3.4: {}
|
|
||||||
|
|
||||||
is-async-function@2.1.1:
|
is-async-function@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
async-function: 1.0.0
|
async-function: 1.0.0
|
||||||
@@ -13912,8 +13653,6 @@ snapshots:
|
|||||||
|
|
||||||
is-unicode-supported@2.1.0: {}
|
is-unicode-supported@2.1.0: {}
|
||||||
|
|
||||||
is-url@1.2.4: {}
|
|
||||||
|
|
||||||
is-weakmap@2.0.2: {}
|
is-weakmap@2.0.2: {}
|
||||||
|
|
||||||
is-weakref@1.1.1:
|
is-weakref@1.1.1:
|
||||||
@@ -13964,10 +13703,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jay-peg@1.1.1:
|
|
||||||
dependencies:
|
|
||||||
restructure: 3.0.2
|
|
||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.15
|
||||||
@@ -14516,8 +14251,6 @@ snapshots:
|
|||||||
|
|
||||||
mdn-data@2.27.1: {}
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
media-engine@1.0.3: {}
|
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
meow@13.2.0: {}
|
meow@13.2.0: {}
|
||||||
@@ -14984,10 +14717,6 @@ snapshots:
|
|||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-svg-path@1.1.0:
|
|
||||||
dependencies:
|
|
||||||
svg-arc-to-cubic-bezier: 3.2.0
|
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -15198,8 +14927,6 @@ snapshots:
|
|||||||
|
|
||||||
pako@0.2.9: {}
|
pako@0.2.9: {}
|
||||||
|
|
||||||
pako@1.0.11: {}
|
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -15227,8 +14954,6 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
parse-svg-path@0.1.2: {}
|
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
@@ -15279,13 +15004,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
|
|
||||||
pdf-lib@1.17.1:
|
|
||||||
dependencies:
|
|
||||||
'@pdf-lib/standard-fonts': 1.0.0
|
|
||||||
'@pdf-lib/upng': 1.0.1
|
|
||||||
pako: 1.0.11
|
|
||||||
tslib: 1.14.1
|
|
||||||
|
|
||||||
pdf-parse@2.4.5:
|
pdf-parse@2.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/canvas': 0.1.80
|
'@napi-rs/canvas': 0.1.80
|
||||||
@@ -15535,10 +15253,6 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
queue@6.0.2:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
@@ -15957,8 +15671,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
scheduler@0.25.0-rc-603e6108-20241029: {}
|
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
schema-dts@1.1.5: {}
|
schema-dts@1.1.5: {}
|
||||||
@@ -16111,10 +15823,6 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
|
||||||
dependencies:
|
|
||||||
is-arrayish: 0.3.4
|
|
||||||
|
|
||||||
sirv@2.0.4:
|
sirv@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@polka/url': 1.0.0-next.29
|
'@polka/url': 1.0.0-next.29
|
||||||
@@ -16351,10 +16059,6 @@ snapshots:
|
|||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
@@ -16425,8 +16129,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
svg-arc-to-cubic-bezier@3.2.0: {}
|
|
||||||
|
|
||||||
svg-to-pdfkit@0.1.8:
|
svg-to-pdfkit@0.1.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
pdfkit: 0.18.0
|
pdfkit: 0.18.0
|
||||||
@@ -16895,12 +16597,6 @@ snapshots:
|
|||||||
d3-time: 3.1.0
|
d3-time: 3.1.0
|
||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
vite-compatible-readable-stream@3.6.1:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
string_decoder: 1.3.0
|
|
||||||
util-deprecate: 1.0.2
|
|
||||||
|
|
||||||
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
@@ -17232,8 +16928,6 @@ snapshots:
|
|||||||
|
|
||||||
yoctocolors@2.1.2: {}
|
yoctocolors@2.1.2: {}
|
||||||
|
|
||||||
yoga-layout@3.2.1: {}
|
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@3.25.76):
|
zod-validation-error@4.0.2(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env ts-node
|
|
||||||
/**
|
|
||||||
* Master Brochure Generator
|
|
||||||
*
|
|
||||||
* Generates a multi-page PDF brochure containing all products.
|
|
||||||
* Combines Payload CMS marketing text with Excel technical data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as React from 'react';
|
|
||||||
import { renderToBuffer } from '@react-pdf/renderer';
|
|
||||||
import { getAllProducts } from '../lib/products';
|
|
||||||
import { getExcelTechnicalDataForProduct, preloadExcelData } from '../lib/excel-products';
|
|
||||||
import { PDFBrochure } from '../lib/pdf-brochure';
|
|
||||||
import { ProductData as PDFProductData } from '../lib/pdf-datasheet';
|
|
||||||
|
|
||||||
const CONFIG = {
|
|
||||||
outputDir: path.join(process.cwd(), 'public/downloads'),
|
|
||||||
locales: ['en', 'de'] as const,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
async function ensureOutputDir() {
|
|
||||||
if (!fs.existsSync(CONFIG.outputDir)) {
|
|
||||||
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateBrochure(locale: 'en' | 'de') {
|
|
||||||
console.log(`\nGenerating brochure for locale: ${locale.toUpperCase()}...`);
|
|
||||||
|
|
||||||
// 1. Load data
|
|
||||||
const products = await getAllProducts(locale);
|
|
||||||
console.log(`- Loaded ${products.length} products from Payload`);
|
|
||||||
|
|
||||||
// 2. Map and Enrich
|
|
||||||
const pdfProducts: PDFProductData[] = products.map((p, index) => {
|
|
||||||
const excelData = getExcelTechnicalDataForProduct({
|
|
||||||
slug: p.slug,
|
|
||||||
sku: p.frontmatter.sku,
|
|
||||||
name: p.frontmatter.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: index + 1,
|
|
||||||
name: p.frontmatter.title,
|
|
||||||
sku: p.frontmatter.sku,
|
|
||||||
shortDescriptionHtml: p.frontmatter.description,
|
|
||||||
descriptionHtml: p.frontmatter.description,
|
|
||||||
applicationHtml: '',
|
|
||||||
featuredImage: p.frontmatter.images[0] || null,
|
|
||||||
images: p.frontmatter.images,
|
|
||||||
categories: p.frontmatter.categories.map(name => ({ name })),
|
|
||||||
attributes: excelData?.attributes || [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pdfProducts.length === 0) {
|
|
||||||
console.warn(`! No products found for ${locale}. Skipping.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Render
|
|
||||||
console.log(`- Rendering PDF with ${pdfProducts.length} pages...`);
|
|
||||||
const buffer = await renderToBuffer(
|
|
||||||
React.createElement(PDFBrochure, {
|
|
||||||
products: pdfProducts,
|
|
||||||
locale,
|
|
||||||
title: locale === 'de' ? 'Produktkatalog' : 'Product Catalog',
|
|
||||||
subtitle: '2026',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Save
|
|
||||||
const fileName = `KLZ_Cables_Brochure_2026_${locale.toUpperCase()}.pdf`;
|
|
||||||
const filePath = path.join(CONFIG.outputDir, fileName);
|
|
||||||
fs.writeFileSync(filePath, buffer);
|
|
||||||
|
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
console.log(`✓ Generated: ${fileName} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const start = Date.now();
|
|
||||||
console.log('--- KLZ Brochure Generator ---');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ensureOutputDir();
|
|
||||||
preloadExcelData();
|
|
||||||
|
|
||||||
for (const locale of CONFIG.locales) {
|
|
||||||
await generateBrochure(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n✅ Brochure generation completed in ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ Fatal error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,21 +7,21 @@ describe('Task 6 Content Updates', () => {
|
|||||||
const enHomePath = path.join(process.cwd(), 'content', 'en', 'home.mdx');
|
const enHomePath = path.join(process.cwd(), 'content', 'en', 'home.mdx');
|
||||||
const deKompetenzenPath = path.join(process.cwd(), 'content', 'de', 'kompetenzen.mdx');
|
const deKompetenzenPath = path.join(process.cwd(), 'content', 'de', 'kompetenzen.mdx');
|
||||||
const enKompetenzenPath = path.join(process.cwd(), 'content', 'en', 'kompetenzen.mdx');
|
const enKompetenzenPath = path.join(process.cwd(), 'content', 'en', 'kompetenzen.mdx');
|
||||||
const deKabeltiefbauPath = path.join(process.cwd(), 'content', 'de', 'kabeltiefbau.mdx');
|
const deKabelnetzbauPath = path.join(process.cwd(), 'content', 'de', 'kabeltiefbau.mdx');
|
||||||
const enKabeltiefbauPath = path.join(process.cwd(), 'content', 'en', 'kabeltiefbau.mdx');
|
const enKabelnetzbauPath = path.join(process.cwd(), 'content', 'en', 'kabeltiefbau.mdx');
|
||||||
const deUeberUnsPath = path.join(process.cwd(), 'content', 'de', 'ueber-uns.mdx');
|
const deUeberUnsPath = path.join(process.cwd(), 'content', 'de', 'ueber-uns.mdx');
|
||||||
const enUeberUnsPath = path.join(process.cwd(), 'content', 'en', 'ueber-uns.mdx');
|
const enUeberUnsPath = path.join(process.cwd(), 'content', 'en', 'ueber-uns.mdx');
|
||||||
|
|
||||||
it('should verify Kabelleitungstiefbau changes in DE and EN kompetenzen.mdx', () => {
|
it('should verify Kabelleitungsnetzbau changes in DE and EN kompetenzen.mdx', () => {
|
||||||
const deContent = fs.readFileSync(deKompetenzenPath, 'utf8');
|
const deContent = fs.readFileSync(deKompetenzenPath, 'utf8');
|
||||||
const enContent = fs.readFileSync(enKompetenzenPath, 'utf8');
|
const enContent = fs.readFileSync(enKompetenzenPath, 'utf8');
|
||||||
|
|
||||||
// DE Kompetenzen
|
// DE Kompetenzen
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
expect(deContent).toContain('description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)"');
|
||||||
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
||||||
expect(deContent).toContain('"name": "Kabelleitungstiefbau"');
|
expect(deContent).toContain('"name": "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
expect(deContent).toContain('"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
||||||
|
|
||||||
// EN Competencies
|
// EN Competencies
|
||||||
@@ -33,13 +33,13 @@ describe('Task 6 Content Updates', () => {
|
|||||||
expect(enContent).toContain('"description": "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV."');
|
expect(enContent).toContain('"description": "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV."');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should verify Kabelleitungstiefbau changes in DE and EN home.mdx', () => {
|
it('should verify Kabelleitungsnetzbau changes in DE and EN home.mdx', () => {
|
||||||
const deContent = fs.readFileSync(deHomePath, 'utf8');
|
const deContent = fs.readFileSync(deHomePath, 'utf8');
|
||||||
const enContent = fs.readFileSync(enHomePath, 'utf8');
|
const enContent = fs.readFileSync(enHomePath, 'utf8');
|
||||||
|
|
||||||
// DE Home
|
// DE Home
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
||||||
expect(deContent).toContain('100+');
|
expect(deContent).toContain('100+');
|
||||||
expect(deContent).toContain('Mitarbeitende');
|
expect(deContent).toContain('Mitarbeitende');
|
||||||
|
|
||||||
@@ -50,18 +50,18 @@ describe('Task 6 Content Updates', () => {
|
|||||||
expect(enContent).toContain('Employees');
|
expect(enContent).toContain('Employees');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should verify Kabelleitungstiefbau changes in DE and EN kabeltiefbau.mdx', () => {
|
it('should verify Kabelleitungsnetzbau changes in DE and EN kabeltiefbau.mdx', () => {
|
||||||
const deContent = fs.readFileSync(deKabeltiefbauPath, 'utf8');
|
const deContent = fs.readFileSync(deKabelnetzbauPath, 'utf8');
|
||||||
const enContent = fs.readFileSync(enKabeltiefbauPath, 'utf8');
|
const enContent = fs.readFileSync(enKabelnetzbauPath, 'utf8');
|
||||||
|
|
||||||
// DE Kabeltiefbau
|
// DE Kabelnetzbau
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungstiefbau');
|
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
||||||
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
||||||
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
|
||||||
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
||||||
|
|
||||||
// EN Kabeltiefbau
|
// EN Kabelnetzbau
|
||||||
expect(enContent).toContain('title: "Cable Civil Engineering"');
|
expect(enContent).toContain('title: "Cable Civil Engineering"');
|
||||||
expect(enContent).toContain('excerpt: "Professional cable civil engineering');
|
expect(enContent).toContain('excerpt: "Professional cable civil engineering');
|
||||||
expect(enContent).toContain('cable assembly up to 110 kV');
|
expect(enContent).toContain('cable assembly up to 110 kV');
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ describe('E2E Navigation & Content (MDX Architecture)', () => {
|
|||||||
it('should verify blog listing and post loading', async () => {
|
it('should verify blog listing and post loading', async () => {
|
||||||
await page.goto(`${BASE_URL}/de/blog`, { waitUntil: 'networkidle2', timeout: 60000 });
|
await page.goto(`${BASE_URL}/de/blog`, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||||
let bodyText = await page.evaluate(() => document.body.innerText);
|
let bodyText = await page.evaluate(() => document.body.innerText);
|
||||||
expect(bodyText).toContain('Moderne Verfahren im Kabeltiefbau');
|
expect(bodyText).toContain('Moderne Verfahren im Kabelnetzbau');
|
||||||
|
|
||||||
// Click on the first article (if found) or navigate directly
|
// Click on the first article (if found) or navigate directly
|
||||||
await page.goto(`${BASE_URL}/de/blog/moderne-verfahren-kabeltiefbau`, { waitUntil: 'networkidle2', timeout: 60000 });
|
await page.goto(`${BASE_URL}/de/blog/moderne-verfahren-kabeltiefbau`, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||||
|
|||||||
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
63
venv/bin/activate
Normal file
63
venv/bin/activate
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV="/Volumes/Alpha SSD/Coding/e-tib.com/venv"
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1="(venv) ${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT="(venv) "
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
26
venv/bin/activate.csh
Normal file
26
venv/bin/activate.csh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV "/Volumes/Alpha SSD/Coding/e-tib.com/venv"
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = "(venv) $prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT "(venv) "
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
69
venv/bin/activate.fish
Normal file
69
venv/bin/activate.fish
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/); you cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV "/Volumes/Alpha SSD/Coding/e-tib.com/venv"
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT "(venv) "
|
||||||
|
end
|
||||||
10
venv/bin/pip
Executable file
10
venv/bin/pip
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
'''exec' "/Volumes/Alpha SSD/Coding/e-tib.com/venv/bin/python3" "$0" "$@"
|
||||||
|
' '''
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
10
venv/bin/pip3
Executable file
10
venv/bin/pip3
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
'''exec' "/Volumes/Alpha SSD/Coding/e-tib.com/venv/bin/python3" "$0" "$@"
|
||||||
|
' '''
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
10
venv/bin/pip3.11
Executable file
10
venv/bin/pip3.11
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
'''exec' "/Volumes/Alpha SSD/Coding/e-tib.com/venv/bin/python3" "$0" "$@"
|
||||||
|
' '''
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
1
venv/bin/python
Symbolic link
1
venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/Users/marcmintel/.pyenv/versions/3.11.9/bin/python3
|
||||||
1
venv/bin/python3.11
Symbolic link
1
venv/bin/python3.11
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libXau.6.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libXau.6.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libavif.16.4.1.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libavif.16.4.1.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libbrotlicommon.1.2.0.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libbrotlicommon.1.2.0.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libbrotlidec.1.2.0.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libbrotlidec.1.2.0.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libfreetype.6.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libfreetype.6.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libharfbuzz.0.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libharfbuzz.0.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libjpeg.62.4.0.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libjpeg.62.4.0.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/liblcms2.2.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/liblcms2.2.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/liblzma.5.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/liblzma.5.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libopenjp2.2.5.4.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libopenjp2.2.5.4.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libpng16.16.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libpng16.16.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libsharpyuv.0.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libsharpyuv.0.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libtiff.6.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libtiff.6.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libwebp.7.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libwebp.7.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libwebpdemux.2.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libwebpdemux.2.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libwebpmux.3.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libwebpmux.3.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libxcb.1.1.0.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libxcb.1.1.0.dylib
Executable file
Binary file not shown.
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libz.1.3.1.zlib-ng.dylib
Executable file
BIN
venv/lib/python3.11/site-packages/PIL/.dylibs/libz.1.3.1.zlib-ng.dylib
Executable file
Binary file not shown.
293
venv/lib/python3.11/site-packages/PIL/AvifImagePlugin.py
Normal file
293
venv/lib/python3.11/site-packages/PIL/AvifImagePlugin.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import ExifTags, Image, ImageFile
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import _avif
|
||||||
|
|
||||||
|
SUPPORTED = True
|
||||||
|
except ImportError:
|
||||||
|
SUPPORTED = False
|
||||||
|
|
||||||
|
# Decoder options as module globals, until there is a way to pass parameters
|
||||||
|
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
|
||||||
|
DECODE_CODEC_CHOICE = "auto"
|
||||||
|
DEFAULT_MAX_THREADS = 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_codec_version(codec_name: str) -> str | None:
|
||||||
|
versions = _avif.codec_versions()
|
||||||
|
for version in versions.split(", "):
|
||||||
|
if version.split(" [")[0] == codec_name:
|
||||||
|
return version.split(":")[-1].split(" ")[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool | str:
|
||||||
|
if prefix[4:8] != b"ftyp":
|
||||||
|
return False
|
||||||
|
major_brand = prefix[8:12]
|
||||||
|
if major_brand in (
|
||||||
|
# coding brands
|
||||||
|
b"avif",
|
||||||
|
b"avis",
|
||||||
|
# We accept files with AVIF container brands; we can't yet know if
|
||||||
|
# the ftyp box has the correct compatible brands, but if it doesn't
|
||||||
|
# then the plugin will raise a SyntaxError which Pillow will catch
|
||||||
|
# before moving on to the next plugin that accepts the file.
|
||||||
|
#
|
||||||
|
# Also, because this file might not actually be an AVIF file, we
|
||||||
|
# don't raise an error if AVIF support isn't properly compiled.
|
||||||
|
b"mif1",
|
||||||
|
b"msf1",
|
||||||
|
):
|
||||||
|
if not SUPPORTED:
|
||||||
|
return (
|
||||||
|
"image file could not be identified because AVIF support not installed"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_max_threads() -> int:
|
||||||
|
if DEFAULT_MAX_THREADS:
|
||||||
|
return DEFAULT_MAX_THREADS
|
||||||
|
if hasattr(os, "sched_getaffinity"):
|
||||||
|
return len(os.sched_getaffinity(0))
|
||||||
|
else:
|
||||||
|
return os.cpu_count() or 1
|
||||||
|
|
||||||
|
|
||||||
|
class AvifImageFile(ImageFile.ImageFile):
|
||||||
|
format = "AVIF"
|
||||||
|
format_description = "AVIF image"
|
||||||
|
__frame = -1
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
if not SUPPORTED:
|
||||||
|
msg = "image file could not be opened because AVIF support not installed"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
|
||||||
|
DECODE_CODEC_CHOICE
|
||||||
|
):
|
||||||
|
msg = "Invalid opening codec"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
assert self.fp is not None
|
||||||
|
self._decoder = _avif.AvifDecoder(
|
||||||
|
self.fp.read(),
|
||||||
|
DECODE_CODEC_CHOICE,
|
||||||
|
_get_default_max_threads(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get info from decoder
|
||||||
|
self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
|
||||||
|
self._decoder.get_info()
|
||||||
|
)
|
||||||
|
self.is_animated = self.n_frames > 1
|
||||||
|
|
||||||
|
if icc:
|
||||||
|
self.info["icc_profile"] = icc
|
||||||
|
if xmp:
|
||||||
|
self.info["xmp"] = xmp
|
||||||
|
|
||||||
|
if exif_orientation != 1 or exif:
|
||||||
|
exif_data = Image.Exif()
|
||||||
|
if exif:
|
||||||
|
exif_data.load(exif)
|
||||||
|
original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
|
||||||
|
else:
|
||||||
|
original_orientation = 1
|
||||||
|
if exif_orientation != original_orientation:
|
||||||
|
exif_data[ExifTags.Base.Orientation] = exif_orientation
|
||||||
|
exif = exif_data.tobytes()
|
||||||
|
if exif:
|
||||||
|
self.info["exif"] = exif
|
||||||
|
self.seek(0)
|
||||||
|
|
||||||
|
def seek(self, frame: int) -> None:
|
||||||
|
if not self._seek_check(frame):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set tile
|
||||||
|
self.__frame = frame
|
||||||
|
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
|
||||||
|
|
||||||
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
|
if self.tile:
|
||||||
|
# We need to load the image data for this frame
|
||||||
|
data, timescale, pts_in_timescales, duration_in_timescales = (
|
||||||
|
self._decoder.get_frame(self.__frame)
|
||||||
|
)
|
||||||
|
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
|
||||||
|
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
|
||||||
|
|
||||||
|
if self.fp and self._exclusive_fp:
|
||||||
|
self.fp.close()
|
||||||
|
self.fp = BytesIO(data)
|
||||||
|
|
||||||
|
return super().load()
|
||||||
|
|
||||||
|
def load_seek(self, pos: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
return self.__frame
|
||||||
|
|
||||||
|
|
||||||
|
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
_save(im, fp, filename, save_all=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _save(
|
||||||
|
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
|
||||||
|
) -> None:
|
||||||
|
info = im.encoderinfo.copy()
|
||||||
|
if save_all:
|
||||||
|
append_images = list(info.get("append_images", []))
|
||||||
|
else:
|
||||||
|
append_images = []
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for ims in [im] + append_images:
|
||||||
|
total += getattr(ims, "n_frames", 1)
|
||||||
|
|
||||||
|
quality = info.get("quality", 75)
|
||||||
|
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
||||||
|
msg = "Invalid quality setting"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
duration = info.get("duration", 0)
|
||||||
|
subsampling = info.get("subsampling", "4:2:0")
|
||||||
|
speed = info.get("speed", 6)
|
||||||
|
max_threads = info.get("max_threads", _get_default_max_threads())
|
||||||
|
codec = info.get("codec", "auto")
|
||||||
|
if codec != "auto" and not _avif.encoder_codec_available(codec):
|
||||||
|
msg = "Invalid saving codec"
|
||||||
|
raise ValueError(msg)
|
||||||
|
range_ = info.get("range", "full")
|
||||||
|
tile_rows_log2 = info.get("tile_rows", 0)
|
||||||
|
tile_cols_log2 = info.get("tile_cols", 0)
|
||||||
|
alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
|
||||||
|
autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
|
||||||
|
|
||||||
|
icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
|
||||||
|
exif_orientation = 1
|
||||||
|
if exif := info.get("exif"):
|
||||||
|
if isinstance(exif, Image.Exif):
|
||||||
|
exif_data = exif
|
||||||
|
else:
|
||||||
|
exif_data = Image.Exif()
|
||||||
|
exif_data.load(exif)
|
||||||
|
if ExifTags.Base.Orientation in exif_data:
|
||||||
|
exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
|
||||||
|
exif = exif_data.tobytes() if exif_data else b""
|
||||||
|
elif isinstance(exif, Image.Exif):
|
||||||
|
exif = exif_data.tobytes()
|
||||||
|
|
||||||
|
xmp = info.get("xmp")
|
||||||
|
|
||||||
|
if isinstance(xmp, str):
|
||||||
|
xmp = xmp.encode("utf-8")
|
||||||
|
|
||||||
|
advanced = info.get("advanced")
|
||||||
|
if advanced is not None:
|
||||||
|
if isinstance(advanced, dict):
|
||||||
|
advanced = advanced.items()
|
||||||
|
try:
|
||||||
|
advanced = tuple(advanced)
|
||||||
|
except TypeError:
|
||||||
|
invalid = True
|
||||||
|
else:
|
||||||
|
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
|
||||||
|
if invalid:
|
||||||
|
msg = (
|
||||||
|
"advanced codec options must be a dict of key-value string "
|
||||||
|
"pairs or a series of key-value two-tuples"
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Setup the AVIF encoder
|
||||||
|
enc = _avif.AvifEncoder(
|
||||||
|
im.size,
|
||||||
|
subsampling,
|
||||||
|
quality,
|
||||||
|
speed,
|
||||||
|
max_threads,
|
||||||
|
codec,
|
||||||
|
range_,
|
||||||
|
tile_rows_log2,
|
||||||
|
tile_cols_log2,
|
||||||
|
alpha_premultiplied,
|
||||||
|
autotiling,
|
||||||
|
icc_profile or b"",
|
||||||
|
exif or b"",
|
||||||
|
exif_orientation,
|
||||||
|
xmp or b"",
|
||||||
|
advanced,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add each frame
|
||||||
|
frame_idx = 0
|
||||||
|
frame_duration = 0
|
||||||
|
cur_idx = im.tell()
|
||||||
|
is_single_frame = total == 1
|
||||||
|
try:
|
||||||
|
for ims in [im] + append_images:
|
||||||
|
# Get number of frames in this image
|
||||||
|
nfr = getattr(ims, "n_frames", 1)
|
||||||
|
|
||||||
|
for idx in range(nfr):
|
||||||
|
ims.seek(idx)
|
||||||
|
|
||||||
|
# Make sure image mode is supported
|
||||||
|
frame = ims
|
||||||
|
rawmode = ims.mode
|
||||||
|
if ims.mode not in {"RGB", "RGBA"}:
|
||||||
|
rawmode = "RGBA" if ims.has_transparency_data else "RGB"
|
||||||
|
frame = ims.convert(rawmode)
|
||||||
|
|
||||||
|
# Update frame duration
|
||||||
|
if isinstance(duration, (list, tuple)):
|
||||||
|
frame_duration = duration[frame_idx]
|
||||||
|
else:
|
||||||
|
frame_duration = duration
|
||||||
|
|
||||||
|
# Append the frame to the animation encoder
|
||||||
|
enc.add(
|
||||||
|
frame.tobytes("raw", rawmode),
|
||||||
|
frame_duration,
|
||||||
|
frame.size,
|
||||||
|
rawmode,
|
||||||
|
is_single_frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update frame index
|
||||||
|
frame_idx += 1
|
||||||
|
|
||||||
|
if not save_all:
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
im.seek(cur_idx)
|
||||||
|
|
||||||
|
# Get the final output from the encoder
|
||||||
|
data = enc.finish()
|
||||||
|
if data is None:
|
||||||
|
msg = "cannot write file as AVIF (encoder returned None)"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
fp.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
|
||||||
|
if SUPPORTED:
|
||||||
|
Image.register_save(AvifImageFile.format, _save)
|
||||||
|
Image.register_save_all(AvifImageFile.format, _save_all)
|
||||||
|
Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
|
||||||
|
Image.register_mime(AvifImageFile.format, "image/avif")
|
||||||
123
venv/lib/python3.11/site-packages/PIL/BdfFontFile.py
Normal file
123
venv/lib/python3.11/site-packages/PIL/BdfFontFile.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# bitmap distribution font (bdf) file parser
|
||||||
|
#
|
||||||
|
# history:
|
||||||
|
# 1996-05-16 fl created (as bdf2pil)
|
||||||
|
# 1997-08-25 fl converted to FontFile driver
|
||||||
|
# 2001-05-25 fl removed bogus __init__ call
|
||||||
|
# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
|
||||||
|
# 2003-04-22 fl more robustification (from Graham Dumpleton)
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2003 by Secret Labs AB.
|
||||||
|
# Copyright (c) 1997-2003 by Fredrik Lundh.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parse X Bitmap Distribution Format (BDF)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from . import FontFile, Image
|
||||||
|
|
||||||
|
|
||||||
|
def bdf_char(
|
||||||
|
f: BinaryIO,
|
||||||
|
) -> (
|
||||||
|
tuple[
|
||||||
|
str,
|
||||||
|
int,
|
||||||
|
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
|
||||||
|
Image.Image,
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
):
|
||||||
|
# skip to STARTCHAR
|
||||||
|
while True:
|
||||||
|
s = f.readline()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
if s.startswith(b"STARTCHAR"):
|
||||||
|
break
|
||||||
|
id = s[9:].strip().decode("ascii")
|
||||||
|
|
||||||
|
# load symbol properties
|
||||||
|
props = {}
|
||||||
|
while True:
|
||||||
|
s = f.readline()
|
||||||
|
if not s or s.startswith(b"BITMAP"):
|
||||||
|
break
|
||||||
|
i = s.find(b" ")
|
||||||
|
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||||
|
|
||||||
|
# load bitmap
|
||||||
|
bitmap = bytearray()
|
||||||
|
while True:
|
||||||
|
s = f.readline()
|
||||||
|
if not s or s.startswith(b"ENDCHAR"):
|
||||||
|
break
|
||||||
|
bitmap += s[:-1]
|
||||||
|
|
||||||
|
# The word BBX
|
||||||
|
# followed by the width in x (BBw), height in y (BBh),
|
||||||
|
# and x and y displacement (BBxoff0, BByoff0)
|
||||||
|
# of the lower left corner from the origin of the character.
|
||||||
|
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
|
||||||
|
|
||||||
|
# The word DWIDTH
|
||||||
|
# followed by the width in x and y of the character in device pixels.
|
||||||
|
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
|
||||||
|
|
||||||
|
bbox = (
|
||||||
|
(dwx, dwy),
|
||||||
|
(x_disp, -y_disp - height, width + x_disp, -y_disp),
|
||||||
|
(0, 0, width, height),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
|
||||||
|
except ValueError:
|
||||||
|
# deal with zero-width characters
|
||||||
|
im = Image.new("1", (width, height))
|
||||||
|
|
||||||
|
return id, int(props["ENCODING"]), bbox, im
|
||||||
|
|
||||||
|
|
||||||
|
class BdfFontFile(FontFile.FontFile):
|
||||||
|
"""Font file plugin for the X11 BDF format."""
|
||||||
|
|
||||||
|
def __init__(self, fp: BinaryIO) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
s = fp.readline()
|
||||||
|
if not s.startswith(b"STARTFONT 2.1"):
|
||||||
|
msg = "not a valid BDF file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
props = {}
|
||||||
|
comments = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
s = fp.readline()
|
||||||
|
if not s or s.startswith(b"ENDPROPERTIES"):
|
||||||
|
break
|
||||||
|
i = s.find(b" ")
|
||||||
|
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||||
|
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
|
||||||
|
if s.find(b"LogicalFontDescription") < 0:
|
||||||
|
comments.append(s[i + 1 : -1].decode("ascii"))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
c = bdf_char(fp)
|
||||||
|
if not c:
|
||||||
|
break
|
||||||
|
id, ch, (xy, dst, src), im = c
|
||||||
|
if 0 <= ch < len(self.glyph):
|
||||||
|
self.glyph[ch] = xy, dst, src, im
|
||||||
498
venv/lib/python3.11/site-packages/PIL/BlpImagePlugin.py
Normal file
498
venv/lib/python3.11/site-packages/PIL/BlpImagePlugin.py
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
"""
|
||||||
|
Blizzard Mipmap Format (.blp)
|
||||||
|
Jerome Leclanche <jerome@leclan.ch>
|
||||||
|
|
||||||
|
The contents of this file are hereby released in the public domain (CC0)
|
||||||
|
Full text of the CC0 license:
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
BLP1 files, used mostly in Warcraft III, are not fully supported.
|
||||||
|
All types of BLP2 files used in World of Warcraft are supported.
|
||||||
|
|
||||||
|
The BLP file structure consists of a header, up to 16 mipmaps of the
|
||||||
|
texture
|
||||||
|
|
||||||
|
Texture sizes must be powers of two, though the two dimensions do
|
||||||
|
not have to be equal; 512x256 is valid, but 512x200 is not.
|
||||||
|
The first mipmap (mipmap #0) is the full size image; each subsequent
|
||||||
|
mipmap halves both dimensions. The final mipmap should be 1x1.
|
||||||
|
|
||||||
|
BLP files come in many different flavours:
|
||||||
|
* JPEG-compressed (type == 0) - only supported for BLP1.
|
||||||
|
* RAW images (type == 1, encoding == 1). Each mipmap is stored as an
|
||||||
|
array of 8-bit values, one per pixel, left to right, top to bottom.
|
||||||
|
Each value is an index to the palette.
|
||||||
|
* DXT-compressed (type == 1, encoding == 2):
|
||||||
|
- DXT1 compression is used if alpha_encoding == 0.
|
||||||
|
- An additional alpha bit is used if alpha_depth == 1.
|
||||||
|
- DXT3 compression is used if alpha_encoding == 1.
|
||||||
|
- DXT5 compression is used if alpha_encoding == 7.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from enum import IntEnum
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
|
||||||
|
class Format(IntEnum):
|
||||||
|
JPEG = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Encoding(IntEnum):
|
||||||
|
UNCOMPRESSED = 1
|
||||||
|
DXT = 2
|
||||||
|
UNCOMPRESSED_RAW_BGRA = 3
|
||||||
|
|
||||||
|
|
||||||
|
class AlphaEncoding(IntEnum):
|
||||||
|
DXT1 = 0
|
||||||
|
DXT3 = 1
|
||||||
|
DXT5 = 7
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_565(i: int) -> tuple[int, int, int]:
|
||||||
|
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
|
||||||
|
|
||||||
|
|
||||||
|
def decode_dxt1(
|
||||||
|
data: bytes, alpha: bool = False
|
||||||
|
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||||
|
"""
|
||||||
|
input: one "row" of data (i.e. will produce 4*width pixels)
|
||||||
|
"""
|
||||||
|
|
||||||
|
blocks = len(data) // 8 # number of blocks in row
|
||||||
|
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||||
|
|
||||||
|
for block_index in range(blocks):
|
||||||
|
# Decode next 8-byte block.
|
||||||
|
idx = block_index * 8
|
||||||
|
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
|
||||||
|
|
||||||
|
r0, g0, b0 = unpack_565(color0)
|
||||||
|
r1, g1, b1 = unpack_565(color1)
|
||||||
|
|
||||||
|
# Decode this block into 4x4 pixels
|
||||||
|
# Accumulate the results onto our 4 row accumulators
|
||||||
|
for j in range(4):
|
||||||
|
for i in range(4):
|
||||||
|
# get next control op and generate a pixel
|
||||||
|
|
||||||
|
control = bits & 3
|
||||||
|
bits = bits >> 2
|
||||||
|
|
||||||
|
a = 0xFF
|
||||||
|
if control == 0:
|
||||||
|
r, g, b = r0, g0, b0
|
||||||
|
elif control == 1:
|
||||||
|
r, g, b = r1, g1, b1
|
||||||
|
elif control == 2:
|
||||||
|
if color0 > color1:
|
||||||
|
r = (2 * r0 + r1) // 3
|
||||||
|
g = (2 * g0 + g1) // 3
|
||||||
|
b = (2 * b0 + b1) // 3
|
||||||
|
else:
|
||||||
|
r = (r0 + r1) // 2
|
||||||
|
g = (g0 + g1) // 2
|
||||||
|
b = (b0 + b1) // 2
|
||||||
|
elif control == 3:
|
||||||
|
if color0 > color1:
|
||||||
|
r = (2 * r1 + r0) // 3
|
||||||
|
g = (2 * g1 + g0) // 3
|
||||||
|
b = (2 * b1 + b0) // 3
|
||||||
|
else:
|
||||||
|
r, g, b, a = 0, 0, 0, 0
|
||||||
|
|
||||||
|
if alpha:
|
||||||
|
ret[j].extend([r, g, b, a])
|
||||||
|
else:
|
||||||
|
ret[j].extend([r, g, b])
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||||
|
"""
|
||||||
|
input: one "row" of data (i.e. will produce 4*width pixels)
|
||||||
|
"""
|
||||||
|
|
||||||
|
blocks = len(data) // 16 # number of blocks in row
|
||||||
|
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||||
|
|
||||||
|
for block_index in range(blocks):
|
||||||
|
idx = block_index * 16
|
||||||
|
block = data[idx : idx + 16]
|
||||||
|
# Decode next 16-byte block.
|
||||||
|
bits = struct.unpack_from("<8B", block)
|
||||||
|
color0, color1 = struct.unpack_from("<HH", block, 8)
|
||||||
|
|
||||||
|
(code,) = struct.unpack_from("<I", block, 12)
|
||||||
|
|
||||||
|
r0, g0, b0 = unpack_565(color0)
|
||||||
|
r1, g1, b1 = unpack_565(color1)
|
||||||
|
|
||||||
|
for j in range(4):
|
||||||
|
high = False # Do we want the higher bits?
|
||||||
|
for i in range(4):
|
||||||
|
alphacode_index = (4 * j + i) // 2
|
||||||
|
a = bits[alphacode_index]
|
||||||
|
if high:
|
||||||
|
high = False
|
||||||
|
a >>= 4
|
||||||
|
else:
|
||||||
|
high = True
|
||||||
|
a &= 0xF
|
||||||
|
a *= 17 # We get a value between 0 and 15
|
||||||
|
|
||||||
|
color_code = (code >> 2 * (4 * j + i)) & 0x03
|
||||||
|
|
||||||
|
if color_code == 0:
|
||||||
|
r, g, b = r0, g0, b0
|
||||||
|
elif color_code == 1:
|
||||||
|
r, g, b = r1, g1, b1
|
||||||
|
elif color_code == 2:
|
||||||
|
r = (2 * r0 + r1) // 3
|
||||||
|
g = (2 * g0 + g1) // 3
|
||||||
|
b = (2 * b0 + b1) // 3
|
||||||
|
elif color_code == 3:
|
||||||
|
r = (2 * r1 + r0) // 3
|
||||||
|
g = (2 * g1 + g0) // 3
|
||||||
|
b = (2 * b1 + b0) // 3
|
||||||
|
|
||||||
|
ret[j].extend([r, g, b, a])
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||||
|
"""
|
||||||
|
input: one "row" of data (i.e. will produce 4 * width pixels)
|
||||||
|
"""
|
||||||
|
|
||||||
|
blocks = len(data) // 16 # number of blocks in row
|
||||||
|
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||||
|
|
||||||
|
for block_index in range(blocks):
|
||||||
|
idx = block_index * 16
|
||||||
|
block = data[idx : idx + 16]
|
||||||
|
# Decode next 16-byte block.
|
||||||
|
a0, a1 = struct.unpack_from("<BB", block)
|
||||||
|
|
||||||
|
bits = struct.unpack_from("<6B", block, 2)
|
||||||
|
alphacode1 = bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24)
|
||||||
|
alphacode2 = bits[0] | (bits[1] << 8)
|
||||||
|
|
||||||
|
color0, color1 = struct.unpack_from("<HH", block, 8)
|
||||||
|
|
||||||
|
(code,) = struct.unpack_from("<I", block, 12)
|
||||||
|
|
||||||
|
r0, g0, b0 = unpack_565(color0)
|
||||||
|
r1, g1, b1 = unpack_565(color1)
|
||||||
|
|
||||||
|
for j in range(4):
|
||||||
|
for i in range(4):
|
||||||
|
# get next control op and generate a pixel
|
||||||
|
alphacode_index = 3 * (4 * j + i)
|
||||||
|
|
||||||
|
if alphacode_index <= 12:
|
||||||
|
alphacode = (alphacode2 >> alphacode_index) & 0x07
|
||||||
|
elif alphacode_index == 15:
|
||||||
|
alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06)
|
||||||
|
else: # alphacode_index >= 18 and alphacode_index <= 45
|
||||||
|
alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07
|
||||||
|
|
||||||
|
if alphacode == 0:
|
||||||
|
a = a0
|
||||||
|
elif alphacode == 1:
|
||||||
|
a = a1
|
||||||
|
elif a0 > a1:
|
||||||
|
a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7
|
||||||
|
elif alphacode == 6:
|
||||||
|
a = 0
|
||||||
|
elif alphacode == 7:
|
||||||
|
a = 255
|
||||||
|
else:
|
||||||
|
a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5
|
||||||
|
|
||||||
|
color_code = (code >> 2 * (4 * j + i)) & 0x03
|
||||||
|
|
||||||
|
if color_code == 0:
|
||||||
|
r, g, b = r0, g0, b0
|
||||||
|
elif color_code == 1:
|
||||||
|
r, g, b = r1, g1, b1
|
||||||
|
elif color_code == 2:
|
||||||
|
r = (2 * r0 + r1) // 3
|
||||||
|
g = (2 * g0 + g1) // 3
|
||||||
|
b = (2 * b0 + b1) // 3
|
||||||
|
elif color_code == 3:
|
||||||
|
r = (2 * r1 + r0) // 3
|
||||||
|
g = (2 * g1 + g0) // 3
|
||||||
|
b = (2 * b1 + b0) // 3
|
||||||
|
|
||||||
|
ret[j].extend([r, g, b, a])
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class BLPFormatError(NotImplementedError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith((b"BLP1", b"BLP2"))
|
||||||
|
|
||||||
|
|
||||||
|
class BlpImageFile(ImageFile.ImageFile):
|
||||||
|
"""
|
||||||
|
Blizzard Mipmap Format
|
||||||
|
"""
|
||||||
|
|
||||||
|
format = "BLP"
|
||||||
|
format_description = "Blizzard Mipmap Format"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
self.magic = self.fp.read(4)
|
||||||
|
if not _accept(self.magic):
|
||||||
|
msg = f"Bad BLP magic {repr(self.magic)}"
|
||||||
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
|
compression = struct.unpack("<i", self.fp.read(4))[0]
|
||||||
|
if self.magic == b"BLP1":
|
||||||
|
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
|
||||||
|
else:
|
||||||
|
encoding = struct.unpack("<b", self.fp.read(1))[0]
|
||||||
|
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
|
||||||
|
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
|
||||||
|
self.fp.seek(1, os.SEEK_CUR) # mips
|
||||||
|
|
||||||
|
self._size = struct.unpack("<II", self.fp.read(8))
|
||||||
|
|
||||||
|
args: tuple[int, int, bool] | tuple[int, int, bool, int]
|
||||||
|
if self.magic == b"BLP1":
|
||||||
|
encoding = struct.unpack("<i", self.fp.read(4))[0]
|
||||||
|
self.fp.seek(4, os.SEEK_CUR) # subtype
|
||||||
|
|
||||||
|
args = (compression, encoding, alpha)
|
||||||
|
offset = 28
|
||||||
|
else:
|
||||||
|
args = (compression, encoding, alpha, alpha_encoding)
|
||||||
|
offset = 20
|
||||||
|
|
||||||
|
decoder = self.magic.decode()
|
||||||
|
|
||||||
|
self._mode = "RGBA" if alpha else "RGB"
|
||||||
|
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
||||||
|
|
||||||
|
|
||||||
|
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
|
||||||
|
_pulls_fd = True
|
||||||
|
|
||||||
|
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||||
|
try:
|
||||||
|
self._read_header()
|
||||||
|
self._load()
|
||||||
|
except struct.error as e:
|
||||||
|
msg = "Truncated BLP file"
|
||||||
|
raise OSError(msg) from e
|
||||||
|
return -1, 0
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _load(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _read_header(self) -> None:
|
||||||
|
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||||
|
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||||
|
|
||||||
|
def _safe_read(self, length: int) -> bytes:
|
||||||
|
assert self.fd is not None
|
||||||
|
return ImageFile._safe_read(self.fd, length)
|
||||||
|
|
||||||
|
def _read_palette(self) -> list[tuple[int, int, int, int]]:
|
||||||
|
ret = []
|
||||||
|
for i in range(256):
|
||||||
|
try:
|
||||||
|
b, g, r, a = struct.unpack("<4B", self._safe_read(4))
|
||||||
|
except struct.error:
|
||||||
|
break
|
||||||
|
ret.append((b, g, r, a))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _read_bgra(
|
||||||
|
self, palette: list[tuple[int, int, int, int]], alpha: bool
|
||||||
|
) -> bytearray:
|
||||||
|
data = bytearray()
|
||||||
|
_data = BytesIO(self._safe_read(self._lengths[0]))
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
(offset,) = struct.unpack("<B", _data.read(1))
|
||||||
|
except struct.error:
|
||||||
|
break
|
||||||
|
b, g, r, a = palette[offset]
|
||||||
|
d: tuple[int, ...] = (r, g, b)
|
||||||
|
if alpha:
|
||||||
|
d += (a,)
|
||||||
|
data.extend(d)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class BLP1Decoder(_BLPBaseDecoder):
|
||||||
|
def _load(self) -> None:
|
||||||
|
self._compression, self._encoding, alpha = self.args
|
||||||
|
|
||||||
|
if self._compression == Format.JPEG:
|
||||||
|
self._decode_jpeg_stream()
|
||||||
|
|
||||||
|
elif self._compression == 1:
|
||||||
|
if self._encoding in (4, 5):
|
||||||
|
palette = self._read_palette()
|
||||||
|
data = self._read_bgra(palette, alpha)
|
||||||
|
self.set_as_raw(data)
|
||||||
|
else:
|
||||||
|
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
|
||||||
|
raise BLPFormatError(msg)
|
||||||
|
else:
|
||||||
|
msg = f"Unsupported BLP compression {repr(self._encoding)}"
|
||||||
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
|
def _decode_jpeg_stream(self) -> None:
|
||||||
|
from .JpegImagePlugin import JpegImageFile
|
||||||
|
|
||||||
|
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
||||||
|
jpeg_header = self._safe_read(jpeg_header_size)
|
||||||
|
assert self.fd is not None
|
||||||
|
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
|
||||||
|
data = self._safe_read(self._lengths[0])
|
||||||
|
data = jpeg_header + data
|
||||||
|
image = JpegImageFile(BytesIO(data))
|
||||||
|
Image._decompression_bomb_check(image.size)
|
||||||
|
if image.mode == "CMYK":
|
||||||
|
args = image.tile[0].args
|
||||||
|
assert isinstance(args, tuple)
|
||||||
|
image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
|
||||||
|
self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
|
||||||
|
|
||||||
|
|
||||||
|
class BLP2Decoder(_BLPBaseDecoder):
|
||||||
|
def _load(self) -> None:
|
||||||
|
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
|
||||||
|
|
||||||
|
palette = self._read_palette()
|
||||||
|
|
||||||
|
assert self.fd is not None
|
||||||
|
self.fd.seek(self._offsets[0])
|
||||||
|
|
||||||
|
if self._compression == 1:
|
||||||
|
# Uncompressed or DirectX compression
|
||||||
|
|
||||||
|
if self._encoding == Encoding.UNCOMPRESSED:
|
||||||
|
data = self._read_bgra(palette, alpha)
|
||||||
|
|
||||||
|
elif self._encoding == Encoding.DXT:
|
||||||
|
data = bytearray()
|
||||||
|
if self._alpha_encoding == AlphaEncoding.DXT1:
|
||||||
|
linesize = (self.state.xsize + 3) // 4 * 8
|
||||||
|
for yb in range((self.state.ysize + 3) // 4):
|
||||||
|
for d in decode_dxt1(self._safe_read(linesize), alpha):
|
||||||
|
data += d
|
||||||
|
|
||||||
|
elif self._alpha_encoding == AlphaEncoding.DXT3:
|
||||||
|
linesize = (self.state.xsize + 3) // 4 * 16
|
||||||
|
for yb in range((self.state.ysize + 3) // 4):
|
||||||
|
for d in decode_dxt3(self._safe_read(linesize)):
|
||||||
|
data += d
|
||||||
|
|
||||||
|
elif self._alpha_encoding == AlphaEncoding.DXT5:
|
||||||
|
linesize = (self.state.xsize + 3) // 4 * 16
|
||||||
|
for yb in range((self.state.ysize + 3) // 4):
|
||||||
|
for d in decode_dxt5(self._safe_read(linesize)):
|
||||||
|
data += d
|
||||||
|
else:
|
||||||
|
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
|
||||||
|
raise BLPFormatError(msg)
|
||||||
|
else:
|
||||||
|
msg = f"Unknown BLP encoding {repr(self._encoding)}"
|
||||||
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = f"Unknown BLP compression {repr(self._compression)}"
|
||||||
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
|
self.set_as_raw(data)
|
||||||
|
|
||||||
|
|
||||||
|
class BLPEncoder(ImageFile.PyEncoder):
|
||||||
|
_pushes_fd = True
|
||||||
|
|
||||||
|
def _write_palette(self) -> bytes:
|
||||||
|
data = b""
|
||||||
|
assert self.im is not None
|
||||||
|
palette = self.im.getpalette("RGBA", "RGBA")
|
||||||
|
for i in range(len(palette) // 4):
|
||||||
|
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
||||||
|
data += struct.pack("<4B", b, g, r, a)
|
||||||
|
while len(data) < 256 * 4:
|
||||||
|
data += b"\x00" * 4
|
||||||
|
return data
|
||||||
|
|
||||||
|
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||||
|
palette_data = self._write_palette()
|
||||||
|
|
||||||
|
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
||||||
|
data = struct.pack("<16I", offset, *((0,) * 15))
|
||||||
|
|
||||||
|
assert self.im is not None
|
||||||
|
w, h = self.im.size
|
||||||
|
data += struct.pack("<16I", w * h, *((0,) * 15))
|
||||||
|
|
||||||
|
data += palette_data
|
||||||
|
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
data += struct.pack("<B", self.im.getpixel((x, y)))
|
||||||
|
|
||||||
|
return len(data), 0, data
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
if im.mode != "P":
|
||||||
|
msg = "Unsupported BLP image mode"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
|
||||||
|
fp.write(magic)
|
||||||
|
|
||||||
|
assert im.palette is not None
|
||||||
|
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
|
||||||
|
|
||||||
|
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
|
||||||
|
if magic == b"BLP1":
|
||||||
|
fp.write(struct.pack("<L", alpha_depth))
|
||||||
|
else:
|
||||||
|
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
||||||
|
fp.write(struct.pack("<b", alpha_depth))
|
||||||
|
fp.write(struct.pack("<b", 0)) # alpha encoding
|
||||||
|
fp.write(struct.pack("<b", 0)) # mips
|
||||||
|
fp.write(struct.pack("<II", *im.size))
|
||||||
|
if magic == b"BLP1":
|
||||||
|
fp.write(struct.pack("<i", 5))
|
||||||
|
fp.write(struct.pack("<i", 0))
|
||||||
|
|
||||||
|
ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
|
||||||
|
Image.register_extension(BlpImageFile.format, ".blp")
|
||||||
|
Image.register_decoder("BLP1", BLP1Decoder)
|
||||||
|
Image.register_decoder("BLP2", BLP2Decoder)
|
||||||
|
|
||||||
|
Image.register_save(BlpImageFile.format, _save)
|
||||||
|
Image.register_encoder("BLP", BLPEncoder)
|
||||||
514
venv/lib/python3.11/site-packages/PIL/BmpImagePlugin.py
Normal file
514
venv/lib/python3.11/site-packages/PIL/BmpImagePlugin.py
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# BMP file handler
|
||||||
|
#
|
||||||
|
# Windows (and OS/2) native bitmap storage format.
|
||||||
|
#
|
||||||
|
# history:
|
||||||
|
# 1995-09-01 fl Created
|
||||||
|
# 1996-04-30 fl Added save
|
||||||
|
# 1997-08-27 fl Fixed save of 1-bit images
|
||||||
|
# 1998-03-06 fl Load P images as L where possible
|
||||||
|
# 1998-07-03 fl Load P images as 1 where possible
|
||||||
|
# 1998-12-29 fl Handle small palettes
|
||||||
|
# 2002-12-30 fl Fixed load of 1-bit palette images
|
||||||
|
# 2003-04-21 fl Fixed load of 1-bit monochrome images
|
||||||
|
# 2003-04-23 fl Added limited support for BI_BITFIELDS compression
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2003 by Secret Labs AB
|
||||||
|
# Copyright (c) 1995-2003 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import IO, Any
|
||||||
|
|
||||||
|
from . import Image, ImageFile, ImagePalette
|
||||||
|
from ._binary import i16le as i16
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
from ._binary import o8
|
||||||
|
from ._binary import o16le as o16
|
||||||
|
from ._binary import o32le as o32
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Read BMP file
|
||||||
|
|
||||||
|
BIT2MODE = {
|
||||||
|
# bits => mode, rawmode
|
||||||
|
1: ("P", "P;1"),
|
||||||
|
4: ("P", "P;4"),
|
||||||
|
8: ("P", "P"),
|
||||||
|
16: ("RGB", "BGR;15"),
|
||||||
|
24: ("RGB", "BGR"),
|
||||||
|
32: ("RGB", "BGRX"),
|
||||||
|
}
|
||||||
|
|
||||||
|
USE_RAW_ALPHA = False
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(b"BM")
|
||||||
|
|
||||||
|
|
||||||
|
def _dib_accept(prefix: bytes) -> bool:
|
||||||
|
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Image plugin for the Windows BMP format.
|
||||||
|
# =============================================================================
|
||||||
|
class BmpImageFile(ImageFile.ImageFile):
|
||||||
|
"""Image plugin for the Windows Bitmap format (BMP)"""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Description
|
||||||
|
format_description = "Windows Bitmap"
|
||||||
|
format = "BMP"
|
||||||
|
|
||||||
|
# -------------------------------------------------- BMP Compression values
|
||||||
|
COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
|
||||||
|
for k, v in COMPRESSIONS.items():
|
||||||
|
vars()[k] = v
|
||||||
|
|
||||||
|
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
|
||||||
|
"""Read relevant info about the BMP"""
|
||||||
|
assert self.fp is not None
|
||||||
|
read, seek = self.fp.read, self.fp.seek
|
||||||
|
if header:
|
||||||
|
seek(header)
|
||||||
|
# read bmp header size @offset 14 (this is part of the header size)
|
||||||
|
file_info: dict[str, bool | int | tuple[int, ...]] = {
|
||||||
|
"header_size": i32(read(4)),
|
||||||
|
"direction": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------- If requested, read header at a specific position
|
||||||
|
# read the rest of the bmp header, without its size
|
||||||
|
assert isinstance(file_info["header_size"], int)
|
||||||
|
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
|
||||||
|
|
||||||
|
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
|
||||||
|
# ----- This format has different offsets because of width/height types
|
||||||
|
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
|
||||||
|
if file_info["header_size"] == 12:
|
||||||
|
file_info["width"] = i16(header_data, 0)
|
||||||
|
file_info["height"] = i16(header_data, 2)
|
||||||
|
file_info["planes"] = i16(header_data, 4)
|
||||||
|
file_info["bits"] = i16(header_data, 6)
|
||||||
|
file_info["compression"] = self.COMPRESSIONS["RAW"]
|
||||||
|
file_info["palette_padding"] = 3
|
||||||
|
|
||||||
|
# --------------------------------------------- Windows Bitmap v3 to v5
|
||||||
|
# 40: BITMAPINFOHEADER
|
||||||
|
# 52: BITMAPV2HEADER
|
||||||
|
# 56: BITMAPV3HEADER
|
||||||
|
# 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
|
||||||
|
# 108: BITMAPV4HEADER
|
||||||
|
# 124: BITMAPV5HEADER
|
||||||
|
elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
|
||||||
|
file_info["y_flip"] = header_data[7] == 0xFF
|
||||||
|
file_info["direction"] = 1 if file_info["y_flip"] else -1
|
||||||
|
file_info["width"] = i32(header_data, 0)
|
||||||
|
file_info["height"] = (
|
||||||
|
i32(header_data, 4)
|
||||||
|
if not file_info["y_flip"]
|
||||||
|
else 2**32 - i32(header_data, 4)
|
||||||
|
)
|
||||||
|
file_info["planes"] = i16(header_data, 8)
|
||||||
|
file_info["bits"] = i16(header_data, 10)
|
||||||
|
file_info["compression"] = i32(header_data, 12)
|
||||||
|
# byte size of pixel data
|
||||||
|
file_info["data_size"] = i32(header_data, 16)
|
||||||
|
file_info["pixels_per_meter"] = (
|
||||||
|
i32(header_data, 20),
|
||||||
|
i32(header_data, 24),
|
||||||
|
)
|
||||||
|
file_info["colors"] = i32(header_data, 28)
|
||||||
|
file_info["palette_padding"] = 4
|
||||||
|
assert isinstance(file_info["pixels_per_meter"], tuple)
|
||||||
|
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
|
||||||
|
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
|
||||||
|
masks = ["r_mask", "g_mask", "b_mask"]
|
||||||
|
if len(header_data) >= 48:
|
||||||
|
if len(header_data) >= 52:
|
||||||
|
masks.append("a_mask")
|
||||||
|
else:
|
||||||
|
file_info["a_mask"] = 0x0
|
||||||
|
for idx, mask in enumerate(masks):
|
||||||
|
file_info[mask] = i32(header_data, 36 + idx * 4)
|
||||||
|
else:
|
||||||
|
# 40 byte headers only have the three components in the
|
||||||
|
# bitfields masks, ref:
|
||||||
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
|
||||||
|
# See also
|
||||||
|
# https://github.com/python-pillow/Pillow/issues/1293
|
||||||
|
# There is a 4th component in the RGBQuad, in the alpha
|
||||||
|
# location, but it is listed as a reserved component,
|
||||||
|
# and it is not generally an alpha channel
|
||||||
|
file_info["a_mask"] = 0x0
|
||||||
|
for mask in masks:
|
||||||
|
file_info[mask] = i32(read(4))
|
||||||
|
assert isinstance(file_info["r_mask"], int)
|
||||||
|
assert isinstance(file_info["g_mask"], int)
|
||||||
|
assert isinstance(file_info["b_mask"], int)
|
||||||
|
assert isinstance(file_info["a_mask"], int)
|
||||||
|
file_info["rgb_mask"] = (
|
||||||
|
file_info["r_mask"],
|
||||||
|
file_info["g_mask"],
|
||||||
|
file_info["b_mask"],
|
||||||
|
)
|
||||||
|
file_info["rgba_mask"] = (
|
||||||
|
file_info["r_mask"],
|
||||||
|
file_info["g_mask"],
|
||||||
|
file_info["b_mask"],
|
||||||
|
file_info["a_mask"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = f"Unsupported BMP header type ({file_info['header_size']})"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# ------------------ Special case : header is reported 40, which
|
||||||
|
# ---------------------- is shorter than real size for bpp >= 16
|
||||||
|
assert isinstance(file_info["width"], int)
|
||||||
|
assert isinstance(file_info["height"], int)
|
||||||
|
self._size = file_info["width"], file_info["height"]
|
||||||
|
|
||||||
|
# ------- If color count was not found in the header, compute from bits
|
||||||
|
assert isinstance(file_info["bits"], int)
|
||||||
|
if not file_info.get("colors", 0):
|
||||||
|
file_info["colors"] = 1 << file_info["bits"]
|
||||||
|
assert isinstance(file_info["palette_padding"], int)
|
||||||
|
assert isinstance(file_info["colors"], int)
|
||||||
|
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
|
||||||
|
offset += file_info["palette_padding"] * file_info["colors"]
|
||||||
|
|
||||||
|
# ---------------------- Check bit depth for unusual unsupported values
|
||||||
|
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
|
||||||
|
if not self.mode:
|
||||||
|
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# ---------------- Process BMP with Bitfields compression (not palette)
|
||||||
|
decoder_name = "raw"
|
||||||
|
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
|
||||||
|
SUPPORTED: dict[int, list[tuple[int, ...]]] = {
|
||||||
|
32: [
|
||||||
|
(0xFF0000, 0xFF00, 0xFF, 0x0),
|
||||||
|
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
|
||||||
|
(0xFF000000, 0xFF00, 0xFF, 0x0),
|
||||||
|
(0xFF000000, 0xFF0000, 0xFF00, 0xFF),
|
||||||
|
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
|
||||||
|
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
|
||||||
|
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
|
||||||
|
(0x0, 0x0, 0x0, 0x0),
|
||||||
|
],
|
||||||
|
24: [(0xFF0000, 0xFF00, 0xFF)],
|
||||||
|
16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
|
||||||
|
}
|
||||||
|
MASK_MODES = {
|
||||||
|
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
|
||||||
|
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
|
||||||
|
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
|
||||||
|
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
|
||||||
|
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
|
||||||
|
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
|
||||||
|
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
|
||||||
|
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
|
||||||
|
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
|
||||||
|
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
|
||||||
|
(16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
|
||||||
|
}
|
||||||
|
if file_info["bits"] in SUPPORTED:
|
||||||
|
if (
|
||||||
|
file_info["bits"] == 32
|
||||||
|
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
|
||||||
|
):
|
||||||
|
assert isinstance(file_info["rgba_mask"], tuple)
|
||||||
|
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
|
||||||
|
self._mode = "RGBA" if "A" in raw_mode else self.mode
|
||||||
|
elif (
|
||||||
|
file_info["bits"] in (24, 16)
|
||||||
|
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
|
||||||
|
):
|
||||||
|
assert isinstance(file_info["rgb_mask"], tuple)
|
||||||
|
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
|
||||||
|
else:
|
||||||
|
msg = "Unsupported BMP bitfields layout"
|
||||||
|
raise OSError(msg)
|
||||||
|
else:
|
||||||
|
msg = "Unsupported BMP bitfields layout"
|
||||||
|
raise OSError(msg)
|
||||||
|
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
|
||||||
|
if file_info["bits"] == 32 and (
|
||||||
|
header == 22 or USE_RAW_ALPHA # 32-bit .cur offset
|
||||||
|
):
|
||||||
|
raw_mode, self._mode = "BGRA", "RGBA"
|
||||||
|
elif file_info["compression"] in (
|
||||||
|
self.COMPRESSIONS["RLE8"],
|
||||||
|
self.COMPRESSIONS["RLE4"],
|
||||||
|
):
|
||||||
|
decoder_name = "bmp_rle"
|
||||||
|
else:
|
||||||
|
msg = f"Unsupported BMP compression ({file_info['compression']})"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# --------------- Once the header is processed, process the palette/LUT
|
||||||
|
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
|
||||||
|
# ---------------------------------------------------- 1-bit images
|
||||||
|
if not (0 < file_info["colors"] <= 65536):
|
||||||
|
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
|
||||||
|
raise OSError(msg)
|
||||||
|
else:
|
||||||
|
padding = file_info["palette_padding"]
|
||||||
|
palette = read(padding * file_info["colors"])
|
||||||
|
grayscale = True
|
||||||
|
indices = (
|
||||||
|
(0, 255)
|
||||||
|
if file_info["colors"] == 2
|
||||||
|
else list(range(file_info["colors"]))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------- Check if grayscale and ignore palette if so
|
||||||
|
for ind, val in enumerate(indices):
|
||||||
|
rgb = palette[ind * padding : ind * padding + 3]
|
||||||
|
if rgb != o8(val) * 3:
|
||||||
|
grayscale = False
|
||||||
|
|
||||||
|
# ------- If all colors are gray, white or black, ditch palette
|
||||||
|
if grayscale:
|
||||||
|
self._mode = "1" if file_info["colors"] == 2 else "L"
|
||||||
|
raw_mode = self.mode
|
||||||
|
else:
|
||||||
|
self._mode = "P"
|
||||||
|
self.palette = ImagePalette.raw(
|
||||||
|
"BGRX" if padding == 4 else "BGR", palette
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------- Finally set the tile data for the plugin
|
||||||
|
self.info["compression"] = file_info["compression"]
|
||||||
|
args: list[Any] = [raw_mode]
|
||||||
|
if decoder_name == "bmp_rle":
|
||||||
|
args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
|
||||||
|
else:
|
||||||
|
assert isinstance(file_info["width"], int)
|
||||||
|
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
|
||||||
|
args.append(file_info["direction"])
|
||||||
|
self.tile = [
|
||||||
|
ImageFile._Tile(
|
||||||
|
decoder_name,
|
||||||
|
(0, 0, file_info["width"], file_info["height"]),
|
||||||
|
offset or self.fp.tell(),
|
||||||
|
tuple(args),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
"""Open file, check magic number and read header"""
|
||||||
|
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||||
|
assert self.fp is not None
|
||||||
|
head_data = self.fp.read(14)
|
||||||
|
# choke if the file does not have the required magic bytes
|
||||||
|
if not _accept(head_data):
|
||||||
|
msg = "Not a BMP file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
# read the start position of the BMP image data (u32)
|
||||||
|
offset = i32(head_data, 10)
|
||||||
|
# load bitmap information (offset=raster info)
|
||||||
|
self._bitmap(offset=offset)
|
||||||
|
|
||||||
|
|
||||||
|
class BmpRleDecoder(ImageFile.PyDecoder):
|
||||||
|
_pulls_fd = True
|
||||||
|
|
||||||
|
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||||
|
assert self.fd is not None
|
||||||
|
rle4 = self.args[1]
|
||||||
|
data = bytearray()
|
||||||
|
x = 0
|
||||||
|
dest_length = self.state.xsize * self.state.ysize
|
||||||
|
while len(data) < dest_length:
|
||||||
|
pixels = self.fd.read(1)
|
||||||
|
byte = self.fd.read(1)
|
||||||
|
if not pixels or not byte:
|
||||||
|
break
|
||||||
|
num_pixels = pixels[0]
|
||||||
|
if num_pixels:
|
||||||
|
# encoded mode
|
||||||
|
if x + num_pixels > self.state.xsize:
|
||||||
|
# Too much data for row
|
||||||
|
num_pixels = max(0, self.state.xsize - x)
|
||||||
|
if rle4:
|
||||||
|
first_pixel = o8(byte[0] >> 4)
|
||||||
|
second_pixel = o8(byte[0] & 0x0F)
|
||||||
|
for index in range(num_pixels):
|
||||||
|
if index % 2 == 0:
|
||||||
|
data += first_pixel
|
||||||
|
else:
|
||||||
|
data += second_pixel
|
||||||
|
else:
|
||||||
|
data += byte * num_pixels
|
||||||
|
x += num_pixels
|
||||||
|
else:
|
||||||
|
if byte[0] == 0:
|
||||||
|
# end of line
|
||||||
|
while len(data) % self.state.xsize != 0:
|
||||||
|
data += b"\x00"
|
||||||
|
x = 0
|
||||||
|
elif byte[0] == 1:
|
||||||
|
# end of bitmap
|
||||||
|
break
|
||||||
|
elif byte[0] == 2:
|
||||||
|
# delta
|
||||||
|
bytes_read = self.fd.read(2)
|
||||||
|
if len(bytes_read) < 2:
|
||||||
|
break
|
||||||
|
right, up = bytes_read
|
||||||
|
data += b"\x00" * (right + up * self.state.xsize)
|
||||||
|
x = len(data) % self.state.xsize
|
||||||
|
else:
|
||||||
|
# absolute mode
|
||||||
|
if rle4:
|
||||||
|
# 2 pixels per byte
|
||||||
|
byte_count = byte[0] // 2
|
||||||
|
bytes_read = self.fd.read(byte_count)
|
||||||
|
for byte_read in bytes_read:
|
||||||
|
data += o8(byte_read >> 4)
|
||||||
|
data += o8(byte_read & 0x0F)
|
||||||
|
else:
|
||||||
|
byte_count = byte[0]
|
||||||
|
bytes_read = self.fd.read(byte_count)
|
||||||
|
data += bytes_read
|
||||||
|
if len(bytes_read) < byte_count:
|
||||||
|
break
|
||||||
|
x += byte[0]
|
||||||
|
|
||||||
|
# align to 16-bit word boundary
|
||||||
|
if self.fd.tell() % 2 != 0:
|
||||||
|
self.fd.seek(1, os.SEEK_CUR)
|
||||||
|
rawmode = "L" if self.mode == "L" else "P"
|
||||||
|
self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
|
||||||
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Image plugin for the DIB format (BMP alias)
|
||||||
|
# =============================================================================
|
||||||
|
class DibImageFile(BmpImageFile):
|
||||||
|
format = "DIB"
|
||||||
|
format_description = "Windows Bitmap"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
self._bitmap()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Write BMP file
|
||||||
|
|
||||||
|
|
||||||
|
SAVE = {
|
||||||
|
"1": ("1", 1, 2),
|
||||||
|
"L": ("L", 8, 256),
|
||||||
|
"P": ("P", 8, 256),
|
||||||
|
"RGB": ("BGR", 24, 0),
|
||||||
|
"RGBA": ("BGRA", 32, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
_save(im, fp, filename, False)
|
||||||
|
|
||||||
|
|
||||||
|
def _save(
|
||||||
|
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
rawmode, bits, colors = SAVE[im.mode]
|
||||||
|
except KeyError as e:
|
||||||
|
msg = f"cannot write mode {im.mode} as BMP"
|
||||||
|
raise OSError(msg) from e
|
||||||
|
|
||||||
|
info = im.encoderinfo
|
||||||
|
|
||||||
|
dpi = info.get("dpi", (96, 96))
|
||||||
|
|
||||||
|
# 1 meter == 39.3701 inches
|
||||||
|
ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
|
||||||
|
|
||||||
|
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
|
||||||
|
header = 40 # or 64 for OS/2 version 2
|
||||||
|
image = stride * im.size[1]
|
||||||
|
|
||||||
|
if im.mode == "1":
|
||||||
|
palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255))
|
||||||
|
elif im.mode == "L":
|
||||||
|
palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256))
|
||||||
|
elif im.mode == "P":
|
||||||
|
palette = im.im.getpalette("RGB", "BGRX")
|
||||||
|
colors = len(palette) // 4
|
||||||
|
else:
|
||||||
|
palette = None
|
||||||
|
|
||||||
|
# bitmap header
|
||||||
|
if bitmap_header:
|
||||||
|
offset = 14 + header + colors * 4
|
||||||
|
file_size = offset + image
|
||||||
|
if file_size > 2**32 - 1:
|
||||||
|
msg = "File size is too large for the BMP format"
|
||||||
|
raise ValueError(msg)
|
||||||
|
fp.write(
|
||||||
|
b"BM" # file type (magic)
|
||||||
|
+ o32(file_size) # file size
|
||||||
|
+ o32(0) # reserved
|
||||||
|
+ o32(offset) # image data offset
|
||||||
|
)
|
||||||
|
|
||||||
|
# bitmap info header
|
||||||
|
fp.write(
|
||||||
|
o32(header) # info header size
|
||||||
|
+ o32(im.size[0]) # width
|
||||||
|
+ o32(im.size[1]) # height
|
||||||
|
+ o16(1) # planes
|
||||||
|
+ o16(bits) # depth
|
||||||
|
+ o32(0) # compression (0=uncompressed)
|
||||||
|
+ o32(image) # size of bitmap
|
||||||
|
+ o32(ppm[0]) # resolution
|
||||||
|
+ o32(ppm[1]) # resolution
|
||||||
|
+ o32(colors) # colors used
|
||||||
|
+ o32(colors) # colors important
|
||||||
|
)
|
||||||
|
|
||||||
|
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
|
||||||
|
|
||||||
|
if palette:
|
||||||
|
fp.write(palette)
|
||||||
|
|
||||||
|
ImageFile._save(
|
||||||
|
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Registry
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
|
||||||
|
Image.register_save(BmpImageFile.format, _save)
|
||||||
|
|
||||||
|
Image.register_extension(BmpImageFile.format, ".bmp")
|
||||||
|
|
||||||
|
Image.register_mime(BmpImageFile.format, "image/bmp")
|
||||||
|
|
||||||
|
Image.register_decoder("bmp_rle", BmpRleDecoder)
|
||||||
|
|
||||||
|
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
||||||
|
Image.register_save(DibImageFile.format, _dib_save)
|
||||||
|
|
||||||
|
Image.register_extension(DibImageFile.format, ".dib")
|
||||||
|
|
||||||
|
Image.register_mime(DibImageFile.format, "image/bmp")
|
||||||
72
venv/lib/python3.11/site-packages/PIL/BufrStubImagePlugin.py
Normal file
72
venv/lib/python3.11/site-packages/PIL/BufrStubImagePlugin.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# BUFR stub adapter
|
||||||
|
#
|
||||||
|
# Copyright (c) 1996-2003 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
_handler = None
|
||||||
|
|
||||||
|
|
||||||
|
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||||
|
"""
|
||||||
|
Install application-specific BUFR image handler.
|
||||||
|
|
||||||
|
:param handler: Handler object.
|
||||||
|
"""
|
||||||
|
global _handler
|
||||||
|
_handler = handler
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Image adapter
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith((b"BUFR", b"ZCZC"))
|
||||||
|
|
||||||
|
|
||||||
|
class BufrStubImageFile(ImageFile.StubImageFile):
|
||||||
|
format = "BUFR"
|
||||||
|
format_description = "BUFR"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
if not _accept(self.fp.read(4)):
|
||||||
|
msg = "Not a BUFR file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self.fp.seek(-4, os.SEEK_CUR)
|
||||||
|
|
||||||
|
# make something up
|
||||||
|
self._mode = "F"
|
||||||
|
self._size = 1, 1
|
||||||
|
|
||||||
|
def _load(self) -> ImageFile.StubHandler | None:
|
||||||
|
return _handler
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
if _handler is None or not hasattr(_handler, "save"):
|
||||||
|
msg = "BUFR save handler not installed"
|
||||||
|
raise OSError(msg)
|
||||||
|
_handler.save(im, fp, filename)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Registry
|
||||||
|
|
||||||
|
Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
|
||||||
|
Image.register_save(BufrStubImageFile.format, _save)
|
||||||
|
|
||||||
|
Image.register_extension(BufrStubImageFile.format, ".bufr")
|
||||||
173
venv/lib/python3.11/site-packages/PIL/ContainerIO.py
Normal file
173
venv/lib/python3.11/site-packages/PIL/ContainerIO.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# a class to read from a container file
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1995-06-18 fl Created
|
||||||
|
# 1995-09-07 fl Added readline(), readlines()
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2001 by Secret Labs AB
|
||||||
|
# Copyright (c) 1995 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import IO, AnyStr, NoReturn
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerIO(IO[AnyStr]):
|
||||||
|
"""
|
||||||
|
A file object that provides read access to a part of an existing
|
||||||
|
file (for example a TAR file).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
|
||||||
|
"""
|
||||||
|
Create file object.
|
||||||
|
|
||||||
|
:param file: Existing file.
|
||||||
|
:param offset: Start of region, in bytes.
|
||||||
|
:param length: Size of region, in bytes.
|
||||||
|
"""
|
||||||
|
self.fh: IO[AnyStr] = file
|
||||||
|
self.pos = 0
|
||||||
|
self.offset = offset
|
||||||
|
self.length = length
|
||||||
|
self.fh.seek(offset)
|
||||||
|
|
||||||
|
##
|
||||||
|
# Always false.
|
||||||
|
|
||||||
|
def isatty(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def seekable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def seek(self, offset: int, mode: int = io.SEEK_SET) -> int:
|
||||||
|
"""
|
||||||
|
Move file pointer.
|
||||||
|
|
||||||
|
:param offset: Offset in bytes.
|
||||||
|
:param mode: Starting position. Use 0 for beginning of region, 1
|
||||||
|
for current offset, and 2 for end of region. You cannot move
|
||||||
|
the pointer outside the defined region.
|
||||||
|
:returns: Offset from start of region, in bytes.
|
||||||
|
"""
|
||||||
|
if mode == 1:
|
||||||
|
self.pos = self.pos + offset
|
||||||
|
elif mode == 2:
|
||||||
|
self.pos = self.length + offset
|
||||||
|
else:
|
||||||
|
self.pos = offset
|
||||||
|
# clamp
|
||||||
|
self.pos = max(0, min(self.pos, self.length))
|
||||||
|
self.fh.seek(self.offset + self.pos)
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
"""
|
||||||
|
Get current file pointer.
|
||||||
|
|
||||||
|
:returns: Offset from start of region, in bytes.
|
||||||
|
"""
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def readable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def read(self, n: int = -1) -> AnyStr:
|
||||||
|
"""
|
||||||
|
Read data.
|
||||||
|
|
||||||
|
:param n: Number of bytes to read. If omitted, zero or negative,
|
||||||
|
read until end of region.
|
||||||
|
:returns: An 8-bit string.
|
||||||
|
"""
|
||||||
|
if n > 0:
|
||||||
|
n = min(n, self.length - self.pos)
|
||||||
|
else:
|
||||||
|
n = self.length - self.pos
|
||||||
|
if n <= 0: # EOF
|
||||||
|
return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
|
||||||
|
self.pos = self.pos + n
|
||||||
|
return self.fh.read(n)
|
||||||
|
|
||||||
|
def readline(self, n: int = -1) -> AnyStr:
|
||||||
|
"""
|
||||||
|
Read a line of text.
|
||||||
|
|
||||||
|
:param n: Number of bytes to read. If omitted, zero or negative,
|
||||||
|
read until end of line.
|
||||||
|
:returns: An 8-bit string.
|
||||||
|
"""
|
||||||
|
s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
|
||||||
|
newline_character = b"\n" if "b" in self.fh.mode else "\n"
|
||||||
|
while True:
|
||||||
|
c = self.read(1)
|
||||||
|
if not c:
|
||||||
|
break
|
||||||
|
s = s + c
|
||||||
|
if c == newline_character or len(s) == n:
|
||||||
|
break
|
||||||
|
return s
|
||||||
|
|
||||||
|
def readlines(self, n: int | None = -1) -> list[AnyStr]:
|
||||||
|
"""
|
||||||
|
Read multiple lines of text.
|
||||||
|
|
||||||
|
:param n: Number of lines to read. If omitted, zero, negative or None,
|
||||||
|
read until end of region.
|
||||||
|
:returns: A list of 8-bit strings.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
while True:
|
||||||
|
s = self.readline()
|
||||||
|
if not s:
|
||||||
|
break
|
||||||
|
lines.append(s)
|
||||||
|
if len(lines) == n:
|
||||||
|
break
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def writable(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write(self, b: AnyStr) -> NoReturn:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def writelines(self, lines: Iterable[AnyStr]) -> NoReturn:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def truncate(self, size: int | None = None) -> int:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __enter__(self) -> ContainerIO[AnyStr]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args: object) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __iter__(self) -> ContainerIO[AnyStr]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> AnyStr:
|
||||||
|
line = self.readline()
|
||||||
|
if not line:
|
||||||
|
msg = "end of region"
|
||||||
|
raise StopIteration(msg)
|
||||||
|
return line
|
||||||
|
|
||||||
|
def fileno(self) -> int:
|
||||||
|
return self.fh.fileno()
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
self.fh.flush()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.fh.close()
|
||||||
75
venv/lib/python3.11/site-packages/PIL/CurImagePlugin.py
Normal file
75
venv/lib/python3.11/site-packages/PIL/CurImagePlugin.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# Windows Cursor support for PIL
|
||||||
|
#
|
||||||
|
# notes:
|
||||||
|
# uses BmpImagePlugin.py to read the bitmap data.
|
||||||
|
#
|
||||||
|
# history:
|
||||||
|
# 96-05-27 fl Created
|
||||||
|
#
|
||||||
|
# Copyright (c) Secret Labs AB 1997.
|
||||||
|
# Copyright (c) Fredrik Lundh 1996.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import BmpImagePlugin, Image
|
||||||
|
from ._binary import i16le as i16
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(b"\0\0\2\0")
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Image plugin for Windows Cursor files.
|
||||||
|
|
||||||
|
|
||||||
|
class CurImageFile(BmpImagePlugin.BmpImageFile):
|
||||||
|
format = "CUR"
|
||||||
|
format_description = "Windows Cursor"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
offset = self.fp.tell()
|
||||||
|
|
||||||
|
# check magic
|
||||||
|
s = self.fp.read(6)
|
||||||
|
if not _accept(s):
|
||||||
|
msg = "not a CUR file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
# pick the largest cursor in the file
|
||||||
|
m = b""
|
||||||
|
for i in range(i16(s, 4)):
|
||||||
|
s = self.fp.read(16)
|
||||||
|
if not m:
|
||||||
|
m = s
|
||||||
|
elif s[0] > m[0] and s[1] > m[1]:
|
||||||
|
m = s
|
||||||
|
if not m:
|
||||||
|
msg = "No cursors were found"
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# load as bitmap
|
||||||
|
self._bitmap(i32(m, 12) + offset)
|
||||||
|
|
||||||
|
# patch up the bitmap height
|
||||||
|
self._size = self.size[0], self.size[1] // 2
|
||||||
|
self.tile = [self.tile[0]._replace(extents=(0, 0) + self.size)]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
Image.register_open(CurImageFile.format, CurImageFile, _accept)
|
||||||
|
|
||||||
|
Image.register_extension(CurImageFile.format, ".cur")
|
||||||
84
venv/lib/python3.11/site-packages/PIL/DcxImagePlugin.py
Normal file
84
venv/lib/python3.11/site-packages/PIL/DcxImagePlugin.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# DCX file handling
|
||||||
|
#
|
||||||
|
# DCX is a container file format defined by Intel, commonly used
|
||||||
|
# for fax applications. Each DCX file consists of a directory
|
||||||
|
# (a list of file offsets) followed by a set of (usually 1-bit)
|
||||||
|
# PCX files.
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1995-09-09 fl Created
|
||||||
|
# 1996-03-20 fl Properly derived from PcxImageFile.
|
||||||
|
# 1998-07-15 fl Renamed offset attribute to avoid name clash
|
||||||
|
# 2002-07-30 fl Fixed file handling
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-98 by Secret Labs AB.
|
||||||
|
# Copyright (c) 1995-96 by Fredrik Lundh.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import Image
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
from ._util import DeferredError
|
||||||
|
from .PcxImagePlugin import PcxImageFile
|
||||||
|
|
||||||
|
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return len(prefix) >= 4 and i32(prefix) == MAGIC
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Image plugin for the Intel DCX format.
|
||||||
|
|
||||||
|
|
||||||
|
class DcxImageFile(PcxImageFile):
|
||||||
|
format = "DCX"
|
||||||
|
format_description = "Intel DCX"
|
||||||
|
_close_exclusive_fp_after_loading = False
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
# Header
|
||||||
|
assert self.fp is not None
|
||||||
|
s = self.fp.read(4)
|
||||||
|
if not _accept(s):
|
||||||
|
msg = "not a DCX file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
# Component directory
|
||||||
|
self._offset = []
|
||||||
|
for i in range(1024):
|
||||||
|
offset = i32(self.fp.read(4))
|
||||||
|
if not offset:
|
||||||
|
break
|
||||||
|
self._offset.append(offset)
|
||||||
|
|
||||||
|
self._fp = self.fp
|
||||||
|
self.frame = -1
|
||||||
|
self.n_frames = len(self._offset)
|
||||||
|
self.is_animated = self.n_frames > 1
|
||||||
|
self.seek(0)
|
||||||
|
|
||||||
|
def seek(self, frame: int) -> None:
|
||||||
|
if not self._seek_check(frame):
|
||||||
|
return
|
||||||
|
if isinstance(self._fp, DeferredError):
|
||||||
|
raise self._fp.ex
|
||||||
|
self.frame = frame
|
||||||
|
self.fp = self._fp
|
||||||
|
self.fp.seek(self._offset[frame])
|
||||||
|
PcxImageFile._open(self)
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
return self.frame
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
|
||||||
|
|
||||||
|
Image.register_extension(DcxImageFile.format, ".dcx")
|
||||||
625
venv/lib/python3.11/site-packages/PIL/DdsImagePlugin.py
Normal file
625
venv/lib/python3.11/site-packages/PIL/DdsImagePlugin.py
Normal file
@@ -0,0 +1,625 @@
|
|||||||
|
"""
|
||||||
|
A Pillow plugin for .dds files (S3TC-compressed aka DXTC)
|
||||||
|
Jerome Leclanche <jerome@leclan.ch>
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
|
||||||
|
|
||||||
|
The contents of this file are hereby released in the public domain (CC0)
|
||||||
|
Full text of the CC0 license:
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from enum import IntEnum, IntFlag
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import Image, ImageFile, ImagePalette
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
from ._binary import o8
|
||||||
|
from ._binary import o32le as o32
|
||||||
|
|
||||||
|
# Magic ("DDS ")
|
||||||
|
DDS_MAGIC = 0x20534444
|
||||||
|
|
||||||
|
|
||||||
|
# DDS flags
|
||||||
|
class DDSD(IntFlag):
|
||||||
|
CAPS = 0x1
|
||||||
|
HEIGHT = 0x2
|
||||||
|
WIDTH = 0x4
|
||||||
|
PITCH = 0x8
|
||||||
|
PIXELFORMAT = 0x1000
|
||||||
|
MIPMAPCOUNT = 0x20000
|
||||||
|
LINEARSIZE = 0x80000
|
||||||
|
DEPTH = 0x800000
|
||||||
|
|
||||||
|
|
||||||
|
# DDS caps
|
||||||
|
class DDSCAPS(IntFlag):
|
||||||
|
COMPLEX = 0x8
|
||||||
|
TEXTURE = 0x1000
|
||||||
|
MIPMAP = 0x400000
|
||||||
|
|
||||||
|
|
||||||
|
class DDSCAPS2(IntFlag):
|
||||||
|
CUBEMAP = 0x200
|
||||||
|
CUBEMAP_POSITIVEX = 0x400
|
||||||
|
CUBEMAP_NEGATIVEX = 0x800
|
||||||
|
CUBEMAP_POSITIVEY = 0x1000
|
||||||
|
CUBEMAP_NEGATIVEY = 0x2000
|
||||||
|
CUBEMAP_POSITIVEZ = 0x4000
|
||||||
|
CUBEMAP_NEGATIVEZ = 0x8000
|
||||||
|
VOLUME = 0x200000
|
||||||
|
|
||||||
|
|
||||||
|
# Pixel Format
|
||||||
|
class DDPF(IntFlag):
|
||||||
|
ALPHAPIXELS = 0x1
|
||||||
|
ALPHA = 0x2
|
||||||
|
FOURCC = 0x4
|
||||||
|
PALETTEINDEXED8 = 0x20
|
||||||
|
RGB = 0x40
|
||||||
|
LUMINANCE = 0x20000
|
||||||
|
|
||||||
|
|
||||||
|
# dxgiformat.h
|
||||||
|
class DXGI_FORMAT(IntEnum):
|
||||||
|
UNKNOWN = 0
|
||||||
|
R32G32B32A32_TYPELESS = 1
|
||||||
|
R32G32B32A32_FLOAT = 2
|
||||||
|
R32G32B32A32_UINT = 3
|
||||||
|
R32G32B32A32_SINT = 4
|
||||||
|
R32G32B32_TYPELESS = 5
|
||||||
|
R32G32B32_FLOAT = 6
|
||||||
|
R32G32B32_UINT = 7
|
||||||
|
R32G32B32_SINT = 8
|
||||||
|
R16G16B16A16_TYPELESS = 9
|
||||||
|
R16G16B16A16_FLOAT = 10
|
||||||
|
R16G16B16A16_UNORM = 11
|
||||||
|
R16G16B16A16_UINT = 12
|
||||||
|
R16G16B16A16_SNORM = 13
|
||||||
|
R16G16B16A16_SINT = 14
|
||||||
|
R32G32_TYPELESS = 15
|
||||||
|
R32G32_FLOAT = 16
|
||||||
|
R32G32_UINT = 17
|
||||||
|
R32G32_SINT = 18
|
||||||
|
R32G8X24_TYPELESS = 19
|
||||||
|
D32_FLOAT_S8X24_UINT = 20
|
||||||
|
R32_FLOAT_X8X24_TYPELESS = 21
|
||||||
|
X32_TYPELESS_G8X24_UINT = 22
|
||||||
|
R10G10B10A2_TYPELESS = 23
|
||||||
|
R10G10B10A2_UNORM = 24
|
||||||
|
R10G10B10A2_UINT = 25
|
||||||
|
R11G11B10_FLOAT = 26
|
||||||
|
R8G8B8A8_TYPELESS = 27
|
||||||
|
R8G8B8A8_UNORM = 28
|
||||||
|
R8G8B8A8_UNORM_SRGB = 29
|
||||||
|
R8G8B8A8_UINT = 30
|
||||||
|
R8G8B8A8_SNORM = 31
|
||||||
|
R8G8B8A8_SINT = 32
|
||||||
|
R16G16_TYPELESS = 33
|
||||||
|
R16G16_FLOAT = 34
|
||||||
|
R16G16_UNORM = 35
|
||||||
|
R16G16_UINT = 36
|
||||||
|
R16G16_SNORM = 37
|
||||||
|
R16G16_SINT = 38
|
||||||
|
R32_TYPELESS = 39
|
||||||
|
D32_FLOAT = 40
|
||||||
|
R32_FLOAT = 41
|
||||||
|
R32_UINT = 42
|
||||||
|
R32_SINT = 43
|
||||||
|
R24G8_TYPELESS = 44
|
||||||
|
D24_UNORM_S8_UINT = 45
|
||||||
|
R24_UNORM_X8_TYPELESS = 46
|
||||||
|
X24_TYPELESS_G8_UINT = 47
|
||||||
|
R8G8_TYPELESS = 48
|
||||||
|
R8G8_UNORM = 49
|
||||||
|
R8G8_UINT = 50
|
||||||
|
R8G8_SNORM = 51
|
||||||
|
R8G8_SINT = 52
|
||||||
|
R16_TYPELESS = 53
|
||||||
|
R16_FLOAT = 54
|
||||||
|
D16_UNORM = 55
|
||||||
|
R16_UNORM = 56
|
||||||
|
R16_UINT = 57
|
||||||
|
R16_SNORM = 58
|
||||||
|
R16_SINT = 59
|
||||||
|
R8_TYPELESS = 60
|
||||||
|
R8_UNORM = 61
|
||||||
|
R8_UINT = 62
|
||||||
|
R8_SNORM = 63
|
||||||
|
R8_SINT = 64
|
||||||
|
A8_UNORM = 65
|
||||||
|
R1_UNORM = 66
|
||||||
|
R9G9B9E5_SHAREDEXP = 67
|
||||||
|
R8G8_B8G8_UNORM = 68
|
||||||
|
G8R8_G8B8_UNORM = 69
|
||||||
|
BC1_TYPELESS = 70
|
||||||
|
BC1_UNORM = 71
|
||||||
|
BC1_UNORM_SRGB = 72
|
||||||
|
BC2_TYPELESS = 73
|
||||||
|
BC2_UNORM = 74
|
||||||
|
BC2_UNORM_SRGB = 75
|
||||||
|
BC3_TYPELESS = 76
|
||||||
|
BC3_UNORM = 77
|
||||||
|
BC3_UNORM_SRGB = 78
|
||||||
|
BC4_TYPELESS = 79
|
||||||
|
BC4_UNORM = 80
|
||||||
|
BC4_SNORM = 81
|
||||||
|
BC5_TYPELESS = 82
|
||||||
|
BC5_UNORM = 83
|
||||||
|
BC5_SNORM = 84
|
||||||
|
B5G6R5_UNORM = 85
|
||||||
|
B5G5R5A1_UNORM = 86
|
||||||
|
B8G8R8A8_UNORM = 87
|
||||||
|
B8G8R8X8_UNORM = 88
|
||||||
|
R10G10B10_XR_BIAS_A2_UNORM = 89
|
||||||
|
B8G8R8A8_TYPELESS = 90
|
||||||
|
B8G8R8A8_UNORM_SRGB = 91
|
||||||
|
B8G8R8X8_TYPELESS = 92
|
||||||
|
B8G8R8X8_UNORM_SRGB = 93
|
||||||
|
BC6H_TYPELESS = 94
|
||||||
|
BC6H_UF16 = 95
|
||||||
|
BC6H_SF16 = 96
|
||||||
|
BC7_TYPELESS = 97
|
||||||
|
BC7_UNORM = 98
|
||||||
|
BC7_UNORM_SRGB = 99
|
||||||
|
AYUV = 100
|
||||||
|
Y410 = 101
|
||||||
|
Y416 = 102
|
||||||
|
NV12 = 103
|
||||||
|
P010 = 104
|
||||||
|
P016 = 105
|
||||||
|
OPAQUE_420 = 106
|
||||||
|
YUY2 = 107
|
||||||
|
Y210 = 108
|
||||||
|
Y216 = 109
|
||||||
|
NV11 = 110
|
||||||
|
AI44 = 111
|
||||||
|
IA44 = 112
|
||||||
|
P8 = 113
|
||||||
|
A8P8 = 114
|
||||||
|
B4G4R4A4_UNORM = 115
|
||||||
|
P208 = 130
|
||||||
|
V208 = 131
|
||||||
|
V408 = 132
|
||||||
|
SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
|
||||||
|
SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
|
||||||
|
|
||||||
|
|
||||||
|
class D3DFMT(IntEnum):
|
||||||
|
UNKNOWN = 0
|
||||||
|
R8G8B8 = 20
|
||||||
|
A8R8G8B8 = 21
|
||||||
|
X8R8G8B8 = 22
|
||||||
|
R5G6B5 = 23
|
||||||
|
X1R5G5B5 = 24
|
||||||
|
A1R5G5B5 = 25
|
||||||
|
A4R4G4B4 = 26
|
||||||
|
R3G3B2 = 27
|
||||||
|
A8 = 28
|
||||||
|
A8R3G3B2 = 29
|
||||||
|
X4R4G4B4 = 30
|
||||||
|
A2B10G10R10 = 31
|
||||||
|
A8B8G8R8 = 32
|
||||||
|
X8B8G8R8 = 33
|
||||||
|
G16R16 = 34
|
||||||
|
A2R10G10B10 = 35
|
||||||
|
A16B16G16R16 = 36
|
||||||
|
A8P8 = 40
|
||||||
|
P8 = 41
|
||||||
|
L8 = 50
|
||||||
|
A8L8 = 51
|
||||||
|
A4L4 = 52
|
||||||
|
V8U8 = 60
|
||||||
|
L6V5U5 = 61
|
||||||
|
X8L8V8U8 = 62
|
||||||
|
Q8W8V8U8 = 63
|
||||||
|
V16U16 = 64
|
||||||
|
A2W10V10U10 = 67
|
||||||
|
D16_LOCKABLE = 70
|
||||||
|
D32 = 71
|
||||||
|
D15S1 = 73
|
||||||
|
D24S8 = 75
|
||||||
|
D24X8 = 77
|
||||||
|
D24X4S4 = 79
|
||||||
|
D16 = 80
|
||||||
|
D32F_LOCKABLE = 82
|
||||||
|
D24FS8 = 83
|
||||||
|
D32_LOCKABLE = 84
|
||||||
|
S8_LOCKABLE = 85
|
||||||
|
L16 = 81
|
||||||
|
VERTEXDATA = 100
|
||||||
|
INDEX16 = 101
|
||||||
|
INDEX32 = 102
|
||||||
|
Q16W16V16U16 = 110
|
||||||
|
R16F = 111
|
||||||
|
G16R16F = 112
|
||||||
|
A16B16G16R16F = 113
|
||||||
|
R32F = 114
|
||||||
|
G32R32F = 115
|
||||||
|
A32B32G32R32F = 116
|
||||||
|
CxV8U8 = 117
|
||||||
|
A1 = 118
|
||||||
|
A2B10G10R10_XR_BIAS = 119
|
||||||
|
BINARYBUFFER = 199
|
||||||
|
|
||||||
|
UYVY = i32(b"UYVY")
|
||||||
|
R8G8_B8G8 = i32(b"RGBG")
|
||||||
|
YUY2 = i32(b"YUY2")
|
||||||
|
G8R8_G8B8 = i32(b"GRGB")
|
||||||
|
DXT1 = i32(b"DXT1")
|
||||||
|
DXT2 = i32(b"DXT2")
|
||||||
|
DXT3 = i32(b"DXT3")
|
||||||
|
DXT4 = i32(b"DXT4")
|
||||||
|
DXT5 = i32(b"DXT5")
|
||||||
|
DX10 = i32(b"DX10")
|
||||||
|
BC4S = i32(b"BC4S")
|
||||||
|
BC4U = i32(b"BC4U")
|
||||||
|
BC5S = i32(b"BC5S")
|
||||||
|
BC5U = i32(b"BC5U")
|
||||||
|
ATI1 = i32(b"ATI1")
|
||||||
|
ATI2 = i32(b"ATI2")
|
||||||
|
MULTI2_ARGB8 = i32(b"MET1")
|
||||||
|
|
||||||
|
|
||||||
|
# Backward compatibility layer
|
||||||
|
module = sys.modules[__name__]
|
||||||
|
for item in DDSD:
|
||||||
|
assert item.name is not None
|
||||||
|
setattr(module, f"DDSD_{item.name}", item.value)
|
||||||
|
for item1 in DDSCAPS:
|
||||||
|
assert item1.name is not None
|
||||||
|
setattr(module, f"DDSCAPS_{item1.name}", item1.value)
|
||||||
|
for item2 in DDSCAPS2:
|
||||||
|
assert item2.name is not None
|
||||||
|
setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
|
||||||
|
for item3 in DDPF:
|
||||||
|
assert item3.name is not None
|
||||||
|
setattr(module, f"DDPF_{item3.name}", item3.value)
|
||||||
|
|
||||||
|
DDS_FOURCC = DDPF.FOURCC
|
||||||
|
DDS_RGB = DDPF.RGB
|
||||||
|
DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
|
||||||
|
DDS_LUMINANCE = DDPF.LUMINANCE
|
||||||
|
DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
|
||||||
|
DDS_ALPHA = DDPF.ALPHA
|
||||||
|
DDS_PAL8 = DDPF.PALETTEINDEXED8
|
||||||
|
|
||||||
|
DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
|
||||||
|
DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
|
||||||
|
DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
|
||||||
|
DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
|
||||||
|
DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
|
||||||
|
|
||||||
|
DDS_HEIGHT = DDSD.HEIGHT
|
||||||
|
DDS_WIDTH = DDSD.WIDTH
|
||||||
|
|
||||||
|
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
|
||||||
|
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
|
||||||
|
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
|
||||||
|
|
||||||
|
DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
|
||||||
|
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
|
||||||
|
DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
|
||||||
|
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
|
||||||
|
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
|
||||||
|
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
|
||||||
|
|
||||||
|
DXT1_FOURCC = D3DFMT.DXT1
|
||||||
|
DXT3_FOURCC = D3DFMT.DXT3
|
||||||
|
DXT5_FOURCC = D3DFMT.DXT5
|
||||||
|
|
||||||
|
DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
|
||||||
|
DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
|
||||||
|
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
|
||||||
|
DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
|
||||||
|
DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
|
||||||
|
DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
|
||||||
|
DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
|
||||||
|
DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
|
||||||
|
DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
|
||||||
|
DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
|
||||||
|
DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
|
||||||
|
|
||||||
|
|
||||||
|
class DdsImageFile(ImageFile.ImageFile):
|
||||||
|
format = "DDS"
|
||||||
|
format_description = "DirectDraw Surface"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
if not _accept(self.fp.read(4)):
|
||||||
|
msg = "not a DDS file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
(header_size,) = struct.unpack("<I", self.fp.read(4))
|
||||||
|
if header_size != 124:
|
||||||
|
msg = f"Unsupported header size {repr(header_size)}"
|
||||||
|
raise OSError(msg)
|
||||||
|
header = self.fp.read(header_size - 4)
|
||||||
|
if len(header) != 120:
|
||||||
|
msg = f"Incomplete header: {len(header)} bytes"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
flags, height, width = struct.unpack("<3I", header[:12])
|
||||||
|
self._size = (width, height)
|
||||||
|
extents = (0, 0) + self.size
|
||||||
|
|
||||||
|
pitch, depth, mipmaps = struct.unpack("<3I", header[12:24])
|
||||||
|
struct.unpack("<11I", header[24:68]) # reserved
|
||||||
|
|
||||||
|
# pixel format
|
||||||
|
pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header[68:84])
|
||||||
|
n = 0
|
||||||
|
rawmode = None
|
||||||
|
if pfflags & DDPF.RGB:
|
||||||
|
# Texture contains uncompressed RGB data
|
||||||
|
if pfflags & DDPF.ALPHAPIXELS:
|
||||||
|
self._mode = "RGBA"
|
||||||
|
mask_count = 4
|
||||||
|
else:
|
||||||
|
self._mode = "RGB"
|
||||||
|
mask_count = 3
|
||||||
|
|
||||||
|
masks = struct.unpack(f"<{mask_count}I", header[84 : 84 + mask_count * 4])
|
||||||
|
self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
|
||||||
|
return
|
||||||
|
elif pfflags & DDPF.LUMINANCE:
|
||||||
|
if bitcount == 8:
|
||||||
|
self._mode = "L"
|
||||||
|
elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS:
|
||||||
|
self._mode = "LA"
|
||||||
|
else:
|
||||||
|
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
|
||||||
|
raise OSError(msg)
|
||||||
|
elif pfflags & DDPF.PALETTEINDEXED8:
|
||||||
|
self._mode = "P"
|
||||||
|
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
||||||
|
self.palette.mode = "RGBA"
|
||||||
|
elif pfflags & DDPF.FOURCC:
|
||||||
|
offset = header_size + 4
|
||||||
|
if fourcc == D3DFMT.DXT1:
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "DXT1"
|
||||||
|
n = 1
|
||||||
|
elif fourcc == D3DFMT.DXT3:
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "DXT3"
|
||||||
|
n = 2
|
||||||
|
elif fourcc == D3DFMT.DXT5:
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "DXT5"
|
||||||
|
n = 3
|
||||||
|
elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
|
||||||
|
self._mode = "L"
|
||||||
|
self.pixel_format = "BC4"
|
||||||
|
n = 4
|
||||||
|
elif fourcc == D3DFMT.BC5S:
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.pixel_format = "BC5S"
|
||||||
|
n = 5
|
||||||
|
elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.pixel_format = "BC5"
|
||||||
|
n = 5
|
||||||
|
elif fourcc == D3DFMT.DX10:
|
||||||
|
offset += 20
|
||||||
|
# ignoring flags which pertain to volume textures and cubemaps
|
||||||
|
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
|
||||||
|
self.fp.read(16)
|
||||||
|
if dxgi_format in (
|
||||||
|
DXGI_FORMAT.BC1_UNORM,
|
||||||
|
DXGI_FORMAT.BC1_TYPELESS,
|
||||||
|
):
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "BC1"
|
||||||
|
n = 1
|
||||||
|
elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM):
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "BC2"
|
||||||
|
n = 2
|
||||||
|
elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM):
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "BC3"
|
||||||
|
n = 3
|
||||||
|
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
|
||||||
|
self._mode = "L"
|
||||||
|
self.pixel_format = "BC4"
|
||||||
|
n = 4
|
||||||
|
elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM):
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.pixel_format = "BC5"
|
||||||
|
n = 5
|
||||||
|
elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.pixel_format = "BC5S"
|
||||||
|
n = 5
|
||||||
|
elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.pixel_format = "BC6H"
|
||||||
|
n = 6
|
||||||
|
elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.pixel_format = "BC6HS"
|
||||||
|
n = 6
|
||||||
|
elif dxgi_format in (
|
||||||
|
DXGI_FORMAT.BC7_TYPELESS,
|
||||||
|
DXGI_FORMAT.BC7_UNORM,
|
||||||
|
DXGI_FORMAT.BC7_UNORM_SRGB,
|
||||||
|
):
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.pixel_format = "BC7"
|
||||||
|
n = 7
|
||||||
|
if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB:
|
||||||
|
self.info["gamma"] = 1 / 2.2
|
||||||
|
elif dxgi_format in (
|
||||||
|
DXGI_FORMAT.R8G8B8A8_TYPELESS,
|
||||||
|
DXGI_FORMAT.R8G8B8A8_UNORM,
|
||||||
|
DXGI_FORMAT.R8G8B8A8_UNORM_SRGB,
|
||||||
|
):
|
||||||
|
self._mode = "RGBA"
|
||||||
|
if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
|
||||||
|
self.info["gamma"] = 1 / 2.2
|
||||||
|
else:
|
||||||
|
msg = f"Unimplemented DXGI format {dxgi_format}"
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
else:
|
||||||
|
msg = f"Unimplemented pixel format {repr(fourcc)}"
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
else:
|
||||||
|
msg = f"Unknown pixel format flags {pfflags}"
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
if n:
|
||||||
|
self.tile = [
|
||||||
|
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
|
||||||
|
|
||||||
|
def load_seek(self, pos: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DdsRgbDecoder(ImageFile.PyDecoder):
|
||||||
|
_pulls_fd = True
|
||||||
|
|
||||||
|
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||||
|
assert self.fd is not None
|
||||||
|
bitcount, masks = self.args
|
||||||
|
|
||||||
|
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
|
||||||
|
# Calculate how many zeros each mask is padded with
|
||||||
|
mask_offsets = []
|
||||||
|
# And the maximum value of each channel without the padding
|
||||||
|
mask_totals = []
|
||||||
|
for mask in masks:
|
||||||
|
offset = 0
|
||||||
|
if mask != 0:
|
||||||
|
while mask >> (offset + 1) << (offset + 1) == mask:
|
||||||
|
offset += 1
|
||||||
|
mask_offsets.append(offset)
|
||||||
|
mask_totals.append(mask >> offset)
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
bytecount = bitcount // 8
|
||||||
|
dest_length = self.state.xsize * self.state.ysize * len(masks)
|
||||||
|
while len(data) < dest_length:
|
||||||
|
value = int.from_bytes(self.fd.read(bytecount), "little")
|
||||||
|
for i, mask in enumerate(masks):
|
||||||
|
masked_value = value & mask
|
||||||
|
# Remove the zero padding, and scale it to 8 bits
|
||||||
|
data += o8(
|
||||||
|
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
|
||||||
|
if mask_totals[i]
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
self.set_as_raw(data)
|
||||||
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
||||||
|
msg = f"cannot write mode {im.mode} as DDS"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
|
||||||
|
bitcount = len(im.getbands()) * 8
|
||||||
|
pixel_format = im.encoderinfo.get("pixel_format")
|
||||||
|
args: tuple[int] | str
|
||||||
|
if pixel_format:
|
||||||
|
codec_name = "bcn"
|
||||||
|
flags |= DDSD.LINEARSIZE
|
||||||
|
pitch = (im.width + 3) * 4
|
||||||
|
rgba_mask = [0, 0, 0, 0]
|
||||||
|
pixel_flags = DDPF.FOURCC
|
||||||
|
if pixel_format == "DXT1":
|
||||||
|
fourcc = D3DFMT.DXT1
|
||||||
|
args = (1,)
|
||||||
|
elif pixel_format == "DXT3":
|
||||||
|
fourcc = D3DFMT.DXT3
|
||||||
|
args = (2,)
|
||||||
|
elif pixel_format == "DXT5":
|
||||||
|
fourcc = D3DFMT.DXT5
|
||||||
|
args = (3,)
|
||||||
|
else:
|
||||||
|
fourcc = D3DFMT.DX10
|
||||||
|
if pixel_format == "BC2":
|
||||||
|
args = (2,)
|
||||||
|
dxgi_format = DXGI_FORMAT.BC2_TYPELESS
|
||||||
|
elif pixel_format == "BC3":
|
||||||
|
args = (3,)
|
||||||
|
dxgi_format = DXGI_FORMAT.BC3_TYPELESS
|
||||||
|
elif pixel_format == "BC5":
|
||||||
|
args = (5,)
|
||||||
|
dxgi_format = DXGI_FORMAT.BC5_TYPELESS
|
||||||
|
if im.mode != "RGB":
|
||||||
|
msg = "only RGB mode can be written as BC5"
|
||||||
|
raise OSError(msg)
|
||||||
|
else:
|
||||||
|
msg = f"cannot write pixel format {pixel_format}"
|
||||||
|
raise OSError(msg)
|
||||||
|
else:
|
||||||
|
codec_name = "raw"
|
||||||
|
flags |= DDSD.PITCH
|
||||||
|
pitch = (im.width * bitcount + 7) // 8
|
||||||
|
|
||||||
|
alpha = im.mode[-1] == "A"
|
||||||
|
if im.mode[0] == "L":
|
||||||
|
pixel_flags = DDPF.LUMINANCE
|
||||||
|
args = im.mode
|
||||||
|
if alpha:
|
||||||
|
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
|
||||||
|
else:
|
||||||
|
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
|
||||||
|
else:
|
||||||
|
pixel_flags = DDPF.RGB
|
||||||
|
args = im.mode[::-1]
|
||||||
|
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
|
||||||
|
|
||||||
|
if alpha:
|
||||||
|
r, g, b, a = im.split()
|
||||||
|
im = Image.merge("RGBA", (a, r, g, b))
|
||||||
|
if alpha:
|
||||||
|
pixel_flags |= DDPF.ALPHAPIXELS
|
||||||
|
rgba_mask.append(0xFF000000 if alpha else 0)
|
||||||
|
|
||||||
|
fourcc = D3DFMT.UNKNOWN
|
||||||
|
fp.write(
|
||||||
|
o32(DDS_MAGIC)
|
||||||
|
+ struct.pack(
|
||||||
|
"<7I",
|
||||||
|
124, # header size
|
||||||
|
flags, # flags
|
||||||
|
im.height,
|
||||||
|
im.width,
|
||||||
|
pitch,
|
||||||
|
0, # depth
|
||||||
|
0, # mipmaps
|
||||||
|
)
|
||||||
|
+ struct.pack("11I", *((0,) * 11)) # reserved
|
||||||
|
# pfsize, pfflags, fourcc, bitcount
|
||||||
|
+ struct.pack("<4I", 32, pixel_flags, fourcc, bitcount)
|
||||||
|
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
|
||||||
|
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
|
||||||
|
)
|
||||||
|
if fourcc == D3DFMT.DX10:
|
||||||
|
fp.write(
|
||||||
|
# dxgi_format, 2D resource, misc, array size, straight alpha
|
||||||
|
struct.pack("<5I", dxgi_format, 3, 0, 0, 1)
|
||||||
|
)
|
||||||
|
ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)])
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(b"DDS ")
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
|
||||||
|
Image.register_decoder("dds_rgb", DdsRgbDecoder)
|
||||||
|
Image.register_save(DdsImageFile.format, _save)
|
||||||
|
Image.register_extension(DdsImageFile.format, ".dds")
|
||||||
481
venv/lib/python3.11/site-packages/PIL/EpsImagePlugin.py
Normal file
481
venv/lib/python3.11/site-packages/PIL/EpsImagePlugin.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# EPS file handling
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1995-09-01 fl Created (0.1)
|
||||||
|
# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2)
|
||||||
|
# 1996-08-22 fl Don't choke on floating point BoundingBox values
|
||||||
|
# 1996-08-23 fl Handle files from Macintosh (0.3)
|
||||||
|
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
|
||||||
|
# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5)
|
||||||
|
# 2014-05-07 e Handling of EPS with binary preview and fixed resolution
|
||||||
|
# resizing
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-2003 by Secret Labs AB.
|
||||||
|
# Copyright (c) 1995-2003 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||||
|
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||||
|
|
||||||
|
gs_binary: str | bool | None = None
|
||||||
|
gs_windows_binary = None
|
||||||
|
|
||||||
|
|
||||||
|
def has_ghostscript() -> bool:
|
||||||
|
global gs_binary, gs_windows_binary
|
||||||
|
if gs_binary is None:
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
if gs_windows_binary is None:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
for binary in ("gswin32c", "gswin64c", "gs"):
|
||||||
|
if shutil.which(binary) is not None:
|
||||||
|
gs_windows_binary = binary
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
gs_windows_binary = False
|
||||||
|
gs_binary = gs_windows_binary
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
||||||
|
gs_binary = "gs"
|
||||||
|
except OSError:
|
||||||
|
gs_binary = False
|
||||||
|
return gs_binary is not False
|
||||||
|
|
||||||
|
|
||||||
|
def Ghostscript(
|
||||||
|
tile: list[ImageFile._Tile],
|
||||||
|
size: tuple[int, int],
|
||||||
|
fp: IO[bytes],
|
||||||
|
scale: int = 1,
|
||||||
|
transparency: bool = False,
|
||||||
|
) -> Image.core.ImagingCore:
|
||||||
|
"""Render an image using Ghostscript"""
|
||||||
|
global gs_binary
|
||||||
|
if not has_ghostscript():
|
||||||
|
msg = "Unable to locate Ghostscript on paths"
|
||||||
|
raise OSError(msg)
|
||||||
|
assert isinstance(gs_binary, str)
|
||||||
|
|
||||||
|
# Unpack decoder tile
|
||||||
|
args = tile[0].args
|
||||||
|
assert isinstance(args, tuple)
|
||||||
|
length, bbox = args
|
||||||
|
|
||||||
|
# Hack to support hi-res rendering
|
||||||
|
scale = int(scale) or 1
|
||||||
|
width = size[0] * scale
|
||||||
|
height = size[1] * scale
|
||||||
|
# resolution is dependent on bbox and size
|
||||||
|
res_x = 72.0 * width / (bbox[2] - bbox[0])
|
||||||
|
res_y = 72.0 * height / (bbox[3] - bbox[1])
|
||||||
|
|
||||||
|
out_fd, outfile = tempfile.mkstemp()
|
||||||
|
os.close(out_fd)
|
||||||
|
|
||||||
|
infile_temp = None
|
||||||
|
if hasattr(fp, "name") and os.path.exists(fp.name):
|
||||||
|
infile = fp.name
|
||||||
|
else:
|
||||||
|
in_fd, infile_temp = tempfile.mkstemp()
|
||||||
|
os.close(in_fd)
|
||||||
|
infile = infile_temp
|
||||||
|
|
||||||
|
# Ignore length and offset!
|
||||||
|
# Ghostscript can read it
|
||||||
|
# Copy whole file to read in Ghostscript
|
||||||
|
with open(infile_temp, "wb") as f:
|
||||||
|
# fetch length of fp
|
||||||
|
fp.seek(0, io.SEEK_END)
|
||||||
|
fsize = fp.tell()
|
||||||
|
# ensure start position
|
||||||
|
# go back
|
||||||
|
fp.seek(0)
|
||||||
|
lengthfile = fsize
|
||||||
|
while lengthfile > 0:
|
||||||
|
s = fp.read(min(lengthfile, 100 * 1024))
|
||||||
|
if not s:
|
||||||
|
break
|
||||||
|
lengthfile -= len(s)
|
||||||
|
f.write(s)
|
||||||
|
|
||||||
|
if transparency:
|
||||||
|
# "RGBA"
|
||||||
|
device = "pngalpha"
|
||||||
|
else:
|
||||||
|
# "pnmraw" automatically chooses between
|
||||||
|
# PBM ("1"), PGM ("L"), and PPM ("RGB").
|
||||||
|
device = "pnmraw"
|
||||||
|
|
||||||
|
# Build Ghostscript command
|
||||||
|
command = [
|
||||||
|
gs_binary,
|
||||||
|
"-q", # quiet mode
|
||||||
|
f"-g{width:d}x{height:d}", # set output geometry (pixels)
|
||||||
|
f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch)
|
||||||
|
"-dBATCH", # exit after processing
|
||||||
|
"-dNOPAUSE", # don't pause between pages
|
||||||
|
"-dSAFER", # safe mode
|
||||||
|
f"-sDEVICE={device}",
|
||||||
|
f"-sOutputFile={outfile}", # output file
|
||||||
|
# adjust for image origin
|
||||||
|
"-c",
|
||||||
|
f"{-bbox[0]} {-bbox[1]} translate",
|
||||||
|
"-f",
|
||||||
|
infile, # input file
|
||||||
|
# showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
|
||||||
|
"-c",
|
||||||
|
"showpage",
|
||||||
|
]
|
||||||
|
|
||||||
|
# push data through Ghostscript
|
||||||
|
try:
|
||||||
|
startupinfo = None
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
subprocess.check_call(command, startupinfo=startupinfo)
|
||||||
|
with Image.open(outfile) as out_im:
|
||||||
|
out_im.load()
|
||||||
|
return out_im.im.copy()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(outfile)
|
||||||
|
if infile_temp:
|
||||||
|
os.unlink(infile_temp)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(b"%!PS") or (
|
||||||
|
len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Image plugin for Encapsulated PostScript. This plugin supports only
|
||||||
|
# a few variants of this format.
|
||||||
|
|
||||||
|
|
||||||
|
class EpsImageFile(ImageFile.ImageFile):
|
||||||
|
"""EPS File Parser for the Python Imaging Library"""
|
||||||
|
|
||||||
|
format = "EPS"
|
||||||
|
format_description = "Encapsulated Postscript"
|
||||||
|
|
||||||
|
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
length, offset = self._find_offset(self.fp)
|
||||||
|
|
||||||
|
# go to offset - start of "%!PS"
|
||||||
|
self.fp.seek(offset)
|
||||||
|
|
||||||
|
self._mode = "RGB"
|
||||||
|
|
||||||
|
# When reading header comments, the first comment is used.
|
||||||
|
# When reading trailer comments, the last comment is used.
|
||||||
|
bounding_box: list[int] | None = None
|
||||||
|
imagedata_size: tuple[int, int] | None = None
|
||||||
|
|
||||||
|
byte_arr = bytearray(255)
|
||||||
|
bytes_mv = memoryview(byte_arr)
|
||||||
|
bytes_read = 0
|
||||||
|
reading_header_comments = True
|
||||||
|
reading_trailer_comments = False
|
||||||
|
trailer_reached = False
|
||||||
|
|
||||||
|
def check_required_header_comments() -> None:
|
||||||
|
"""
|
||||||
|
The EPS specification requires that some headers exist.
|
||||||
|
This should be checked when the header comments formally end,
|
||||||
|
when image data starts, or when the file ends, whichever comes first.
|
||||||
|
"""
|
||||||
|
if "PS-Adobe" not in self.info:
|
||||||
|
msg = 'EPS header missing "%!PS-Adobe" comment'
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
if "BoundingBox" not in self.info:
|
||||||
|
msg = 'EPS header missing "%%BoundingBox" comment'
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
def read_comment(s: str) -> bool:
|
||||||
|
nonlocal bounding_box, reading_trailer_comments
|
||||||
|
try:
|
||||||
|
m = split.match(s)
|
||||||
|
except re.error as e:
|
||||||
|
msg = "not an EPS file"
|
||||||
|
raise SyntaxError(msg) from e
|
||||||
|
|
||||||
|
if not m:
|
||||||
|
return False
|
||||||
|
|
||||||
|
k, v = m.group(1, 2)
|
||||||
|
self.info[k] = v
|
||||||
|
if k == "BoundingBox":
|
||||||
|
if v == "(atend)":
|
||||||
|
reading_trailer_comments = True
|
||||||
|
elif not bounding_box or (trailer_reached and reading_trailer_comments):
|
||||||
|
try:
|
||||||
|
# Note: The DSC spec says that BoundingBox
|
||||||
|
# fields should be integers, but some drivers
|
||||||
|
# put floating point values there anyway.
|
||||||
|
bounding_box = [int(float(i)) for i in v.split()]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
while True:
|
||||||
|
byte = self.fp.read(1)
|
||||||
|
if byte == b"":
|
||||||
|
# if we didn't read a byte we must be at the end of the file
|
||||||
|
if bytes_read == 0:
|
||||||
|
if reading_header_comments:
|
||||||
|
check_required_header_comments()
|
||||||
|
break
|
||||||
|
elif byte in b"\r\n":
|
||||||
|
# if we read a line ending character, ignore it and parse what
|
||||||
|
# we have already read. if we haven't read any other characters,
|
||||||
|
# continue reading
|
||||||
|
if bytes_read == 0:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# ASCII/hexadecimal lines in an EPS file must not exceed
|
||||||
|
# 255 characters, not including line ending characters
|
||||||
|
if bytes_read >= 255:
|
||||||
|
# only enforce this for lines starting with a "%",
|
||||||
|
# otherwise assume it's binary data
|
||||||
|
if byte_arr[0] == ord("%"):
|
||||||
|
msg = "not an EPS file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
else:
|
||||||
|
if reading_header_comments:
|
||||||
|
check_required_header_comments()
|
||||||
|
reading_header_comments = False
|
||||||
|
# reset bytes_read so we can keep reading
|
||||||
|
# data until the end of the line
|
||||||
|
bytes_read = 0
|
||||||
|
byte_arr[bytes_read] = byte[0]
|
||||||
|
bytes_read += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if reading_header_comments:
|
||||||
|
# Load EPS header
|
||||||
|
|
||||||
|
# if this line doesn't start with a "%",
|
||||||
|
# or does start with "%%EndComments",
|
||||||
|
# then we've reached the end of the header/comments
|
||||||
|
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
|
||||||
|
check_required_header_comments()
|
||||||
|
reading_header_comments = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||||
|
if not read_comment(s):
|
||||||
|
m = field.match(s)
|
||||||
|
if m:
|
||||||
|
k = m.group(1)
|
||||||
|
if k.startswith("PS-Adobe"):
|
||||||
|
self.info["PS-Adobe"] = k[9:]
|
||||||
|
else:
|
||||||
|
self.info[k] = ""
|
||||||
|
elif s[0] == "%":
|
||||||
|
# handle non-DSC PostScript comments that some
|
||||||
|
# tools mistakenly put in the Comments section
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
msg = "bad EPS header"
|
||||||
|
raise OSError(msg)
|
||||||
|
elif bytes_mv[:11] == b"%ImageData:":
|
||||||
|
# Check for an "ImageData" descriptor
|
||||||
|
# https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
|
||||||
|
|
||||||
|
# If we've already read an "ImageData" descriptor,
|
||||||
|
# don't read another one.
|
||||||
|
if imagedata_size:
|
||||||
|
bytes_read = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Values:
|
||||||
|
# columns
|
||||||
|
# rows
|
||||||
|
# bit depth (1 or 8)
|
||||||
|
# mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
|
||||||
|
# number of padding channels
|
||||||
|
# block size (number of bytes per row per channel)
|
||||||
|
# binary/ascii (1: binary, 2: ascii)
|
||||||
|
# data start identifier (the image data follows after a single line
|
||||||
|
# consisting only of this quoted value)
|
||||||
|
image_data_values = byte_arr[11:bytes_read].split(None, 7)
|
||||||
|
columns, rows, bit_depth, mode_id = (
|
||||||
|
int(value) for value in image_data_values[:4]
|
||||||
|
)
|
||||||
|
|
||||||
|
if bit_depth == 1:
|
||||||
|
self._mode = "1"
|
||||||
|
elif bit_depth == 8:
|
||||||
|
try:
|
||||||
|
self._mode = self.mode_map[mode_id]
|
||||||
|
except ValueError:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Parse the columns and rows after checking the bit depth and mode
|
||||||
|
# in case the bit depth and/or mode are invalid.
|
||||||
|
imagedata_size = columns, rows
|
||||||
|
elif bytes_mv[:5] == b"%%EOF":
|
||||||
|
break
|
||||||
|
elif trailer_reached and reading_trailer_comments:
|
||||||
|
# Load EPS trailer
|
||||||
|
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||||
|
read_comment(s)
|
||||||
|
elif bytes_mv[:9] == b"%%Trailer":
|
||||||
|
trailer_reached = True
|
||||||
|
elif bytes_mv[:14] == b"%%BeginBinary:":
|
||||||
|
bytecount = int(byte_arr[14:bytes_read])
|
||||||
|
self.fp.seek(bytecount, os.SEEK_CUR)
|
||||||
|
bytes_read = 0
|
||||||
|
|
||||||
|
# A "BoundingBox" is always required,
|
||||||
|
# even if an "ImageData" descriptor size exists.
|
||||||
|
if not bounding_box:
|
||||||
|
msg = "cannot determine EPS bounding box"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# An "ImageData" size takes precedence over the "BoundingBox".
|
||||||
|
self._size = imagedata_size or (
|
||||||
|
bounding_box[2] - bounding_box[0],
|
||||||
|
bounding_box[3] - bounding_box[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tile = [
|
||||||
|
ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
|
||||||
|
]
|
||||||
|
|
||||||
|
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
|
||||||
|
s = fp.read(4)
|
||||||
|
|
||||||
|
if s == b"%!PS":
|
||||||
|
# for HEAD without binary preview
|
||||||
|
fp.seek(0, io.SEEK_END)
|
||||||
|
length = fp.tell()
|
||||||
|
offset = 0
|
||||||
|
elif i32(s) == 0xC6D3D0C5:
|
||||||
|
# FIX for: Some EPS file not handled correctly / issue #302
|
||||||
|
# EPS can contain binary data
|
||||||
|
# or start directly with latin coding
|
||||||
|
# more info see:
|
||||||
|
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
|
||||||
|
s = fp.read(8)
|
||||||
|
offset = i32(s)
|
||||||
|
length = i32(s, 4)
|
||||||
|
else:
|
||||||
|
msg = "not an EPS file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
return length, offset
|
||||||
|
|
||||||
|
def load(
|
||||||
|
self, scale: int = 1, transparency: bool = False
|
||||||
|
) -> Image.core.PixelAccess | None:
|
||||||
|
# Load EPS via Ghostscript
|
||||||
|
if self.tile:
|
||||||
|
assert self.fp is not None
|
||||||
|
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
|
||||||
|
self._mode = self.im.mode
|
||||||
|
self._size = self.im.size
|
||||||
|
self.tile = []
|
||||||
|
return Image.Image.load(self)
|
||||||
|
|
||||||
|
def load_seek(self, pos: int) -> None:
|
||||||
|
# we can't incrementally load, so force ImageFile.parser to
|
||||||
|
# use our custom load method by defining this method.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
|
||||||
|
"""EPS Writer for the Python Imaging Library."""
|
||||||
|
|
||||||
|
# make sure image data is available
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
# determine PostScript image mode
|
||||||
|
if im.mode == "L":
|
||||||
|
operator = (8, 1, b"image")
|
||||||
|
elif im.mode == "RGB":
|
||||||
|
operator = (8, 3, b"false 3 colorimage")
|
||||||
|
elif im.mode == "CMYK":
|
||||||
|
operator = (8, 4, b"false 4 colorimage")
|
||||||
|
else:
|
||||||
|
msg = "image mode is not supported"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
if eps:
|
||||||
|
# write EPS header
|
||||||
|
fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
|
||||||
|
fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
|
||||||
|
# fp.write("%%CreationDate: %s"...)
|
||||||
|
fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
|
||||||
|
fp.write(b"%%Pages: 1\n")
|
||||||
|
fp.write(b"%%EndComments\n")
|
||||||
|
fp.write(b"%%Page: 1 1\n")
|
||||||
|
fp.write(b"%%ImageData: %d %d " % im.size)
|
||||||
|
fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
|
||||||
|
|
||||||
|
# image header
|
||||||
|
fp.write(b"gsave\n")
|
||||||
|
fp.write(b"10 dict begin\n")
|
||||||
|
fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
|
||||||
|
fp.write(b"%d %d scale\n" % im.size)
|
||||||
|
fp.write(b"%d %d 8\n" % im.size) # <= bits
|
||||||
|
fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
|
||||||
|
fp.write(b"{ currentfile buf readhexstring pop } bind\n")
|
||||||
|
fp.write(operator[2] + b"\n")
|
||||||
|
if hasattr(fp, "flush"):
|
||||||
|
fp.flush()
|
||||||
|
|
||||||
|
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
|
||||||
|
|
||||||
|
fp.write(b"\n%%%%EndBinary\n")
|
||||||
|
fp.write(b"grestore end\n")
|
||||||
|
if hasattr(fp, "flush"):
|
||||||
|
fp.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
|
||||||
|
|
||||||
|
Image.register_save(EpsImageFile.format, _save)
|
||||||
|
|
||||||
|
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
|
||||||
|
|
||||||
|
Image.register_mime(EpsImageFile.format, "application/postscript")
|
||||||
384
venv/lib/python3.11/site-packages/PIL/ExifTags.py
Normal file
384
venv/lib/python3.11/site-packages/PIL/ExifTags.py
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# EXIF tags
|
||||||
|
#
|
||||||
|
# Copyright (c) 2003 by Secret Labs AB
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module provides constants and clear-text names for various
|
||||||
|
well-known EXIF tags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Base(IntEnum):
|
||||||
|
# possibly incomplete
|
||||||
|
InteropIndex = 0x0001
|
||||||
|
ProcessingSoftware = 0x000B
|
||||||
|
NewSubfileType = 0x00FE
|
||||||
|
SubfileType = 0x00FF
|
||||||
|
ImageWidth = 0x0100
|
||||||
|
ImageLength = 0x0101
|
||||||
|
BitsPerSample = 0x0102
|
||||||
|
Compression = 0x0103
|
||||||
|
PhotometricInterpretation = 0x0106
|
||||||
|
Thresholding = 0x0107
|
||||||
|
CellWidth = 0x0108
|
||||||
|
CellLength = 0x0109
|
||||||
|
FillOrder = 0x010A
|
||||||
|
DocumentName = 0x010D
|
||||||
|
ImageDescription = 0x010E
|
||||||
|
Make = 0x010F
|
||||||
|
Model = 0x0110
|
||||||
|
StripOffsets = 0x0111
|
||||||
|
Orientation = 0x0112
|
||||||
|
SamplesPerPixel = 0x0115
|
||||||
|
RowsPerStrip = 0x0116
|
||||||
|
StripByteCounts = 0x0117
|
||||||
|
MinSampleValue = 0x0118
|
||||||
|
MaxSampleValue = 0x0119
|
||||||
|
XResolution = 0x011A
|
||||||
|
YResolution = 0x011B
|
||||||
|
PlanarConfiguration = 0x011C
|
||||||
|
PageName = 0x011D
|
||||||
|
FreeOffsets = 0x0120
|
||||||
|
FreeByteCounts = 0x0121
|
||||||
|
GrayResponseUnit = 0x0122
|
||||||
|
GrayResponseCurve = 0x0123
|
||||||
|
T4Options = 0x0124
|
||||||
|
T6Options = 0x0125
|
||||||
|
ResolutionUnit = 0x0128
|
||||||
|
PageNumber = 0x0129
|
||||||
|
TransferFunction = 0x012D
|
||||||
|
Software = 0x0131
|
||||||
|
DateTime = 0x0132
|
||||||
|
Artist = 0x013B
|
||||||
|
HostComputer = 0x013C
|
||||||
|
Predictor = 0x013D
|
||||||
|
WhitePoint = 0x013E
|
||||||
|
PrimaryChromaticities = 0x013F
|
||||||
|
ColorMap = 0x0140
|
||||||
|
HalftoneHints = 0x0141
|
||||||
|
TileWidth = 0x0142
|
||||||
|
TileLength = 0x0143
|
||||||
|
TileOffsets = 0x0144
|
||||||
|
TileByteCounts = 0x0145
|
||||||
|
SubIFDs = 0x014A
|
||||||
|
InkSet = 0x014C
|
||||||
|
InkNames = 0x014D
|
||||||
|
NumberOfInks = 0x014E
|
||||||
|
DotRange = 0x0150
|
||||||
|
TargetPrinter = 0x0151
|
||||||
|
ExtraSamples = 0x0152
|
||||||
|
SampleFormat = 0x0153
|
||||||
|
SMinSampleValue = 0x0154
|
||||||
|
SMaxSampleValue = 0x0155
|
||||||
|
TransferRange = 0x0156
|
||||||
|
ClipPath = 0x0157
|
||||||
|
XClipPathUnits = 0x0158
|
||||||
|
YClipPathUnits = 0x0159
|
||||||
|
Indexed = 0x015A
|
||||||
|
JPEGTables = 0x015B
|
||||||
|
OPIProxy = 0x015F
|
||||||
|
JPEGProc = 0x0200
|
||||||
|
JpegIFOffset = 0x0201
|
||||||
|
JpegIFByteCount = 0x0202
|
||||||
|
JpegRestartInterval = 0x0203
|
||||||
|
JpegLosslessPredictors = 0x0205
|
||||||
|
JpegPointTransforms = 0x0206
|
||||||
|
JpegQTables = 0x0207
|
||||||
|
JpegDCTables = 0x0208
|
||||||
|
JpegACTables = 0x0209
|
||||||
|
YCbCrCoefficients = 0x0211
|
||||||
|
YCbCrSubSampling = 0x0212
|
||||||
|
YCbCrPositioning = 0x0213
|
||||||
|
ReferenceBlackWhite = 0x0214
|
||||||
|
XMLPacket = 0x02BC
|
||||||
|
RelatedImageFileFormat = 0x1000
|
||||||
|
RelatedImageWidth = 0x1001
|
||||||
|
RelatedImageLength = 0x1002
|
||||||
|
Rating = 0x4746
|
||||||
|
RatingPercent = 0x4749
|
||||||
|
ImageID = 0x800D
|
||||||
|
CFARepeatPatternDim = 0x828D
|
||||||
|
BatteryLevel = 0x828F
|
||||||
|
Copyright = 0x8298
|
||||||
|
ExposureTime = 0x829A
|
||||||
|
FNumber = 0x829D
|
||||||
|
IPTCNAA = 0x83BB
|
||||||
|
ImageResources = 0x8649
|
||||||
|
ExifOffset = 0x8769
|
||||||
|
InterColorProfile = 0x8773
|
||||||
|
ExposureProgram = 0x8822
|
||||||
|
SpectralSensitivity = 0x8824
|
||||||
|
GPSInfo = 0x8825
|
||||||
|
ISOSpeedRatings = 0x8827
|
||||||
|
OECF = 0x8828
|
||||||
|
Interlace = 0x8829
|
||||||
|
TimeZoneOffset = 0x882A
|
||||||
|
SelfTimerMode = 0x882B
|
||||||
|
SensitivityType = 0x8830
|
||||||
|
StandardOutputSensitivity = 0x8831
|
||||||
|
RecommendedExposureIndex = 0x8832
|
||||||
|
ISOSpeed = 0x8833
|
||||||
|
ISOSpeedLatitudeyyy = 0x8834
|
||||||
|
ISOSpeedLatitudezzz = 0x8835
|
||||||
|
ExifVersion = 0x9000
|
||||||
|
DateTimeOriginal = 0x9003
|
||||||
|
DateTimeDigitized = 0x9004
|
||||||
|
OffsetTime = 0x9010
|
||||||
|
OffsetTimeOriginal = 0x9011
|
||||||
|
OffsetTimeDigitized = 0x9012
|
||||||
|
ComponentsConfiguration = 0x9101
|
||||||
|
CompressedBitsPerPixel = 0x9102
|
||||||
|
ShutterSpeedValue = 0x9201
|
||||||
|
ApertureValue = 0x9202
|
||||||
|
BrightnessValue = 0x9203
|
||||||
|
ExposureBiasValue = 0x9204
|
||||||
|
MaxApertureValue = 0x9205
|
||||||
|
SubjectDistance = 0x9206
|
||||||
|
MeteringMode = 0x9207
|
||||||
|
LightSource = 0x9208
|
||||||
|
Flash = 0x9209
|
||||||
|
FocalLength = 0x920A
|
||||||
|
Noise = 0x920D
|
||||||
|
ImageNumber = 0x9211
|
||||||
|
SecurityClassification = 0x9212
|
||||||
|
ImageHistory = 0x9213
|
||||||
|
TIFFEPStandardID = 0x9216
|
||||||
|
MakerNote = 0x927C
|
||||||
|
UserComment = 0x9286
|
||||||
|
SubsecTime = 0x9290
|
||||||
|
SubsecTimeOriginal = 0x9291
|
||||||
|
SubsecTimeDigitized = 0x9292
|
||||||
|
AmbientTemperature = 0x9400
|
||||||
|
Humidity = 0x9401
|
||||||
|
Pressure = 0x9402
|
||||||
|
WaterDepth = 0x9403
|
||||||
|
Acceleration = 0x9404
|
||||||
|
CameraElevationAngle = 0x9405
|
||||||
|
XPTitle = 0x9C9B
|
||||||
|
XPComment = 0x9C9C
|
||||||
|
XPAuthor = 0x9C9D
|
||||||
|
XPKeywords = 0x9C9E
|
||||||
|
XPSubject = 0x9C9F
|
||||||
|
FlashPixVersion = 0xA000
|
||||||
|
ColorSpace = 0xA001
|
||||||
|
ExifImageWidth = 0xA002
|
||||||
|
ExifImageHeight = 0xA003
|
||||||
|
RelatedSoundFile = 0xA004
|
||||||
|
ExifInteroperabilityOffset = 0xA005
|
||||||
|
FlashEnergy = 0xA20B
|
||||||
|
SpatialFrequencyResponse = 0xA20C
|
||||||
|
FocalPlaneXResolution = 0xA20E
|
||||||
|
FocalPlaneYResolution = 0xA20F
|
||||||
|
FocalPlaneResolutionUnit = 0xA210
|
||||||
|
SubjectLocation = 0xA214
|
||||||
|
ExposureIndex = 0xA215
|
||||||
|
SensingMethod = 0xA217
|
||||||
|
FileSource = 0xA300
|
||||||
|
SceneType = 0xA301
|
||||||
|
CFAPattern = 0xA302
|
||||||
|
CustomRendered = 0xA401
|
||||||
|
ExposureMode = 0xA402
|
||||||
|
WhiteBalance = 0xA403
|
||||||
|
DigitalZoomRatio = 0xA404
|
||||||
|
FocalLengthIn35mmFilm = 0xA405
|
||||||
|
SceneCaptureType = 0xA406
|
||||||
|
GainControl = 0xA407
|
||||||
|
Contrast = 0xA408
|
||||||
|
Saturation = 0xA409
|
||||||
|
Sharpness = 0xA40A
|
||||||
|
DeviceSettingDescription = 0xA40B
|
||||||
|
SubjectDistanceRange = 0xA40C
|
||||||
|
ImageUniqueID = 0xA420
|
||||||
|
CameraOwnerName = 0xA430
|
||||||
|
BodySerialNumber = 0xA431
|
||||||
|
LensSpecification = 0xA432
|
||||||
|
LensMake = 0xA433
|
||||||
|
LensModel = 0xA434
|
||||||
|
LensSerialNumber = 0xA435
|
||||||
|
CompositeImage = 0xA460
|
||||||
|
CompositeImageCount = 0xA461
|
||||||
|
CompositeImageExposureTimes = 0xA462
|
||||||
|
Gamma = 0xA500
|
||||||
|
PrintImageMatching = 0xC4A5
|
||||||
|
DNGVersion = 0xC612
|
||||||
|
DNGBackwardVersion = 0xC613
|
||||||
|
UniqueCameraModel = 0xC614
|
||||||
|
LocalizedCameraModel = 0xC615
|
||||||
|
CFAPlaneColor = 0xC616
|
||||||
|
CFALayout = 0xC617
|
||||||
|
LinearizationTable = 0xC618
|
||||||
|
BlackLevelRepeatDim = 0xC619
|
||||||
|
BlackLevel = 0xC61A
|
||||||
|
BlackLevelDeltaH = 0xC61B
|
||||||
|
BlackLevelDeltaV = 0xC61C
|
||||||
|
WhiteLevel = 0xC61D
|
||||||
|
DefaultScale = 0xC61E
|
||||||
|
DefaultCropOrigin = 0xC61F
|
||||||
|
DefaultCropSize = 0xC620
|
||||||
|
ColorMatrix1 = 0xC621
|
||||||
|
ColorMatrix2 = 0xC622
|
||||||
|
CameraCalibration1 = 0xC623
|
||||||
|
CameraCalibration2 = 0xC624
|
||||||
|
ReductionMatrix1 = 0xC625
|
||||||
|
ReductionMatrix2 = 0xC626
|
||||||
|
AnalogBalance = 0xC627
|
||||||
|
AsShotNeutral = 0xC628
|
||||||
|
AsShotWhiteXY = 0xC629
|
||||||
|
BaselineExposure = 0xC62A
|
||||||
|
BaselineNoise = 0xC62B
|
||||||
|
BaselineSharpness = 0xC62C
|
||||||
|
BayerGreenSplit = 0xC62D
|
||||||
|
LinearResponseLimit = 0xC62E
|
||||||
|
CameraSerialNumber = 0xC62F
|
||||||
|
LensInfo = 0xC630
|
||||||
|
ChromaBlurRadius = 0xC631
|
||||||
|
AntiAliasStrength = 0xC632
|
||||||
|
ShadowScale = 0xC633
|
||||||
|
DNGPrivateData = 0xC634
|
||||||
|
MakerNoteSafety = 0xC635
|
||||||
|
CalibrationIlluminant1 = 0xC65A
|
||||||
|
CalibrationIlluminant2 = 0xC65B
|
||||||
|
BestQualityScale = 0xC65C
|
||||||
|
RawDataUniqueID = 0xC65D
|
||||||
|
OriginalRawFileName = 0xC68B
|
||||||
|
OriginalRawFileData = 0xC68C
|
||||||
|
ActiveArea = 0xC68D
|
||||||
|
MaskedAreas = 0xC68E
|
||||||
|
AsShotICCProfile = 0xC68F
|
||||||
|
AsShotPreProfileMatrix = 0xC690
|
||||||
|
CurrentICCProfile = 0xC691
|
||||||
|
CurrentPreProfileMatrix = 0xC692
|
||||||
|
ColorimetricReference = 0xC6BF
|
||||||
|
CameraCalibrationSignature = 0xC6F3
|
||||||
|
ProfileCalibrationSignature = 0xC6F4
|
||||||
|
AsShotProfileName = 0xC6F6
|
||||||
|
NoiseReductionApplied = 0xC6F7
|
||||||
|
ProfileName = 0xC6F8
|
||||||
|
ProfileHueSatMapDims = 0xC6F9
|
||||||
|
ProfileHueSatMapData1 = 0xC6FA
|
||||||
|
ProfileHueSatMapData2 = 0xC6FB
|
||||||
|
ProfileToneCurve = 0xC6FC
|
||||||
|
ProfileEmbedPolicy = 0xC6FD
|
||||||
|
ProfileCopyright = 0xC6FE
|
||||||
|
ForwardMatrix1 = 0xC714
|
||||||
|
ForwardMatrix2 = 0xC715
|
||||||
|
PreviewApplicationName = 0xC716
|
||||||
|
PreviewApplicationVersion = 0xC717
|
||||||
|
PreviewSettingsName = 0xC718
|
||||||
|
PreviewSettingsDigest = 0xC719
|
||||||
|
PreviewColorSpace = 0xC71A
|
||||||
|
PreviewDateTime = 0xC71B
|
||||||
|
RawImageDigest = 0xC71C
|
||||||
|
OriginalRawFileDigest = 0xC71D
|
||||||
|
SubTileBlockSize = 0xC71E
|
||||||
|
RowInterleaveFactor = 0xC71F
|
||||||
|
ProfileLookTableDims = 0xC725
|
||||||
|
ProfileLookTableData = 0xC726
|
||||||
|
OpcodeList1 = 0xC740
|
||||||
|
OpcodeList2 = 0xC741
|
||||||
|
OpcodeList3 = 0xC74E
|
||||||
|
NoiseProfile = 0xC761
|
||||||
|
FrameRate = 0xC764
|
||||||
|
|
||||||
|
|
||||||
|
"""Maps EXIF tags to tag names."""
|
||||||
|
TAGS = {
|
||||||
|
**{i.value: i.name for i in Base},
|
||||||
|
0x920C: "SpatialFrequencyResponse",
|
||||||
|
0x9214: "SubjectLocation",
|
||||||
|
0x9215: "ExposureIndex",
|
||||||
|
0x828E: "CFAPattern",
|
||||||
|
0x920B: "FlashEnergy",
|
||||||
|
0x9216: "TIFF/EPStandardID",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GPS(IntEnum):
|
||||||
|
GPSVersionID = 0x00
|
||||||
|
GPSLatitudeRef = 0x01
|
||||||
|
GPSLatitude = 0x02
|
||||||
|
GPSLongitudeRef = 0x03
|
||||||
|
GPSLongitude = 0x04
|
||||||
|
GPSAltitudeRef = 0x05
|
||||||
|
GPSAltitude = 0x06
|
||||||
|
GPSTimeStamp = 0x07
|
||||||
|
GPSSatellites = 0x08
|
||||||
|
GPSStatus = 0x09
|
||||||
|
GPSMeasureMode = 0x0A
|
||||||
|
GPSDOP = 0x0B
|
||||||
|
GPSSpeedRef = 0x0C
|
||||||
|
GPSSpeed = 0x0D
|
||||||
|
GPSTrackRef = 0x0E
|
||||||
|
GPSTrack = 0x0F
|
||||||
|
GPSImgDirectionRef = 0x10
|
||||||
|
GPSImgDirection = 0x11
|
||||||
|
GPSMapDatum = 0x12
|
||||||
|
GPSDestLatitudeRef = 0x13
|
||||||
|
GPSDestLatitude = 0x14
|
||||||
|
GPSDestLongitudeRef = 0x15
|
||||||
|
GPSDestLongitude = 0x16
|
||||||
|
GPSDestBearingRef = 0x17
|
||||||
|
GPSDestBearing = 0x18
|
||||||
|
GPSDestDistanceRef = 0x19
|
||||||
|
GPSDestDistance = 0x1A
|
||||||
|
GPSProcessingMethod = 0x1B
|
||||||
|
GPSAreaInformation = 0x1C
|
||||||
|
GPSDateStamp = 0x1D
|
||||||
|
GPSDifferential = 0x1E
|
||||||
|
GPSHPositioningError = 0x1F
|
||||||
|
|
||||||
|
|
||||||
|
"""Maps EXIF GPS tags to tag names."""
|
||||||
|
GPSTAGS = {i.value: i.name for i in GPS}
|
||||||
|
|
||||||
|
|
||||||
|
class Interop(IntEnum):
|
||||||
|
InteropIndex = 0x0001
|
||||||
|
InteropVersion = 0x0002
|
||||||
|
RelatedImageFileFormat = 0x1000
|
||||||
|
RelatedImageWidth = 0x1001
|
||||||
|
RelatedImageHeight = 0x1002
|
||||||
|
|
||||||
|
|
||||||
|
class IFD(IntEnum):
|
||||||
|
Exif = 0x8769
|
||||||
|
GPSInfo = 0x8825
|
||||||
|
MakerNote = 0x927C
|
||||||
|
Makernote = 0x927C # Deprecated
|
||||||
|
Interop = 0xA005
|
||||||
|
IFD1 = -1
|
||||||
|
|
||||||
|
|
||||||
|
class LightSource(IntEnum):
|
||||||
|
Unknown = 0x00
|
||||||
|
Daylight = 0x01
|
||||||
|
Fluorescent = 0x02
|
||||||
|
Tungsten = 0x03
|
||||||
|
Flash = 0x04
|
||||||
|
Fine = 0x09
|
||||||
|
Cloudy = 0x0A
|
||||||
|
Shade = 0x0B
|
||||||
|
DaylightFluorescent = 0x0C
|
||||||
|
DayWhiteFluorescent = 0x0D
|
||||||
|
CoolWhiteFluorescent = 0x0E
|
||||||
|
WhiteFluorescent = 0x0F
|
||||||
|
StandardLightA = 0x11
|
||||||
|
StandardLightB = 0x12
|
||||||
|
StandardLightC = 0x13
|
||||||
|
D55 = 0x14
|
||||||
|
D65 = 0x15
|
||||||
|
D75 = 0x16
|
||||||
|
D50 = 0x17
|
||||||
|
ISO = 0x18
|
||||||
|
Other = 0xFF
|
||||||
153
venv/lib/python3.11/site-packages/PIL/FitsImagePlugin.py
Normal file
153
venv/lib/python3.11/site-packages/PIL/FitsImagePlugin.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# FITS file handling
|
||||||
|
#
|
||||||
|
# Copyright (c) 1998-2003 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
import math
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(b"SIMPLE")
|
||||||
|
|
||||||
|
|
||||||
|
class FitsImageFile(ImageFile.ImageFile):
|
||||||
|
format = "FITS"
|
||||||
|
format_description = "FITS"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
|
||||||
|
headers: dict[bytes, bytes] = {}
|
||||||
|
header_in_progress = False
|
||||||
|
decoder_name = ""
|
||||||
|
while True:
|
||||||
|
header = self.fp.read(80)
|
||||||
|
if not header:
|
||||||
|
msg = "Truncated FITS file"
|
||||||
|
raise OSError(msg)
|
||||||
|
keyword = header[:8].strip()
|
||||||
|
if keyword in (b"SIMPLE", b"XTENSION"):
|
||||||
|
header_in_progress = True
|
||||||
|
elif headers and not header_in_progress:
|
||||||
|
# This is now a data unit
|
||||||
|
break
|
||||||
|
elif keyword == b"END":
|
||||||
|
# Seek to the end of the header unit
|
||||||
|
self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
|
||||||
|
if not decoder_name:
|
||||||
|
decoder_name, offset, args = self._parse_headers(headers)
|
||||||
|
|
||||||
|
header_in_progress = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if decoder_name:
|
||||||
|
# Keep going to read past the headers
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = header[8:].split(b"/")[0].strip()
|
||||||
|
if value.startswith(b"="):
|
||||||
|
value = value[1:].strip()
|
||||||
|
if not headers and (not _accept(keyword) or value != b"T"):
|
||||||
|
msg = "Not a FITS file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
headers[keyword] = value
|
||||||
|
|
||||||
|
if not decoder_name:
|
||||||
|
msg = "No image data"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
offset += self.fp.tell() - 80
|
||||||
|
self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)]
|
||||||
|
|
||||||
|
def _get_size(
|
||||||
|
self, headers: dict[bytes, bytes], prefix: bytes
|
||||||
|
) -> tuple[int, int] | None:
|
||||||
|
naxis = int(headers[prefix + b"NAXIS"])
|
||||||
|
if naxis == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if naxis == 1:
|
||||||
|
return 1, int(headers[prefix + b"NAXIS1"])
|
||||||
|
else:
|
||||||
|
return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
|
||||||
|
|
||||||
|
def _parse_headers(
|
||||||
|
self, headers: dict[bytes, bytes]
|
||||||
|
) -> tuple[str, int, tuple[str | int, ...]]:
|
||||||
|
prefix = b""
|
||||||
|
decoder_name = "raw"
|
||||||
|
offset = 0
|
||||||
|
if (
|
||||||
|
headers.get(b"XTENSION") == b"'BINTABLE'"
|
||||||
|
and headers.get(b"ZIMAGE") == b"T"
|
||||||
|
and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
|
||||||
|
):
|
||||||
|
no_prefix_size = self._get_size(headers, prefix) or (0, 0)
|
||||||
|
number_of_bits = int(headers[b"BITPIX"])
|
||||||
|
offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
|
||||||
|
|
||||||
|
prefix = b"Z"
|
||||||
|
decoder_name = "fits_gzip"
|
||||||
|
|
||||||
|
size = self._get_size(headers, prefix)
|
||||||
|
if not size:
|
||||||
|
return "", 0, ()
|
||||||
|
|
||||||
|
self._size = size
|
||||||
|
|
||||||
|
number_of_bits = int(headers[prefix + b"BITPIX"])
|
||||||
|
if number_of_bits == 8:
|
||||||
|
self._mode = "L"
|
||||||
|
elif number_of_bits == 16:
|
||||||
|
self._mode = "I;16"
|
||||||
|
elif number_of_bits == 32:
|
||||||
|
self._mode = "I"
|
||||||
|
elif number_of_bits in (-32, -64):
|
||||||
|
self._mode = "F"
|
||||||
|
|
||||||
|
args: tuple[str | int, ...]
|
||||||
|
if decoder_name == "raw":
|
||||||
|
args = (self.mode, 0, -1)
|
||||||
|
else:
|
||||||
|
args = (number_of_bits,)
|
||||||
|
return decoder_name, offset, args
|
||||||
|
|
||||||
|
|
||||||
|
class FitsGzipDecoder(ImageFile.PyDecoder):
|
||||||
|
_pulls_fd = True
|
||||||
|
|
||||||
|
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||||
|
assert self.fd is not None
|
||||||
|
with gzip.open(self.fd) as fp:
|
||||||
|
value = fp.read(self.state.xsize * self.state.ysize * 4)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
offset = 0
|
||||||
|
number_of_bits = min(self.args[0] // 8, 4)
|
||||||
|
for y in range(self.state.ysize):
|
||||||
|
row = bytearray()
|
||||||
|
for x in range(self.state.xsize):
|
||||||
|
row += value[offset + (4 - number_of_bits) : offset + 4]
|
||||||
|
offset += 4
|
||||||
|
rows.append(row)
|
||||||
|
self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
|
||||||
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Registry
|
||||||
|
|
||||||
|
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
|
||||||
|
Image.register_decoder("fits_gzip", FitsGzipDecoder)
|
||||||
|
|
||||||
|
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])
|
||||||
184
venv/lib/python3.11/site-packages/PIL/FliImagePlugin.py
Normal file
184
venv/lib/python3.11/site-packages/PIL/FliImagePlugin.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# FLI/FLC file handling.
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 95-09-01 fl Created
|
||||||
|
# 97-01-03 fl Fixed parser, setup decoder tile
|
||||||
|
# 98-07-15 fl Renamed offset attribute to avoid name clash
|
||||||
|
#
|
||||||
|
# Copyright (c) Secret Labs AB 1997-98.
|
||||||
|
# Copyright (c) Fredrik Lundh 1995-97.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import Image, ImageFile, ImagePalette
|
||||||
|
from ._binary import i16le as i16
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
from ._binary import o8
|
||||||
|
from ._util import DeferredError
|
||||||
|
|
||||||
|
#
|
||||||
|
# decoder
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return (
|
||||||
|
len(prefix) >= 16
|
||||||
|
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||||
|
and i16(prefix, 14) in [0, 3] # flags
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Image plugin for the FLI/FLC animation format. Use the <b>seek</b>
|
||||||
|
# method to load individual frames.
|
||||||
|
|
||||||
|
|
||||||
|
class FliImageFile(ImageFile.ImageFile):
|
||||||
|
format = "FLI"
|
||||||
|
format_description = "Autodesk FLI/FLC Animation"
|
||||||
|
_close_exclusive_fp_after_loading = False
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
# HEAD
|
||||||
|
assert self.fp is not None
|
||||||
|
s = self.fp.read(128)
|
||||||
|
if not (
|
||||||
|
_accept(s)
|
||||||
|
and s[20:22] == b"\x00" * 2
|
||||||
|
and s[42:80] == b"\x00" * 38
|
||||||
|
and s[88:] == b"\x00" * 40
|
||||||
|
):
|
||||||
|
msg = "not an FLI/FLC file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
# frames
|
||||||
|
self.n_frames = i16(s, 6)
|
||||||
|
self.is_animated = self.n_frames > 1
|
||||||
|
|
||||||
|
# image characteristics
|
||||||
|
self._mode = "P"
|
||||||
|
self._size = i16(s, 8), i16(s, 10)
|
||||||
|
|
||||||
|
# animation speed
|
||||||
|
duration = i32(s, 16)
|
||||||
|
magic = i16(s, 4)
|
||||||
|
if magic == 0xAF11:
|
||||||
|
duration = (duration * 1000) // 70
|
||||||
|
self.info["duration"] = duration
|
||||||
|
|
||||||
|
# look for palette
|
||||||
|
palette = [(a, a, a) for a in range(256)]
|
||||||
|
|
||||||
|
s = self.fp.read(16)
|
||||||
|
|
||||||
|
self.__offset = 128
|
||||||
|
|
||||||
|
if i16(s, 4) == 0xF100:
|
||||||
|
# prefix chunk; ignore it
|
||||||
|
self.fp.seek(self.__offset + i32(s))
|
||||||
|
s = self.fp.read(16)
|
||||||
|
|
||||||
|
if i16(s, 4) == 0xF1FA:
|
||||||
|
# look for palette chunk
|
||||||
|
number_of_subchunks = i16(s, 6)
|
||||||
|
chunk_size: int | None = None
|
||||||
|
for _ in range(number_of_subchunks):
|
||||||
|
if chunk_size is not None:
|
||||||
|
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
|
||||||
|
s = self.fp.read(6)
|
||||||
|
chunk_type = i16(s, 4)
|
||||||
|
if chunk_type in (4, 11):
|
||||||
|
self._palette(palette, 2 if chunk_type == 11 else 0)
|
||||||
|
break
|
||||||
|
chunk_size = i32(s)
|
||||||
|
if not chunk_size:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.palette = ImagePalette.raw(
|
||||||
|
"RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette)
|
||||||
|
)
|
||||||
|
|
||||||
|
# set things up to decode first frame
|
||||||
|
self.__frame = -1
|
||||||
|
self._fp = self.fp
|
||||||
|
self.__rewind = self.fp.tell()
|
||||||
|
self.seek(0)
|
||||||
|
|
||||||
|
def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None:
|
||||||
|
# load palette
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
assert self.fp is not None
|
||||||
|
for e in range(i16(self.fp.read(2))):
|
||||||
|
s = self.fp.read(2)
|
||||||
|
i = i + s[0]
|
||||||
|
n = s[1]
|
||||||
|
if n == 0:
|
||||||
|
n = 256
|
||||||
|
s = self.fp.read(n * 3)
|
||||||
|
for n in range(0, len(s), 3):
|
||||||
|
r = s[n] << shift
|
||||||
|
g = s[n + 1] << shift
|
||||||
|
b = s[n + 2] << shift
|
||||||
|
palette[i] = (r, g, b)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def seek(self, frame: int) -> None:
|
||||||
|
if not self._seek_check(frame):
|
||||||
|
return
|
||||||
|
if frame < self.__frame:
|
||||||
|
self._seek(0)
|
||||||
|
|
||||||
|
for f in range(self.__frame + 1, frame + 1):
|
||||||
|
self._seek(f)
|
||||||
|
|
||||||
|
def _seek(self, frame: int) -> None:
|
||||||
|
if isinstance(self._fp, DeferredError):
|
||||||
|
raise self._fp.ex
|
||||||
|
if frame == 0:
|
||||||
|
self.__frame = -1
|
||||||
|
self._fp.seek(self.__rewind)
|
||||||
|
self.__offset = 128
|
||||||
|
else:
|
||||||
|
# ensure that the previous frame was loaded
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
if frame != self.__frame + 1:
|
||||||
|
msg = f"cannot seek to frame {frame}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
self.__frame = frame
|
||||||
|
|
||||||
|
# move to next frame
|
||||||
|
self.fp = self._fp
|
||||||
|
self.fp.seek(self.__offset)
|
||||||
|
|
||||||
|
s = self.fp.read(4)
|
||||||
|
if not s:
|
||||||
|
msg = "missing frame size"
|
||||||
|
raise EOFError(msg)
|
||||||
|
|
||||||
|
framesize = i32(s)
|
||||||
|
|
||||||
|
self.decodermaxblock = framesize
|
||||||
|
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)]
|
||||||
|
|
||||||
|
self.__offset += framesize
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
return self.__frame
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# registry
|
||||||
|
|
||||||
|
Image.register_open(FliImageFile.format, FliImageFile, _accept)
|
||||||
|
|
||||||
|
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])
|
||||||
159
venv/lib/python3.11/site-packages/PIL/FontFile.py
Normal file
159
venv/lib/python3.11/site-packages/PIL/FontFile.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# base class for raster font file parsers
|
||||||
|
#
|
||||||
|
# history:
|
||||||
|
# 1997-06-05 fl created
|
||||||
|
# 1997-08-19 fl restrict image width
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997-1998 by Secret Labs AB
|
||||||
|
# Copyright (c) 1997-1998 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from . import Image, ImageFont, _binary
|
||||||
|
|
||||||
|
WIDTH = 800
|
||||||
|
|
||||||
|
|
||||||
|
def puti16(
|
||||||
|
fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Write network order (big-endian) 16-bit sequence"""
|
||||||
|
for v in values:
|
||||||
|
if v < 0:
|
||||||
|
v += 65536
|
||||||
|
fp.write(_binary.o16be(v))
|
||||||
|
|
||||||
|
|
||||||
|
class FontFile:
|
||||||
|
"""Base class for raster font file handlers."""
|
||||||
|
|
||||||
|
bitmap: Image.Image | None = None
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.info: dict[bytes, bytes | int] = {}
|
||||||
|
self.glyph: list[
|
||||||
|
tuple[
|
||||||
|
tuple[int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
Image.Image,
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
] = [None] * 256
|
||||||
|
|
||||||
|
def __getitem__(self, ix: int) -> (
|
||||||
|
tuple[
|
||||||
|
tuple[int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
tuple[int, int, int, int],
|
||||||
|
Image.Image,
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
):
|
||||||
|
return self.glyph[ix]
|
||||||
|
|
||||||
|
def compile(self) -> None:
|
||||||
|
"""Create metrics and bitmap"""
|
||||||
|
|
||||||
|
if self.bitmap:
|
||||||
|
return
|
||||||
|
|
||||||
|
# create bitmap large enough to hold all data
|
||||||
|
h = w = maxwidth = 0
|
||||||
|
lines = 1
|
||||||
|
for glyph in self.glyph:
|
||||||
|
if glyph:
|
||||||
|
d, dst, src, im = glyph
|
||||||
|
h = max(h, src[3] - src[1])
|
||||||
|
w = w + (src[2] - src[0])
|
||||||
|
if w > WIDTH:
|
||||||
|
lines += 1
|
||||||
|
w = src[2] - src[0]
|
||||||
|
maxwidth = max(maxwidth, w)
|
||||||
|
|
||||||
|
xsize = maxwidth
|
||||||
|
ysize = lines * h
|
||||||
|
|
||||||
|
if xsize == 0 and ysize == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ysize = h
|
||||||
|
|
||||||
|
# paste glyphs into bitmap
|
||||||
|
self.bitmap = Image.new("1", (xsize, ysize))
|
||||||
|
self.metrics: list[
|
||||||
|
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
|
||||||
|
| None
|
||||||
|
] = [None] * 256
|
||||||
|
x = y = 0
|
||||||
|
for i in range(256):
|
||||||
|
glyph = self[i]
|
||||||
|
if glyph:
|
||||||
|
d, dst, src, im = glyph
|
||||||
|
xx = src[2] - src[0]
|
||||||
|
x0, y0 = x, y
|
||||||
|
x = x + xx
|
||||||
|
if x > WIDTH:
|
||||||
|
x, y = 0, y + h
|
||||||
|
x0, y0 = x, y
|
||||||
|
x = xx
|
||||||
|
s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0
|
||||||
|
self.bitmap.paste(im.crop(src), s)
|
||||||
|
self.metrics[i] = d, dst, s
|
||||||
|
|
||||||
|
def _encode_metrics(self) -> bytes:
|
||||||
|
values: list[int] = []
|
||||||
|
for i in range(256):
|
||||||
|
m = self.metrics[i]
|
||||||
|
if m:
|
||||||
|
values.extend(m[0] + m[1] + m[2])
|
||||||
|
else:
|
||||||
|
values.extend((0,) * 10)
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
for v in values:
|
||||||
|
if v < 0:
|
||||||
|
v += 65536
|
||||||
|
data += _binary.o16be(v)
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def save(self, filename: str) -> None:
|
||||||
|
"""Save font"""
|
||||||
|
|
||||||
|
self.compile()
|
||||||
|
|
||||||
|
# font data
|
||||||
|
if not self.bitmap:
|
||||||
|
msg = "No bitmap created"
|
||||||
|
raise ValueError(msg)
|
||||||
|
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
|
||||||
|
|
||||||
|
# font metrics
|
||||||
|
with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp:
|
||||||
|
fp.write(b"PILfont\n")
|
||||||
|
fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!!
|
||||||
|
fp.write(b"DATA\n")
|
||||||
|
fp.write(self._encode_metrics())
|
||||||
|
|
||||||
|
def to_imagefont(self) -> ImageFont.ImageFont:
|
||||||
|
"""Convert to ImageFont"""
|
||||||
|
|
||||||
|
self.compile()
|
||||||
|
|
||||||
|
# font data
|
||||||
|
if not self.bitmap:
|
||||||
|
msg = "No bitmap created"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
imagefont = ImageFont.ImageFont()
|
||||||
|
imagefont._load(self.bitmap, self._encode_metrics())
|
||||||
|
return imagefont
|
||||||
258
venv/lib/python3.11/site-packages/PIL/FpxImagePlugin.py
Normal file
258
venv/lib/python3.11/site-packages/PIL/FpxImagePlugin.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
#
|
||||||
|
# THIS IS WORK IN PROGRESS
|
||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# FlashPix support for PIL
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 97-01-25 fl Created (reads uncompressed RGB images only)
|
||||||
|
#
|
||||||
|
# Copyright (c) Secret Labs AB 1997.
|
||||||
|
# Copyright (c) Fredrik Lundh 1997.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import olefile
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
from ._binary import i32le as i32
|
||||||
|
|
||||||
|
# we map from colour field tuples to (mode, rawmode) descriptors
|
||||||
|
MODES = {
|
||||||
|
# opacity
|
||||||
|
(0x00007FFE,): ("A", "L"),
|
||||||
|
# monochrome
|
||||||
|
(0x00010000,): ("L", "L"),
|
||||||
|
(0x00018000, 0x00017FFE): ("RGBA", "LA"),
|
||||||
|
# photo YCC
|
||||||
|
(0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
|
||||||
|
(0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"),
|
||||||
|
# standard RGB (NIFRGB)
|
||||||
|
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
|
||||||
|
(0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(olefile.MAGIC)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Image plugin for the FlashPix images.
|
||||||
|
|
||||||
|
|
||||||
|
class FpxImageFile(ImageFile.ImageFile):
|
||||||
|
format = "FPX"
|
||||||
|
format_description = "FlashPix"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
#
|
||||||
|
# read the OLE directory and see if this is a likely
|
||||||
|
# to be a FlashPix file
|
||||||
|
|
||||||
|
assert self.fp is not None
|
||||||
|
try:
|
||||||
|
self.ole = olefile.OleFileIO(self.fp)
|
||||||
|
except OSError as e:
|
||||||
|
msg = "not an FPX file; invalid OLE file"
|
||||||
|
raise SyntaxError(msg) from e
|
||||||
|
|
||||||
|
root = self.ole.root
|
||||||
|
if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
|
||||||
|
msg = "not an FPX file; bad root CLSID"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self._open_index(1)
|
||||||
|
|
||||||
|
def _open_index(self, index: int = 1) -> None:
|
||||||
|
#
|
||||||
|
# get the Image Contents Property Set
|
||||||
|
|
||||||
|
prop = self.ole.getproperties(
|
||||||
|
[f"Data Object Store {index:06d}", "\005Image Contents"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# size (highest resolution)
|
||||||
|
|
||||||
|
assert isinstance(prop[0x1000002], int)
|
||||||
|
assert isinstance(prop[0x1000003], int)
|
||||||
|
self._size = prop[0x1000002], prop[0x1000003]
|
||||||
|
|
||||||
|
size = max(self.size)
|
||||||
|
i = 1
|
||||||
|
while size > 64:
|
||||||
|
size = size // 2
|
||||||
|
i += 1
|
||||||
|
self.maxid = i - 1
|
||||||
|
|
||||||
|
# mode. instead of using a single field for this, flashpix
|
||||||
|
# requires you to specify the mode for each channel in each
|
||||||
|
# resolution subimage, and leaves it to the decoder to make
|
||||||
|
# sure that they all match. for now, we'll cheat and assume
|
||||||
|
# that this is always the case.
|
||||||
|
|
||||||
|
id = self.maxid << 16
|
||||||
|
|
||||||
|
s = prop[0x2000002 | id]
|
||||||
|
|
||||||
|
if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
|
||||||
|
msg = "Invalid number of bands"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# note: for now, we ignore the "uncalibrated" flag
|
||||||
|
colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands))
|
||||||
|
|
||||||
|
self._mode, self.rawmode = MODES[colors]
|
||||||
|
|
||||||
|
# load JPEG tables, if any
|
||||||
|
self.jpeg = {}
|
||||||
|
for i in range(256):
|
||||||
|
id = 0x3000001 | (i << 16)
|
||||||
|
if id in prop:
|
||||||
|
self.jpeg[i] = prop[id]
|
||||||
|
|
||||||
|
self._open_subimage(1, self.maxid)
|
||||||
|
|
||||||
|
def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
|
||||||
|
#
|
||||||
|
# setup tile descriptors for a given subimage
|
||||||
|
|
||||||
|
stream = [
|
||||||
|
f"Data Object Store {index:06d}",
|
||||||
|
f"Resolution {subimage:04d}",
|
||||||
|
"Subimage 0000 Header",
|
||||||
|
]
|
||||||
|
|
||||||
|
fp = self.ole.openstream(stream)
|
||||||
|
|
||||||
|
# skip prefix
|
||||||
|
fp.read(28)
|
||||||
|
|
||||||
|
# header stream
|
||||||
|
s = fp.read(36)
|
||||||
|
|
||||||
|
size = i32(s, 4), i32(s, 8)
|
||||||
|
# tilecount = i32(s, 12)
|
||||||
|
xtile, ytile = i32(s, 16), i32(s, 20)
|
||||||
|
# channels = i32(s, 24)
|
||||||
|
offset = i32(s, 28)
|
||||||
|
length = i32(s, 32)
|
||||||
|
|
||||||
|
if size != self.size:
|
||||||
|
msg = "subimage mismatch"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# get tile descriptors
|
||||||
|
fp.seek(28 + offset)
|
||||||
|
s = fp.read(i32(s, 12) * length)
|
||||||
|
|
||||||
|
x = y = 0
|
||||||
|
xsize, ysize = size
|
||||||
|
self.tile = []
|
||||||
|
|
||||||
|
for i in range(0, len(s), length):
|
||||||
|
x1 = min(xsize, x + xtile)
|
||||||
|
y1 = min(ysize, y + ytile)
|
||||||
|
|
||||||
|
compression = i32(s, i + 8)
|
||||||
|
|
||||||
|
if compression == 0:
|
||||||
|
self.tile.append(
|
||||||
|
ImageFile._Tile(
|
||||||
|
"raw",
|
||||||
|
(x, y, x1, y1),
|
||||||
|
i32(s, i) + 28,
|
||||||
|
self.rawmode,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif compression == 1:
|
||||||
|
# FIXME: the fill decoder is not implemented
|
||||||
|
self.tile.append(
|
||||||
|
ImageFile._Tile(
|
||||||
|
"fill",
|
||||||
|
(x, y, x1, y1),
|
||||||
|
i32(s, i) + 28,
|
||||||
|
(self.rawmode, s[12:16]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif compression == 2:
|
||||||
|
internal_color_conversion = s[14]
|
||||||
|
jpeg_tables = s[15]
|
||||||
|
rawmode = self.rawmode
|
||||||
|
|
||||||
|
if internal_color_conversion:
|
||||||
|
# The image is stored as usual (usually YCbCr).
|
||||||
|
if rawmode == "RGBA":
|
||||||
|
# For "RGBA", data is stored as YCbCrA based on
|
||||||
|
# negative RGB. The following trick works around
|
||||||
|
# this problem :
|
||||||
|
jpegmode, rawmode = "YCbCrK", "CMYK"
|
||||||
|
else:
|
||||||
|
jpegmode = None # let the decoder decide
|
||||||
|
|
||||||
|
else:
|
||||||
|
# The image is stored as defined by rawmode
|
||||||
|
jpegmode = rawmode
|
||||||
|
|
||||||
|
self.tile.append(
|
||||||
|
ImageFile._Tile(
|
||||||
|
"jpeg",
|
||||||
|
(x, y, x1, y1),
|
||||||
|
i32(s, i) + 28,
|
||||||
|
(rawmode, jpegmode),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: jpeg tables are tile dependent; the prefix
|
||||||
|
# data must be placed in the tile descriptor itself!
|
||||||
|
|
||||||
|
if jpeg_tables:
|
||||||
|
self.tile_prefix = self.jpeg[jpeg_tables]
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = "unknown/invalid compression"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
x += xtile
|
||||||
|
if x >= xsize:
|
||||||
|
x, y = 0, y + ytile
|
||||||
|
if y >= ysize:
|
||||||
|
break # isn't really required
|
||||||
|
|
||||||
|
assert self.fp is not None
|
||||||
|
self.stream = stream
|
||||||
|
self._fp = self.fp
|
||||||
|
self.fp = None
|
||||||
|
|
||||||
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
|
if not self.fp:
|
||||||
|
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
|
||||||
|
|
||||||
|
return ImageFile.ImageFile.load(self)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.ole.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
def __exit__(self, *args: object) -> None:
|
||||||
|
self.ole.close()
|
||||||
|
super().__exit__()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
|
||||||
|
|
||||||
|
Image.register_extension(FpxImageFile.format, ".fpx")
|
||||||
115
venv/lib/python3.11/site-packages/PIL/FtexImagePlugin.py
Normal file
115
venv/lib/python3.11/site-packages/PIL/FtexImagePlugin.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
A Pillow loader for .ftc and .ftu files (FTEX)
|
||||||
|
Jerome Leclanche <jerome@leclan.ch>
|
||||||
|
|
||||||
|
The contents of this file are hereby released in the public domain (CC0)
|
||||||
|
Full text of the CC0 license:
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001
|
||||||
|
|
||||||
|
The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a
|
||||||
|
packed custom format called FTEX. This file format uses file extensions FTC
|
||||||
|
and FTU.
|
||||||
|
* FTC files are compressed textures (using standard texture compression).
|
||||||
|
* FTU files are not compressed.
|
||||||
|
Texture File Format
|
||||||
|
The FTC and FTU texture files both use the same format. This
|
||||||
|
has the following structure:
|
||||||
|
{header}
|
||||||
|
{format_directory}
|
||||||
|
{data}
|
||||||
|
Where:
|
||||||
|
{header} = {
|
||||||
|
u32:magic,
|
||||||
|
u32:version,
|
||||||
|
u32:width,
|
||||||
|
u32:height,
|
||||||
|
u32:mipmap_count,
|
||||||
|
u32:format_count
|
||||||
|
}
|
||||||
|
|
||||||
|
* The "magic" number is "FTEX".
|
||||||
|
* "width" and "height" are the dimensions of the texture.
|
||||||
|
* "mipmap_count" is the number of mipmaps in the texture.
|
||||||
|
* "format_count" is the number of texture formats (different versions of the
|
||||||
|
same texture) in this file.
|
||||||
|
|
||||||
|
{format_directory} = format_count * { u32:format, u32:where }
|
||||||
|
|
||||||
|
The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB
|
||||||
|
uncompressed textures.
|
||||||
|
The texture data for a format starts at the position "where" in the file.
|
||||||
|
|
||||||
|
Each set of texture data in the file has the following structure:
|
||||||
|
{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } }
|
||||||
|
* "mipmap_size" is the number of bytes in that mip level. For compressed
|
||||||
|
textures this is the size of the texture data compressed with DXT1. For 24 bit
|
||||||
|
uncompressed textures, this is 3 * width * height. Following this are the image
|
||||||
|
bytes for that mipmap level.
|
||||||
|
|
||||||
|
Note: All data is stored in little-Endian (Intel) byte order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from enum import IntEnum
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
MAGIC = b"FTEX"
|
||||||
|
|
||||||
|
|
||||||
|
class Format(IntEnum):
|
||||||
|
DXT1 = 0
|
||||||
|
UNCOMPRESSED = 1
|
||||||
|
|
||||||
|
|
||||||
|
class FtexImageFile(ImageFile.ImageFile):
|
||||||
|
format = "FTEX"
|
||||||
|
format_description = "Texture File Format (IW2:EOC)"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
if not _accept(self.fp.read(4)):
|
||||||
|
msg = "not an FTEX file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
struct.unpack("<i", self.fp.read(4)) # version
|
||||||
|
self._size = struct.unpack("<2i", self.fp.read(8))
|
||||||
|
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
||||||
|
|
||||||
|
# Only support single-format files.
|
||||||
|
# I don't know of any multi-format file.
|
||||||
|
assert format_count == 1
|
||||||
|
|
||||||
|
format, where = struct.unpack("<2i", self.fp.read(8))
|
||||||
|
self.fp.seek(where)
|
||||||
|
(mipmap_size,) = struct.unpack("<i", self.fp.read(4))
|
||||||
|
|
||||||
|
data = self.fp.read(mipmap_size)
|
||||||
|
|
||||||
|
if format == Format.DXT1:
|
||||||
|
self._mode = "RGBA"
|
||||||
|
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
|
||||||
|
elif format == Format.UNCOMPRESSED:
|
||||||
|
self._mode = "RGB"
|
||||||
|
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
|
||||||
|
else:
|
||||||
|
msg = f"Invalid texture compression format: {repr(format)}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
self.fp.close()
|
||||||
|
self.fp = BytesIO(data)
|
||||||
|
|
||||||
|
def load_seek(self, pos: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(MAGIC)
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)
|
||||||
|
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])
|
||||||
103
venv/lib/python3.11/site-packages/PIL/GbrImagePlugin.py
Normal file
103
venv/lib/python3.11/site-packages/PIL/GbrImagePlugin.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
#
|
||||||
|
# load a GIMP brush file
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 96-03-14 fl Created
|
||||||
|
# 16-01-08 es Version 2
|
||||||
|
#
|
||||||
|
# Copyright (c) Secret Labs AB 1997.
|
||||||
|
# Copyright (c) Fredrik Lundh 1996.
|
||||||
|
# Copyright (c) Eric Soroos 2016.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for
|
||||||
|
# format documentation.
|
||||||
|
#
|
||||||
|
# This code Interprets version 1 and 2 .gbr files.
|
||||||
|
# Version 1 files are obsolete, and should not be used for new
|
||||||
|
# brushes.
|
||||||
|
# Version 2 files are saved by GIMP v2.8 (at least)
|
||||||
|
# Version 3 files have a format specifier of 18 for 16bit floats in
|
||||||
|
# the color depth field. This is currently unsupported by Pillow.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
from ._binary import i32be as i32
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Image plugin for the GIMP brush format.
|
||||||
|
|
||||||
|
|
||||||
|
class GbrImageFile(ImageFile.ImageFile):
|
||||||
|
format = "GBR"
|
||||||
|
format_description = "GIMP brush file"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
header_size = i32(self.fp.read(4))
|
||||||
|
if header_size < 20:
|
||||||
|
msg = "not a GIMP brush"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
version = i32(self.fp.read(4))
|
||||||
|
if version not in (1, 2):
|
||||||
|
msg = f"Unsupported GIMP brush version: {version}"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
width = i32(self.fp.read(4))
|
||||||
|
height = i32(self.fp.read(4))
|
||||||
|
color_depth = i32(self.fp.read(4))
|
||||||
|
if width == 0 or height == 0:
|
||||||
|
msg = "not a GIMP brush"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
if color_depth not in (1, 4):
|
||||||
|
msg = f"Unsupported GIMP brush color depth: {color_depth}"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
if version == 1:
|
||||||
|
comment_length = header_size - 20
|
||||||
|
else:
|
||||||
|
comment_length = header_size - 28
|
||||||
|
magic_number = self.fp.read(4)
|
||||||
|
if magic_number != b"GIMP":
|
||||||
|
msg = "not a GIMP brush, bad magic number"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
self.info["spacing"] = i32(self.fp.read(4))
|
||||||
|
|
||||||
|
self.info["comment"] = self.fp.read(comment_length)[:-1]
|
||||||
|
|
||||||
|
if color_depth == 1:
|
||||||
|
self._mode = "L"
|
||||||
|
else:
|
||||||
|
self._mode = "RGBA"
|
||||||
|
|
||||||
|
self._size = width, height
|
||||||
|
|
||||||
|
# Image might not be small
|
||||||
|
Image._decompression_bomb_check(self.size)
|
||||||
|
|
||||||
|
# Data is an uncompressed block of w * h * bytes/pixel
|
||||||
|
self._data_size = width * height * color_depth
|
||||||
|
|
||||||
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
|
if self._im is None:
|
||||||
|
assert self.fp is not None
|
||||||
|
self.im = Image.core.new(self.mode, self.size)
|
||||||
|
self.frombytes(self.fp.read(self._data_size))
|
||||||
|
return Image.Image.load(self)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# registry
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
|
||||||
|
Image.register_extension(GbrImageFile.format, ".gbr")
|
||||||
103
venv/lib/python3.11/site-packages/PIL/GdImageFile.py
Normal file
103
venv/lib/python3.11/site-packages/PIL/GdImageFile.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# GD file handling
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1996-04-12 fl Created
|
||||||
|
#
|
||||||
|
# Copyright (c) 1997 by Secret Labs AB.
|
||||||
|
# Copyright (c) 1996 by Fredrik Lundh.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
.. note::
|
||||||
|
This format cannot be automatically recognized, so the
|
||||||
|
class is not registered for use with :py:func:`PIL.Image.open()`. To open a
|
||||||
|
gd file, use the :py:func:`PIL.GdImageFile.open()` function instead.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
|
||||||
|
implementation is provided for convenience and demonstrational
|
||||||
|
purposes only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import ImageFile, ImagePalette, UnidentifiedImageError
|
||||||
|
from ._binary import i16be as i16
|
||||||
|
from ._binary import i32be as i32
|
||||||
|
from ._typing import StrOrBytesPath
|
||||||
|
|
||||||
|
|
||||||
|
class GdImageFile(ImageFile.ImageFile):
|
||||||
|
"""
|
||||||
|
Image plugin for the GD uncompressed format. Note that this format
|
||||||
|
is not supported by the standard :py:func:`PIL.Image.open()` function. To use
|
||||||
|
this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and
|
||||||
|
use the :py:func:`PIL.GdImageFile.open()` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
format = "GD"
|
||||||
|
format_description = "GD uncompressed images"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
# Header
|
||||||
|
assert self.fp is not None
|
||||||
|
|
||||||
|
s = self.fp.read(1037)
|
||||||
|
|
||||||
|
if i16(s) not in [65534, 65535]:
|
||||||
|
msg = "Not a valid GD 2.x .gd file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self._mode = "P"
|
||||||
|
self._size = i16(s, 2), i16(s, 4)
|
||||||
|
|
||||||
|
true_color = s[6]
|
||||||
|
true_color_offset = 2 if true_color else 0
|
||||||
|
|
||||||
|
# transparency index
|
||||||
|
tindex = i32(s, 7 + true_color_offset)
|
||||||
|
if tindex < 256:
|
||||||
|
self.info["transparency"] = tindex
|
||||||
|
|
||||||
|
self.palette = ImagePalette.raw(
|
||||||
|
"RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tile = [
|
||||||
|
ImageFile._Tile(
|
||||||
|
"raw",
|
||||||
|
(0, 0) + self.size,
|
||||||
|
7 + true_color_offset + 6 + 256 * 4,
|
||||||
|
"L",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile:
|
||||||
|
"""
|
||||||
|
Load texture from a GD image file.
|
||||||
|
|
||||||
|
:param fp: GD file name, or an opened file handle.
|
||||||
|
:param mode: Optional mode. In this version, if the mode argument
|
||||||
|
is given, it must be "r".
|
||||||
|
:returns: An image instance.
|
||||||
|
:raises OSError: If the image could not be read.
|
||||||
|
"""
|
||||||
|
if mode != "r":
|
||||||
|
msg = "bad mode"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return GdImageFile(fp)
|
||||||
|
except SyntaxError as e:
|
||||||
|
msg = "cannot identify this image file"
|
||||||
|
raise UnidentifiedImageError(msg) from e
|
||||||
1223
venv/lib/python3.11/site-packages/PIL/GifImagePlugin.py
Normal file
1223
venv/lib/python3.11/site-packages/PIL/GifImagePlugin.py
Normal file
File diff suppressed because it is too large
Load Diff
154
venv/lib/python3.11/site-packages/PIL/GimpGradientFile.py
Normal file
154
venv/lib/python3.11/site-packages/PIL/GimpGradientFile.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#
|
||||||
|
# Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# stuff to read (and render) GIMP gradient files
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 97-08-23 fl Created
|
||||||
|
#
|
||||||
|
# Copyright (c) Secret Labs AB 1997.
|
||||||
|
# Copyright (c) Fredrik Lundh 1997.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Stuff to translate curve segments to palette values (derived from
|
||||||
|
the corresponding code in GIMP, written by Federico Mena Quintero.
|
||||||
|
See the GIMP distribution for more information.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from math import log, pi, sin, sqrt
|
||||||
|
|
||||||
|
from ._binary import o8
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
EPSILON = 1e-10
|
||||||
|
"""""" # Enable auto-doc for data member
|
||||||
|
|
||||||
|
|
||||||
|
def linear(middle: float, pos: float) -> float:
|
||||||
|
if pos <= middle:
|
||||||
|
if middle < EPSILON:
|
||||||
|
return 0.0
|
||||||
|
else:
|
||||||
|
return 0.5 * pos / middle
|
||||||
|
else:
|
||||||
|
pos = pos - middle
|
||||||
|
middle = 1.0 - middle
|
||||||
|
if middle < EPSILON:
|
||||||
|
return 1.0
|
||||||
|
else:
|
||||||
|
return 0.5 + 0.5 * pos / middle
|
||||||
|
|
||||||
|
|
||||||
|
def curved(middle: float, pos: float) -> float:
|
||||||
|
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
||||||
|
|
||||||
|
|
||||||
|
def sine(middle: float, pos: float) -> float:
|
||||||
|
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def sphere_increasing(middle: float, pos: float) -> float:
|
||||||
|
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def sphere_decreasing(middle: float, pos: float) -> float:
|
||||||
|
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
|
||||||
|
"""""" # Enable auto-doc for data member
|
||||||
|
|
||||||
|
|
||||||
|
class GradientFile:
|
||||||
|
gradient: (
|
||||||
|
list[
|
||||||
|
tuple[
|
||||||
|
float,
|
||||||
|
float,
|
||||||
|
float,
|
||||||
|
list[float],
|
||||||
|
list[float],
|
||||||
|
Callable[[float, float], float],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
) = None
|
||||||
|
|
||||||
|
def getpalette(self, entries: int = 256) -> tuple[bytes, str]:
|
||||||
|
assert self.gradient is not None
|
||||||
|
palette = []
|
||||||
|
|
||||||
|
ix = 0
|
||||||
|
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
||||||
|
|
||||||
|
for i in range(entries):
|
||||||
|
x = i / (entries - 1)
|
||||||
|
|
||||||
|
while x1 < x:
|
||||||
|
ix += 1
|
||||||
|
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
|
||||||
|
|
||||||
|
w = x1 - x0
|
||||||
|
|
||||||
|
if w < EPSILON:
|
||||||
|
scale = segment(0.5, 0.5)
|
||||||
|
else:
|
||||||
|
scale = segment((xm - x0) / w, (x - x0) / w)
|
||||||
|
|
||||||
|
# expand to RGBA
|
||||||
|
r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
|
||||||
|
g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
|
||||||
|
b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
|
||||||
|
a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
|
||||||
|
|
||||||
|
# add to palette
|
||||||
|
palette.append(r + g + b + a)
|
||||||
|
|
||||||
|
return b"".join(palette), "RGBA"
|
||||||
|
|
||||||
|
|
||||||
|
class GimpGradientFile(GradientFile):
|
||||||
|
"""File handler for GIMP's gradient format."""
|
||||||
|
|
||||||
|
def __init__(self, fp: IO[bytes]) -> None:
|
||||||
|
if not fp.readline().startswith(b"GIMP Gradient"):
|
||||||
|
msg = "not a GIMP gradient file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
line = fp.readline()
|
||||||
|
|
||||||
|
# GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
|
||||||
|
if line.startswith(b"Name: "):
|
||||||
|
line = fp.readline().strip()
|
||||||
|
|
||||||
|
count = int(line)
|
||||||
|
|
||||||
|
self.gradient = []
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
s = fp.readline().split()
|
||||||
|
w = [float(x) for x in s[:11]]
|
||||||
|
|
||||||
|
x0, x1 = w[0], w[2]
|
||||||
|
xm = w[1]
|
||||||
|
rgb0 = w[3:7]
|
||||||
|
rgb1 = w[7:11]
|
||||||
|
|
||||||
|
segment = SEGMENTS[int(s[11])]
|
||||||
|
cspace = int(s[12])
|
||||||
|
|
||||||
|
if cspace != 0:
|
||||||
|
msg = "cannot handle HSV colour space"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
|
||||||
75
venv/lib/python3.11/site-packages/PIL/GimpPaletteFile.py
Normal file
75
venv/lib/python3.11/site-packages/PIL/GimpPaletteFile.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#
|
||||||
|
# Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# stuff to read GIMP palette files
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 1997-08-23 fl Created
|
||||||
|
# 2004-09-07 fl Support GIMP 2.0 palette files.
|
||||||
|
#
|
||||||
|
# Copyright (c) Secret Labs AB 1997-2004. All rights reserved.
|
||||||
|
# Copyright (c) Fredrik Lundh 1997-2004.
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
|
||||||
|
class GimpPaletteFile:
|
||||||
|
"""File handler for GIMP's palette format."""
|
||||||
|
|
||||||
|
rawmode = "RGB"
|
||||||
|
|
||||||
|
def _read(self, fp: IO[bytes], limit: bool = True) -> None:
|
||||||
|
if not fp.readline().startswith(b"GIMP Palette"):
|
||||||
|
msg = "not a GIMP palette file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
palette: list[int] = []
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
if limit and i == 256 + 3:
|
||||||
|
break
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
s = fp.readline()
|
||||||
|
if not s:
|
||||||
|
break
|
||||||
|
|
||||||
|
# skip fields and comment lines
|
||||||
|
if re.match(rb"\w+:|#", s):
|
||||||
|
continue
|
||||||
|
if limit and len(s) > 100:
|
||||||
|
msg = "bad palette file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
v = s.split(maxsplit=3)
|
||||||
|
if len(v) < 3:
|
||||||
|
msg = "bad palette entry"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
palette += (int(v[i]) for i in range(3))
|
||||||
|
if limit and len(palette) == 768:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.palette = bytes(palette)
|
||||||
|
|
||||||
|
def __init__(self, fp: IO[bytes]) -> None:
|
||||||
|
self._read(fp)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def frombytes(cls, data: bytes) -> GimpPaletteFile:
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
self._read(BytesIO(data), False)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def getpalette(self) -> tuple[bytes, str]:
|
||||||
|
return self.palette, self.rawmode
|
||||||
72
venv/lib/python3.11/site-packages/PIL/GribStubImagePlugin.py
Normal file
72
venv/lib/python3.11/site-packages/PIL/GribStubImagePlugin.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# GRIB stub adapter
|
||||||
|
#
|
||||||
|
# Copyright (c) 1996-2003 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
_handler = None
|
||||||
|
|
||||||
|
|
||||||
|
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||||
|
"""
|
||||||
|
Install application-specific GRIB image handler.
|
||||||
|
|
||||||
|
:param handler: Handler object.
|
||||||
|
"""
|
||||||
|
global _handler
|
||||||
|
_handler = handler
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Image adapter
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
|
||||||
|
|
||||||
|
|
||||||
|
class GribStubImageFile(ImageFile.StubImageFile):
|
||||||
|
format = "GRIB"
|
||||||
|
format_description = "GRIB"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
if not _accept(self.fp.read(8)):
|
||||||
|
msg = "Not a GRIB file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self.fp.seek(-8, os.SEEK_CUR)
|
||||||
|
|
||||||
|
# make something up
|
||||||
|
self._mode = "F"
|
||||||
|
self._size = 1, 1
|
||||||
|
|
||||||
|
def _load(self) -> ImageFile.StubHandler | None:
|
||||||
|
return _handler
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
if _handler is None or not hasattr(_handler, "save"):
|
||||||
|
msg = "GRIB save handler not installed"
|
||||||
|
raise OSError(msg)
|
||||||
|
_handler.save(im, fp, filename)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Registry
|
||||||
|
|
||||||
|
Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept)
|
||||||
|
Image.register_save(GribStubImageFile.format, _save)
|
||||||
|
|
||||||
|
Image.register_extension(GribStubImageFile.format, ".grib")
|
||||||
72
venv/lib/python3.11/site-packages/PIL/Hdf5StubImagePlugin.py
Normal file
72
venv/lib/python3.11/site-packages/PIL/Hdf5StubImagePlugin.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# The Python Imaging Library
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# HDF5 stub adapter
|
||||||
|
#
|
||||||
|
# Copyright (c) 2000-2003 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
_handler = None
|
||||||
|
|
||||||
|
|
||||||
|
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||||
|
"""
|
||||||
|
Install application-specific HDF5 image handler.
|
||||||
|
|
||||||
|
:param handler: Handler object.
|
||||||
|
"""
|
||||||
|
global _handler
|
||||||
|
_handler = handler
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Image adapter
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
return prefix.startswith(b"\x89HDF\r\n\x1a\n")
|
||||||
|
|
||||||
|
|
||||||
|
class HDF5StubImageFile(ImageFile.StubImageFile):
|
||||||
|
format = "HDF5"
|
||||||
|
format_description = "HDF5"
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
assert self.fp is not None
|
||||||
|
if not _accept(self.fp.read(8)):
|
||||||
|
msg = "Not an HDF file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self.fp.seek(-8, os.SEEK_CUR)
|
||||||
|
|
||||||
|
# make something up
|
||||||
|
self._mode = "F"
|
||||||
|
self._size = 1, 1
|
||||||
|
|
||||||
|
def _load(self) -> ImageFile.StubHandler | None:
|
||||||
|
return _handler
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
if _handler is None or not hasattr(_handler, "save"):
|
||||||
|
msg = "HDF5 save handler not installed"
|
||||||
|
raise OSError(msg)
|
||||||
|
_handler.save(im, fp, filename)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Registry
|
||||||
|
|
||||||
|
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
|
||||||
|
Image.register_save(HDF5StubImageFile.format, _save)
|
||||||
|
|
||||||
|
Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user